要专业系统地学习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; } }
映射配置
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); }
表结构、数据如下
根据导航属性来筛选
// 根据导航属性筛选 var res = ctx.Products.Where(x => x.Order != null).ToList(); Console.WriteLine(JsonConvert.SerializeObject(res, set));
Sql执行情况如下
表里面有六个产品,EF执行了七条SQL语句,第一条,查询出所有的产品,然后逐条产品去筛选
根据外键属性来筛选
View Code
SQL执情况和上面差不多,也是七条语句
问题在于这里,如果延迟加载被关闭了,那么根据导航属于查询是无效的。因为关闭了延迟加载,导航属性是Null
// 禁用延迟加载 this.Configuration.LazyLoadingEnabled = false;
就执行了一条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]
所以,应该按照外键属性来筛选比导航属性要好
分页查询:先筛选后分页、先分页后筛选,执行的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));
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
先分页再筛选
var res = ctx.Products.OrderBy(x => x.Price).Skip(1).Take(3).Where(x => x.Price < 50).ToList();
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
明显后面一种的更复杂、难读,原因只是我们得查询方法的位置变了一下
不得不说,LINQ为我们提供了强大的、面向对象的数据查询方法,如果不去关注真正的SQL执行情况,性能问题就在一点点增长。
语义可空
什么意思,就是C#中的空与数据库的空的等价问题,代码一贴就懂了
我要按照Name属性来查询产品
var res = ctx.Products.Where(x => x.Name == "洗发水").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] WHERE N'洗发水' = [Extent1].[Name]
但是现在这样做,我声明一个变量来保存“洗发水”
string name = "洗发水"; var res = ctx.Products.Where(x => x.Name == name).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] WHERE([Extent1].[Name] = @p__linq__0) OR(([Extent1].[Name] IS NULL) AND(@p__linq__0 IS NULL))
那么,我们可以通过在上下文构造函数里面设置一下,针对这种情况让EF能够生成更简单的SQL语句
public class EFDbContext:DbContext { public EFDbContext() { this.Configuration.UseDatabaseNullSemantics = true; } }
表值函数
我们怎样在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
然后调用SqlQuery()方法就行了
var res = ctx.Database.SqlQuery<Product>("select *from func_getProducts()").ToListAsync().Result;
日期操作
如果我想要再products中查询,通过计算得到一个新列,比如说是产品添加时间和当前时间的差距,可能会这样写
var product = ctx.Products.Select(x => new { Time = DateTime.Now - x.AddTime });
但是不行啊,报错信息如下:
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)});
这样写就行了,但是如果这样
var products = ctx.Products.Select(x => new { Time = SqlFunctions.DateDiff("DAY",x.AddTime,DateTime.Now.AddDays(-2))});
就报错了:
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.
他说该方法不能转换为存储表达式。这一点也需要注意。