英文:
DbContext - Extension which get by id locally first
问题
你想要的是,首先从本地加载数据,如果不行,再查询数据库。整个想法是在调用 SaveChangesAsync
之前有多个 IDomainEventPre
运行。因此,为这些记录查询数据库没有意义,因为它们存在于本地,但尚未调用 SaveChangesAsync。
如果不需要包含相关数据,你可以使用这个扩展方法:
public virtual async Task<T> GetByIdLocallyFirst(Guid id)
{
var entity = _dbSet.Local.FirstOrDefault(x => x.Id == id);
if (entity == null)
{
entity = await _dbSet.FindAsync(id);
}
return entity;
}
但在你的情况下,你希望包含相关数据。
以下是你的尝试:
var offer = await _dbContext.Offers.GetByIdLocallyFirstAsync(_dbContext, notification.OfferId, q =>
.Include(x => x.Commodity)
.Include(x => x.ContractType)
.Include(x => x.Customer)
.Include(x => x.Employee)
.Include(x => x.DeliveryLocation)
.Include(x => x.Location), cancellationToken);
但不成功,因为你无法成功加载本地的相关数据。
你的 QueryExtensions
类的代码允许首先从本地加载数据,然后再查询数据库。如果数据在本地找到,你可以手动包含相关数据。如果没有在本地找到,你可以选择查询数据库。最后,你希望能够以如下方式调用它:
var offer = await _dbContext.Offers
.Include(x => x.Commodity)
.Include(x => x.ContractType)
.Include(x => x.Customer)
.Include(x => x.Employee)
.Include(x => x.DeliveryLocation)
.Include(x => x.Location)
.GetByIdLocallyFirstAsync(notification.OfferId, cancellationToken);
如果你需要进一步的帮助,请提出具体问题。
英文:
What I'm trying to do is, load data from local first and if that's not the case, then query the database instead. The whole idea is that I have several IDomainEventPre
which are ran before the SaveChangesAsync
call. So querying the database for these records doesn't make sense, because they exist locally, but SaveChangesAsync is yet to be called.
That would be a simple extension method if I didn't want to include related data, i.e.
public virtual async Task<T> GetByIdLocallyFirst(Guid id)
{
var entity = _dbSet.Local.FirstOrDefault(x => x.Id == id);
if (entity == null)
{
entity = await _dbSet.FindAsync(id);
}
return entity;
}
but in my case, I do want to include the related data.
Snippet
Here is my attempt:
var offer = await _dbContext.Offers.GetByIdLocallyFirstAsync(_dbContext, notification.OfferId, q => q
.Include(x => x.Commodity)
.Include(x => x.ContractType)
.Include(x => x.Customer)
.Include(x => x.Employee)
.Include(x => x.DeliveryLocation)
.Include(x => x.Location), cancellationToken);
However, unsuccessfully as I'm failing to load the related data if it's pulled off local.
public static class QueryExtensions
{
public static async Task<TEntity> GetByIdLocallyFirstAsync<TEntity>(this DbSet<TEntity> dbSet, Guid id, Func<IQueryable<TEntity>, IQueryable<TEntity>> includeFunc = null, CancellationToken cancellationToken = default)
where TEntity : class
{
Func<object, Guid> getId = x =>
{
var property = x.GetType().GetProperty("Id");
if (property == null)
throw new InvalidOperationException($"The entity type {typeof(TEntity)} does not have an 'Id' property of type Guid.");
var value = property.GetValue(x);
if (value == null)
throw new InvalidOperationException($"The 'Id' property on the entity type {typeof(TEntity)} is null.");
return (Guid)value;
};
// First, look in the local cache
var entity = dbSet.Local.FirstOrDefault(x => getId(x) == id);
// If entity is found in the local cache, manually include related data if needed
if (entity != null)
{
// TODO: How do we include related data?
// I assume we do FirstOrDefaultAsync for each one of the tables that we specified in Include? But how do we do that?
return entity;
}
// If not found in the local cache, query the database
if (includeFunc != null)
{
var query = includeFunc(dbSet);
entity = await query.FirstOrDefaultAsync(e => getId(e) == id, cancellationToken);
}
else
{
entity = await dbSet.FindAsync(new object[] { id }, cancellationToken);
}
return entity;
}
}
Lastly, ideally I would like this to be invoked as following:
var offer = await _dbContext.Offers
.Include(x => x.Commodity)
.Include(x => x.ContractType)
.Include(x => x.Customer)
.Include(x => x.Employee)
.Include(x => x.DeliveryLocation)
.Include(x => x.Location)
.GetByIdLocallyFirstAsync(notification.OfferId, cancellationToken);
答案1
得分: 1
首先,您可以将GetByIdLocallyFirst
的第一个实现更改为FindAsync
,因为它已经做了所需的工作:
查找具有给定主键值的实体。如果上下文正在跟踪具有给定主键值的实体,则立即返回该实体,而不会向数据库发出请求。
然后,您可以尝试使用通过EntityEntry
的显式加载相关数据,尽管它可能会很快变得繁琐。非泛型版本如下:
using (var context = new BloggingContext())
{
var blog = context.Blogs
.Single(b => b.BlogId == 1);
context.Entry(blog)
.Collection(b => b.Posts)
.Load();
context.Entry(blog)
.Reference(b => b.Owner)
.Load();
}
从CollectionEntry.Load
文档中了解更多信息:
加载由此导航属性引用的实体,除非
IsLoaded
已经设置为true。
从ReferenceEntry.Load
文档中了解更多信息:
加载由此导航属性引用的实体或实体,除非
IsLoaded
已经设置为true。
在这两种情况下,Load
方法都来自基类 - NavigationEntry
。
尽管在大多数情况下,个人认为不值得费心。如果没有加载任何内容,可能多次命中数据库的成本可能是一个重要考虑因素。
如果对您来说这是一个潜在的重要场景 - 您应该考虑切换到延迟加载相关数据。
英文:
First of all you can change the first implementation of GetByIdLocallyFirst
to just FindAsync
because it already does what is needed:
> Finds an entity with the given primary key values. If an entity with the given primary key values is being tracked by the context, then it is returned immediately without making a request to the database.
Then you can try using the Explicit Loading of Related Data via EntityEntry
though it can become cumbersome quite quickly. The non-generic version looks like:
using (var context = new BloggingContext())
{
var blog = context.Blogs
.Single(b => b.BlogId == 1);
context.Entry(blog)
.Collection(b => b.Posts)
.Load();
context.Entry(blog)
.Reference(b => b.Owner)
.Load();
}
From CollectionEntry.Load
docs:
> Loads the entities referenced by this navigation property, unless IsLoaded
is already set to true.
From ReferenceEntry.Load
docs:
> Loads the entity or entities referenced by this navigation property, unless IsLoaded
is already set to true.
The Load
method in both cases comes from base class - NavigationEntry
.
Though personally I would not bother in most cases. The cost of potentially hitting database multiple times in case nothing was loaded can be a major consideration.
Also if this is a potentially heavy impacting scenario for you - you should consider just switching to the Lazy Loading of Related Data.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论