该项目是一个。net core 7.0 web api,使用实体框架核心和DI。
我有两个DbContext实例,一个实例检查是否创建了实体或记录,如果没有,它调用类中的方法来创建该记录并传递Id。然后,它使用Id再次搜索记录以更新它。
我注意到,如果创建记录的dbContext实例没有正确处理,当我更新同一记录的第二个实例中的属性时,它实际上并没有更新它。它似乎又在更新另一个实例了(我认为)。
由于dbContext具有并发性约束,我专门创建了两个独立的实例,以实现这种关注点分离。
第一个通过依赖注入注入dbContext的类
if (study is null)
{
var logMessage = $"Unable to find {StudyInstanceUid} in the database. Creating a new study and marking it as an exception.";
_logger.LogWarning(logMessage);
var studyId = await _cs.CreateStudy(myStudy);
study = await (from s in _cdb.Studies where s.Id == studyId select s).FirstAsync();
}
具有create方法的服务类
public async Task<int> CreateStudy(studyNotification record)
{
var _optionsBuilder = new DbContextOptionsBuilder<MyDbContext>();
_optionsBuilder.UseSqlServer(_config.GetConnectionString("MyDb"));
using var _cdb = new MyDbContext(_optionsBuilder.Options);
var study = new Study()
{
...
};
_cdb.Studies.Add(study);
await _cdb.SaveChangesAsync();
return study.Id;
}
如果我将上面的代码修改为:
await _cdb.SaveChangesAsync();
int id = study.Id;
return id;
按预期工作。虽然我不了解实体框架核心的内部工作原理,但我认为这两个不同的实例不会相互干扰。我想知道为什么会出现这个问题?
应该没有区别:
await _cdb.SaveChangesAsync();
return study.Id;
和
await _cdb.SaveChangesAsync();
int id = study.Id;
return id;
您将看到不同dbcontext加载的实体之间的差异,以及相对于另一个的事件时间通常归结为跟踪的实例。
如果一个DbContext实例碰巧直接或间接地加载了一个Study实例(比如Id = 5)作为另一个数据读取的结果,默认情况下该实体实例将被DbContext实例跟踪(缓存)。如果另一个新的DbContext实例去加载研究ID #5,它将从数据库加载该记录。假设第二个DbContext修改了该记录并将其保存到数据库中。如果第一个DbContext试图再次读取它,使用如下命令:
var study = context.Studies.Single(x => x.StudyId == studyId);
您可能期望您将从数据库中获得更新的学习记录…通过附加的分析器,你甚至可以看到EF对数据库执行查询,但是你将得到的是DbContext已经拥有的缓存,跟踪的实例,在第二个DbContext做出更改之前。
确保在获取实体时获得当前数据库状态的最简单方法是显式地告诉EF不要跟踪该实体。这意味着EF不会将它添加到跟踪缓存中,更重要的是,它也不会从跟踪缓存中读取它。
var study = context.Studies.AsNoTracking().Single(x => x.StudyId == studyId);
当处理可以并发修改的数据时,无论是在应用程序中的线程之间还是在外部进程/用户之间,重要的是要么使用新的DbContext实例进行操作,要么使用非跟踪查询来确保每个查询每次都将数据状态作为事实来源。这可以像上面一样使用AsNoTracking()
,或者通过使用Select
或ProjectTo
基于投影的读取操作来完成。跟踪查询应该只保留在你想要更新数据的情况下,并尽可能快地完成。
编辑:如果您确实希望更新实体并使用更改跟踪,但要确保这种情况下的实体是最新的,那么您可以检查本地缓存并在找到实体时重新加载该实体,否则请确保检索到的实体来自数据库。如果并发编辑在您的应用程序中是一个重要因素,那么我还建议在相关表中实现并发标记,例如Timestamp或RowVersion,以检查并帮助防止过时的更新。
// Check the local cache, if found, issue a reload to ensure it is up to date:
var study = _cdb.Studies.Local.FirstOrDefault(x => x.StudyId == studyId);
if (study != null)
_cdb.Entry(study).Reload();
else
study = _cdb.Studies.Single(x => x.StudyId == studyId);
这种方法的主要问题是,只要您只想要Study实体而不依赖于相关数据,它就会工作得很好,因为我们不知道跟踪的Study实例是否加载了所有或任何相关数据。例如,如果一个研究有一个引用的集合,我们通常会这样做:
else
study = _cdb.Studies.Include(x => x.References).Single(x => x.StudyId == studyId);
…为了确保引用被加载,问题是,如果我们确实找到了一个缓存的本地研究实例,我们不能安全地假设可能已经加载它的代码也渴望加载引用。集合中可能有引用,但只能是DbContext碰巧也在跟踪的引用。在这种情况下,分离任何被跟踪的实体并重新加载它会更安全:
var study = _cdb.Studies.Local.FirstOrDefault(x => x.StudyId == studyId);
if (study != null)
_cdb.Entry(study).State = EntityState.Detached;
study = _cdb.Studies
.Include(x => x.References)
.Single(x => x.StudyId == studyId);
检查缓存,如果发现,从缓存中删除研究,下一条语句将从数据库中加载研究和相关数据。