在用EF Core时,遇到了如下错误提示:
The instance of entity type ‘XXX’ cannot be tracked because another instance with the same key value for {‘XX’} is already being tracked.
因为EF Core我也是初学,之前只以以下写法简单使用过:
using(var context = new XXXContext())
{
var xDbSet = context.XDbset...;
// do some operations
context.SaveChanges();
}
所以遇到这种错误,一脸懵逼。
只能通过IDE的错误提示大概了解是具有相同键值的实例被重复跟踪而报错。
于是我先上网搜索了一下,找到一种解决方法,
var selfDevice = context.Devices.Single(d => d.DeviceId.Equals(this.Device.DeviceId));
context.Entry(selfDevice).State = Microsoft.EntityFrameworkCore.EntityState.Detached;
该方法就是在从context(上下文)取得实体实例之后,把该实例的状态设为Detached,然后该错误就消失了。
这个错误直觉上来说,就是多次获取同一实体,EF Core重复跟踪实体,导致错误。
因为同一实体被多次获取后,在内存中出现了多个指向它的对象,而你可能对多个对象做不同的变更,这就可能会导致后期写入数据库时产生不确定性,我写入的时候到底应该按哪个变更来写入呢?计算机执行是不能有二义性的。
当然以上是我的猜测,其中还有一些注意项,得往下深入学习才知道。
也借此机会,参考官方文档,稍微深入一下EF Core的实例跟踪相关知识。
正式学习之前,你得具备一些EF Core的常识,
比如,后面说的DbContext是上下文。
变更跟踪有时也可以叫实体跟踪、实例跟踪,因为EF Core使用时实体是在DbContext中的,而DbContext需要实例化生成实例才能用。你所做的变更,是对实体的变更。
首先得知道什么是变更跟踪,跟踪的是什么,为什么要跟踪?
每个DbContext实例都会跟踪实体所做的变更。当调用SaveChanges时,这些被跟踪的实体会反过来将变更写入数据库,驱动数据库变更。
回答一下本节开始的问题:
- 实例跟踪就是使用EF Core时,EF Core会对上下文中对实体做的操作进行跟踪记录,以便后续同步到数据库中。
- 跟踪的对象是上下文中的实体。
- 跟踪的目的是将对实体的更改同步到数据库。
简单讲就是var context = new XXXContext()得到上下文后,用诸如context.DbSet.property = xxx的语句对上下文中的实体做出更改会被跟踪到,然后调用SaveChanges会把对实体的更改写入到数据库中。
PS:实体在上下文中由DbSet暴露。
本文主要介绍EFCore变更跟踪,以及它怎样与数据库查询(Query)和更新(Update)关联起来。
当实体实例处于以下状态时,会被跟踪:
当处于以下状态时,实体实例不再被跟踪:
DbContext被设计成一个短期的工作单元,就像DbContext初始化和配置中描述的那样(用完就关,下次用时再重新获取)。这意味着释放DbContext是停止跟踪实体的标准方法。换句话说,DbContext的生命周期应该是这样的:
💡小提示
采用该方法时,不需要清除变更跟踪器或显式地分离实体实例。
不过,如果你确实需要分离实体,那么调用ChangeTracker.Clear会比逐个分离实体更效率。
每个实体都会关联一个实体状态(EntityState)中,有以下几种实体状态:
EF Core跟踪改变是在属性层面的。例如,如果只修改单个属性值,那么数据库就只更新该值。不过,只有当实体本身处于Modified状态时,属性才能被标记为modified。(或者,换个角度看,Modified状态意味着至少有一个属性值被标记为Modified。)
- 跟踪改变是在属性层面(property level)指context是可以跟踪到属性的变化(或者属性是可跟踪的最小单位?)。
- 只有当实体本身处于Modified状态,属性才能被标记为Modified。大概指的是像added这种情况?全新的实体就没有Modified的一说。
下表概述了不同状态的特性(下表Modified对应的SaveChanges时的动作是Update,可以很好说明什么是属性层面的跟踪):
| 实体状态 | 是否被DbContext跟踪 | 存在数据库中 | 属性修改 | 调用SaveChanges时的动作 |
|---|---|---|---|---|
| Detached | 否 | -(分离后就是程序里的东西了) | - | - |
| Added | 是 | 否(新new出来的,还未加入数据库) | - | Insert |
| Unchanged | 是 | 是 | 否 | - |
| Modified | 是 | 是 | 是 | Update |
| Deleted | 是 | 是 | - | Delete |
❗注意
为了更清晰的描述,本文使用关系数据库的术语。
NoSQL数据库通常支持类似的操作,但可能使用的名称不同。
EF Core变更跟踪在使用同一个DbContext实例来查询实体和调用SaveChanges来更新实体时效果最好。这是因为EF Core会自动跟踪被查询实体的状态,然后在调用SaveChanges时检测这些实体的变化。
与显式跟踪实体实例相比,这种方法有几点优势:
例如,考虑下面这个简单的 blog/posts模型(博客/帖子):
public class Blog
{
public int Id { get; set; }
public string Name { get; set; }
public IList<Post> Posts { get; } = new List<Post>();
}
public class Post
{
public int Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int? BlogId { get; set; }
public Blog Blog { get; set; }
}
我们可以使用该模型来查询blogs和posts,然后做一些变更并更新到数据库:
using var context = new BlogsContext();
var blog = context.Blogs.Include(e => e.Posts).First(e => e.Name == ".NET Blog");
blog.Name = ".NET Blog (Updated!)";
foreach (var post in blog.Posts.Where(e => !e.Title.Contains("5.0")))
{
post.Title = post.Title.Replace("5", "5.0");
}
context.SaveChanges();
调用SaveChanges会导致下列数据库更新,以SQLite为例:
-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p0='.NET Blog (Updated!)' (Size = 20)], CommandType='Text', CommandTimeout='30']
UPDATE "Blogs" SET "Name" = @p0
WHERE "Id" = @p1;
SELECT changes();
-- Executed DbCommand (0ms) [Parameters=[@p1='2' (DbType = String), @p0='Announcing F# 5.0' (Size = 17)], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "Title" = @p0
WHERE "Id" = @p1;
SELECT changes();
变更跟踪器调试视图(change tracker debug view)是一种不错的方法,它用于查看哪些实体被跟踪以及它们的状态是什么。例如,在上面示例中插入(在调用SaveChanges之前)以下代码:
context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
生成以下输出:
Blog {Id: 1} Modified
Id: 1 PK
Name: '.NET Blog (Updated!)' Modified Originally '.NET Blog'
Posts: [{Id: 1}, {Id: 2}, {Id: 3}]
Post {Id: 1} Unchanged
Id: 1 PK
BlogId: 1 FK
Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
Title: 'Announcing the Release of EF Core 5.0'
Blog: {Id: 1}
Post {Id: 2} Modified
Id: 2 PK
BlogId: 1 FK
Content: 'F# 5 is the latest version of F#, the functional programming...'
Title: 'Announcing F# 5.0' Modified Originally 'Announcing F# 5'
Blog: {Id: 1}
特别注意:
在同一个工作单元中,像前面示例那样的更新是可以与插入和删除合并的。例如:
using var context = new BlogsContext();
var blog = context.Blogs.Include(e => e.Posts).First(e => e.Name == ".NET Blog");
// Modify property values
blog.Name = ".NET Blog (Updated!)";
// Insert a new Post
blog.Posts.Add(
new Post
{
Title = "What’s next for System.Text.Json?", Content = ".NET 5.0 was released recently and has come with many..."
});
// Mark an existing Post as Deleted
var postToDelete = blog.Posts.Single(e => e.Title == "Announcing F# 5");
context.Remove(postToDelete);
context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
context.SaveChanges();
本例中:
在调用SaveChanges之前再看看变更跟踪器调试视图,可以看到EF Core是如何跟踪这些变更的:
Blog {Id: 1} Modified
Id: 1 PK
Name: '.NET Blog (Updated!)' Modified Originally '.NET Blog'
Posts: [{Id: 1}, {Id: 2}, {Id: 3}, {Id: -2147482638}]
Post {Id: -2147482638} Added
Id: -2147482638 PK Temporary
BlogId: 1 FK
Content: '.NET 5.0 was released recently and has come with many...'
Title: 'What's next for System.Text.Json?'
Blog: {Id: 1}
Post {Id: 1} Unchanged
Id: 1 PK
BlogId: 1 FK
Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
Title: 'Announcing the Release of EF Core 5.0'
Blog: {Id: 1}
Post {Id: 2} Deleted
Id: 2 PK
BlogId: 1 FK
Content: 'F# 5 is the latest version of F#, the functional programming...'
Title: 'Announcing F# 5'
Blog: {Id: 1}
注意到:
当调用SaveChanges时,这会导致以下数据库命令(使用SQLite):
-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p0='.NET Blog (Updated!)' (Size = 20)], CommandType='Text', CommandTimeout='30']
UPDATE "Blogs" SET "Name" = @p0
WHERE "Id" = @p1;
SELECT changes();
-- Executed DbCommand (0ms) [Parameters=[@p0='2' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Posts"
WHERE "Id" = @p0;
SELECT changes();
-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String), @p1='.NET 5.0 was released recently and has come with many...' (Size = 56), @p2='What's next for System.Text.Json?' (Size = 33)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Posts" ("BlogId", "Content", "Title")
VALUES (@p0, @p1, @p2);
SELECT "Id"
FROM "Posts"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();
💡小提示
调用ChangeTracker.HasChanges()以确定是否发生了任何会导致SaveChanges对数据库进行更新的更改。如果HasChanges返回false,SaveChanges将不执行任何操作。
从这篇文可以知道上下文中实体变更是由变更跟踪器跟踪记录的。并且实体是有状态的,不同状态在SaveChanges回写数据库时会生成不同的数据库操作。但是文章一开始提到的那个错误,似乎并没有得到明确解答。