先来看一段错误提示:The instance of entity type ‘XXXX’ cannot be tracked because another instance with the same key value for {‘Key’} is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached.
这里面的"XXXX"可能是任意一个实体模型的名称,“Key"则是这个实体的Key字段。
出现这个错误的原因是试图更新某个实体时,拥有相同Key的另一个实体也被EF Core tracked了。看一下这段代码:
[HttpPost]
public async Task<IActionResult> TestTracked(DeviceType deviceType)
{
try
{
//type这个实体被EF Core tracked
var type = await context.DeviceTypes.FindAsync(deviceType.Id);
//准备更新deviceType实体,但type实体的key与deviceType的key相同,也就是同一个Key的实体被多次tracked,出现本文开头说的错误
context.Update(deviceType);
await context.SaveChangesAsync();
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
return new JsonResult(new { success = true });
}
要解决这个问题很简单,从linq查询返回实体时指定不跟踪实体就行了。
//把原来的代码:var type = await context.DeviceTypes.FindAsync(deviceType.Id); 改成下面这样
var type = await context.DeviceTypes.AsNoTracking().FirstOrDefaultAsync(d => d.Id == deviceType.Id);
AsNoTracking()
方法就是起这个作用。
但是我今天碰到一个很奇怪的多次tracked问题。
我有一个实体Point,其中包含一个导航属性Devices,这是一个List<Device>,也就是Point实体和Device实体是一对多的关系。Device实体中又包含了几个外键。现在我向服务器提交json数据,准备更新一个Point实体,数据类似如下:
{
"pointId": 1,
"pointName": "测试点",
"devices": [{
"deviceId": 1,
"modelId", 1,
"supplierId: 1,
},{
"deviceId": 2,
"modelId", 2,
"supplierId: 1,
}]
}
Device实体中的modelId和supplierId都是外键。
结果在context.Update(point)
时,出现实体被多次跟踪的错误,错误中实体名是Supplier
,Supplier
是Device
的外键表。在整个更新的过程中,我没有对Supplier
实体做任何查询和更新的动作。所以这个错误就出现的比较古怪了。
经过反复跟踪调试,终于找到问题原因。
在服务器端获取Point实体时,为了在客户端展现SupplieName,我使用了Include和ThenInclude方法:
var point = await context.Points
.Include(p => p.Devices) //获取Devices集合数据
.ThenInclude(p => p.DeviceModel) //获取Device实体的关联实体DeviceModel数据
.Include(p => p.Devices)
.ThenInclude(p => p.Supplier) //获取Device实体的关联实体Supplier的数据
.FirstOrDefaultAsync(p => p.Id == id);
返回到客户端的json对象类似以下:
{
"pointId": 1,
"pointName": "测试点",
"devices": [{
"deviceId": 1,
"modelId", 1,
"deviceModel": { deviceMode对象,略 },
"supplierId: 1,
"supplier": { supplier对象, 略 }
},{
"deviceId": 2,
"modelId", 2,
"deviceModel": { deviceMode对象,略 },
"supplierId: 1,
"supplier": { supplier对象, 略 }
}]
}
客户端更新时,修改了上面json对象的部分字段,再把json对象提交到服务器,因此json对象中的DeviceModel和Supplier对象都不为null,当使用EF Core的Update方法时,因为多个Device对象中的Supplier实体的Key相同,就出现多次tracked错误。在这个案例中,因为每个Device的ModelId都不同,所以没有报DeviceModel实体被多次tracked错误。
找到问题原因,解决就简单了,在调用Update方法之前,把每个Device对象中关联实体都设为null就可以了。
foreach (var device in point.Devices)
{
//把所有外键实体设为null,避免出现多次tracked的错误
device.DeviceModel = null;
device.Supplier = null;
}
也可以在客户端转换json字符串时处理:
$("#devices input").each((_, item) => {
devices.push(JSON.parse($(item).val(), (key, value)=>{
switch(key){
case "deviceModel":
case "supplier":
return null;
default:
return value;
}
}));
})
当然,从代码结构上来讲,在服务器端处理比较好。