要专业系统地学习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.
他说该方法不能转换为存储表达式。这一点也需要注意。
