快照更改跟踪
实体类没有实现属性值改变的通知机制,EF Core是如何检测到变化的呢?
快照更改跟踪:首次跟踪一个实体的时候,EF Core 会创建这个实体的快照。执行SaveChanges()等方法时,EF Core将会把存储的快照中的值与实体的当前值进行比较。
实体的状态
- 已添加(Added):DbContext正在跟踪此实体,但数据库中尚不存在该实体。
- 未改变(Unchanged):DbContext正在跟踪此实体,该实体存在于数据库中,其属性值和从数据库中读取到的值一致,未发生改变。
- 已修改(Modified):DbContext正在跟踪此实体,并存在于数据库中,并且其部分或全部属性值已修改。
- 已删除(Deleted):DbContext正在跟踪此实体,并存在于数据库中,但在下次调用 SaveChanges 时要从数据库中删除对应数据。
- 已分离(Detached):DbContext未跟踪该实体。
SaveChanges()操作
- “已分离”和“未改变”的实体,SaveChanges()忽略;
- “已添加”的实体,SaveChanges() 插入数据库;
- “已修改”的实体,SaveChanges() 更新到数据库;
- “已删除”的实体,SaveChanges() 从数据库删除;
EntityEntry
使用DbContext的Entry()方法来获得实体在EF Core中的跟踪信息对象EntityEntry。EntityEntry类的State属性代表实体的状态,通过DebugView.LongView属性可以看到实体的变化信息。
测试:从数据库中查出3条记录,修改一条、删除一条、一条不动;再new两个对象,其中一个Add,另外一个不动。然后查看它们的EntityEntry。
static async Task Main(string[] args)
{
using (MyDbContext ctx = new MyDbContext())
{
var items = ctx.Books.Take(3).ToArray();
var a1 = items[0];
var a2 = items[1];
var a3 = items[2];
var a4 = new Book { Name = "ASP.NET", Author = "阿明", Price = 25.5 };
var a5 = new Book { Name = "WinForm", Author = "马腾", Price = 19.99 };
a1.Price += 1;
ctx.Remove(a2);
ctx.Books.Add(a4);
EntityEntry e1 = ctx.Entry(a1);
EntityEntry e2 = ctx.Entry(a2);
EntityEntry e3 = ctx.Entry(a3);
EntityEntry e4 = ctx.Entry(a4);
EntityEntry e5 = ctx.Entry(a5);
Console.WriteLine("e1.State:" + e1.State);
Console.WriteLine("e1.DebugView.LongView:" + e1.DebugView.LongView);
Console.WriteLine("e2.State:" + e2.State);
Console.WriteLine("e3.State:" + e3.State);
Console.WriteLine("e4.State:" + e4.State);
Console.WriteLine("e5.State:" + e5.State);
}
}
结论
DbContext会根据跟踪的实体的状态,在SaveChanges()的时候,根据实体状态的不同,生成Update、Delete、Insert等SQL语句,来把内存中实体的变化更新到数据库中。
EF Core优化:AsNoTracking
- 如果通过DbContext查询出来的对象只是用来展示,不会发生状态改变,则可以使用AsNoTracking()来 “禁用跟踪”。
- 分别加AsNoTracking()和不加,分别查看一个对象修改后的EntityEntry 信息。
- 如果查询出来的对象不会被修改、删除等,那么查询时可以AsNoTracking(),就能降低内存占用。
static async Task Main(string[] args)
{
using (MyDbContext ctx = new MyDbContext())
{
//正常查询
var items1 = ctx.Books.Take(3).ToArray();
foreach (var e in items1)
{
Console.WriteLine(e.Name);
}
var a1 = items1[0];
Console.WriteLine(ctx.Entry(a1).State);
//调用AsNoTracking
var items2 = ctx.Books.AsNoTracking().Take(3).ToArray();
foreach (var e in items2)
{
Console.WriteLine(e.Name);
}
var a2 = items2[0];
a2.Price += 200;
Console.WriteLine(ctx.Entry(a2).State);
ctx.SaveChanges();
}
}
实体状态跟踪的妙用
- 开发人员一般不需要关注实体状态的跟踪,让它在背后帮助我们工作即可。
- 有人利用状态跟踪的特点做一些小动作,我不推荐,但是要介绍。
常规更新需要先查询、再更新,两条SQL。
直接更新一条数据
static async Task Main(string[] args)
{
using (MyDbContext ctx = new MyDbContext())
{
Book b = new Book { Id = 4 };//跟踪通过Id定位
b.Author = "小王";
var entry = ctx.Entry(b);
entry.Property("Author").IsModified = true;
Console.WriteLine(entry.DebugView.LongView);
ctx.SaveChanges();
}
}
直接删除一条数据
static async Task Main(string[] args)
{
using (MyDbContext ctx = new MyDbContext())
{
Book b = new Book { Id = 4 };
ctx.Entry(b).State = EntityState.Deleted;
await ctx.SaveChangesAsync();
}
}
注意
上面的技巧代码可读性、可维护性不强,而且使用不当有可能造成不容易发现的Bug。带来的性能提升也是微乎其微的,因此不推荐使用,知道即可。