EF6学习笔记十三:基础知识完结,零碎问题补缺

要专业系统地学习EF前往《你必须掌握的Entity Framework 6.x与Core 2.0》这本书的作者(汪鹏,Jeffcky)的博客:https://www.cnblogs.com/CreateMyself/

EF6的基础知识就算学完了,书中提供了一个基础篇的实战训练,后面就是进阶内容。市面上专讲EF的书籍就两本吧,《你必须掌握的Entity Framework6.x与Core 2.0》这本可以说很难得了,还是很不错的,值得入手。

这里主要讲一些其他应该注意到的问题。

导航属性与外键属性作为筛选条件查询的区别

分页查询:先筛选后分页、先分页后筛选,执行的SQL语句大不同

语义可空:C#中的空与数据库中的空等价问题

调用表值函数

日期操作应该注意的问题

导航属性与外键属性作为筛选条件查询的区别

 我针对产品表进行查询,我想查询某个订单的产品,我是应该根据导航属性来筛选还是外键属性来筛选呢?

order、product model 注意:BaseEntity不属于EF三大继承策略的任何一种


//  基类
public class BaseEntity
    {
        public BaseEntity()
        {
            this.Id = Guid.NewGuid().ToString();
            this.AddTime = DateTime.Now;
        }
        public string Id { get; set; }
        public DateTime AddTime { get; set; }
    }

//  订单
public class Order:BaseEntity
    {
        public string OrderNO { get; set; }
        public string Description { get; set; }
        public virtual ICollection<Product> Products { get; set; }
    }

//  产品
public class Product : BaseEntity
    {
        public string Name { get; set; }
        public decimal Price { get; set; }
        public string Unit { get; set; }
        public string FK_OrderId { get; set; }
        public virtual Order Order { get; set; }
    }

View Code

映射配置


protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Order>().ToTable("tb_Orders")
                .HasMany(x => x.Products)
                .WithRequired(x => x.Order)
                .HasForeignKey(x => x.FK_OrderId);
            modelBuilder.Entity<Product>().ToTable("tb_Products");
//  移除表名复数契约
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
            base.OnModelCreating(modelBuilder);
        }

View Code

表结构、数据如下

根据导航属性来筛选


//  根据导航属性筛选
var res = ctx.Products.Where(x => x.Order != null).ToList();
Console.WriteLine(JsonConvert.SerializeObject(res, set));

View Code

Sql执行情况如下

 

表里面有六个产品,EF执行了七条SQL语句,第一条,查询出所有的产品,然后逐条产品去筛选

根据外键属性来筛选

//  根据外键属性筛选
var res = ctx.Products.Where(x => !string.IsNullOrEmpty(x.FK_OrderId)).ToList();
Console.WriteLine(JsonConvert.SerializeObject(res, set));

View Code

SQL执情况和上面差不多,也是七条语句

问题在于这里,如果延迟加载被关闭了,那么根据导航属于查询是无效的。因为关闭了延迟加载,导航属性是Null


//  禁用延迟加载
this.Configuration.LazyLoadingEnabled = false;

View Code

 就执行了一条SQL语句,查询所有产品


var res = ctx.Products.Where(x => x.Order != null).ToList();

                //                SELECT
                //    [Extent1].[Id] AS[Id],
                //    [Extent1].[Name] AS[Name],
                //    [Extent1].[Price] AS[Price],
                //    [Extent1].[Unit] AS[Unit],
                //    [Extent1].[FK_OrderId] AS[FK_OrderId],
                //    [Extent1].[AddTime]
                //        AS[AddTime]
                //FROM[dbo].[tb_Products] AS[Extent1]

View Code

 所以,应该按照外键属性来筛选比导航属性要好

分页查询:先筛选后分页、先分页后筛选,执行的SQL语句大不同

我们当然知道分页 ,当然是应该先把数据筛选、处理好,最后再来分页,这就跟先穿袜子后穿鞋子一样自然

这里看看,先分页和后分页,EF执行的SQL语句有何不同

先筛选再分页


//  查询价格小于50的产品
var res = ctx.Products.Where(x => x.Price < 50).OrderBy(x => x.Price).Skip(1).Take(5).ToList();
                Console.WriteLine(JsonConvert.SerializeObject(res,set)); 

View Code


SELECT
    [Extent1].[Id] AS[Id],
    [Extent1].[Name] AS[Name],
    [Extent1].[Price] AS[Price],
    [Extent1].[Unit] AS[Unit],
    [Extent1].[FK_OrderId] AS[FK_OrderId],
    [Extent1].[AddTime]
        AS[AddTime]
FROM[dbo].[tb_Products]
        AS[Extent1]
WHERE[Extent1].[Price] < cast(50 as decimal(18))
    ORDER BY row_number() OVER(ORDER BY [Extent1].[Price] ASC)
    OFFSET 1 ROWS FETCH NEXT 5 ROWS ONLY

View Code

先分页再筛选


var res = ctx.Products.OrderBy(x => x.Price).Skip(1).Take(3).Where(x => x.Price < 50).ToList();

View Code


SELECT
    [Limit1].[Id] AS[Id],
    [Limit1].[Name] AS[Name],
    [Limit1].[Price] AS[Price],
    [Limit1].[Unit] AS[Unit],
    [Limit1].[FK_OrderId] AS[FK_OrderId],
    [Limit1].[AddTime]
        AS[AddTime]
FROM(SELECT[Extent1].[Id] AS[Id], [Extent1].[Name] AS[Name], [Extent1].[Price] AS[Price], [Extent1].[Unit] AS[Unit], [Extent1].[FK_OrderId] AS[FK_OrderId], [Extent1].[AddTime] AS [AddTime]
   FROM [dbo].[tb_Products] AS [Extent1]
   ORDER BY row_number() OVER (ORDER BY [Extent1].[Price] ASC)
        OFFSET 1 ROWS FETCH NEXT 3 ROWS ONLY
   )  AS[Limit1]
    WHERE[Limit1].[Price] < cast(50 as decimal(18))
    ORDER BY[Limit1].[Price] ASC

View Code

明显后面一种的更复杂、难读,原因只是我们得查询方法的位置变了一下

不得不说,LINQ为我们提供了强大的、面向对象的数据查询方法,如果不去关注真正的SQL执行情况,性能问题就在一点点增长。

语义可空

什么意思,就是C#中的空与数据库的空的等价问题,代码一贴就懂了

 我要按照Name属性来查询产品


var res = ctx.Products.Where(x => x.Name == "洗发水").ToList();

View Code


SELECT
     [Extent1].[Id] AS[Id],
     [Extent1].[Name] AS[Name],
     [Extent1].[Price] AS[Price],
     [Extent1].[Unit] AS[Unit],
     [Extent1].[FK_OrderId] AS[FK_OrderId],
     [Extent1].[AddTime]
         AS[AddTime]
 FROM[dbo].[tb_Products]
         AS[Extent1]
 WHERE N'洗发水' = [Extent1].[Name]

View Code

但是现在这样做,我声明一个变量来保存“洗发水”


string name = "洗发水";
var res = ctx.Products.Where(x => x.Name == name).ToList();

View Code

 在我们看来应该没有区别,但是真的有区别


SELECT
    [Extent1].[Id] AS[Id],
    [Extent1].[Name] AS[Name],
    [Extent1].[Price] AS[Price],
    [Extent1].[Unit] AS[Unit],
    [Extent1].[FK_OrderId] AS[FK_OrderId],
    [Extent1].[AddTime]
        AS[AddTime]
FROM[dbo].[tb_Products]
        AS[Extent1]
WHERE([Extent1].[Name] = @p__linq__0) OR(([Extent1].[Name] IS NULL) AND(@p__linq__0 IS NULL))

View Code

 那么,我们可以通过在上下文构造函数里面设置一下,针对这种情况让EF能够生成更简单的SQL语句


public class EFDbContext:DbContext
    {
        public EFDbContext()
        {
            this.Configuration.UseDatabaseNullSemantics = true;
        }
}

View Code

表值函数 

 我们怎样在EF中调用自定义的SQL函数呢?

我来一个返回所有产品的函数


create function func_getProducts()
returns @rtProducts table
(
Id nvarchar(36),
[Name] nvarchar(50),
Price decimal(18,2),
Unit nvarchar(10),
FK_OrderId nvarchar(36),
AddTime datetime
)
as
begin
insert @rtProducts
select Id,[Name],Price,Unit,FK_OrderId,AddTime from tb_Products
return
end

View Code

 然后调用SqlQuery()方法就行了


var res = ctx.Database.SqlQuery<Product>("select *from func_getProducts()").ToListAsync().Result;

View Code

日期操作 

 如果我想要再products中查询,通过计算得到一个新列,比如说是产品添加时间和当前时间的差距,可能会这样写


var product = ctx.Products.Select(x => new { Time = DateTime.Now - x.AddTime });

View Code

但是不行啊,报错信息如下:

System.ArgumentException: DbArithmeticExpression arguments must have a numeric common type.
System.Data.Entity.Core.Common.CommandTrees.ExpressionBuilder.DbExpressionBuilder.CreateArithmetic(DbExpressionKind kind, DbExpression left, DbExpression right)

 这种情况我们就需要SqlFunctions类中的方法来


var products = ctx.Products.Select(x => new { Time = SqlFunctions.DateDiff("DAY",x.AddTime,DateTime.Now)});

View Code

这样写就行了,但是如果这样


var products = ctx.Products.Select(x => new { Time = SqlFunctions.DateDiff("DAY",x.AddTime,DateTime.Now.AddDays(-2))});

View Code

 就报错了:

System.NotSupportedException: LINQ to Entities does not recognize the method ‘System.DateTime AddDays(Double)’ method, and this method cannot be translated into a store expression.

 他说该方法不能转换为存储表达式。这一点也需要注意。