DbContext – 首先从本地获取的扩展

huangapple go评论83阅读模式
英文:

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&lt;T&gt; GetByIdLocallyFirst(Guid id)
{
	var entity = _dbSet.Local.FirstOrDefault(x =&gt; 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 =&gt; q
	.Include(x =&gt; x.Commodity)
	.Include(x =&gt; x.ContractType)
	.Include(x =&gt; x.Customer)
	.Include(x =&gt; x.Employee)
	.Include(x =&gt; x.DeliveryLocation)
	.Include(x =&gt; 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&lt;TEntity&gt; GetByIdLocallyFirstAsync&lt;TEntity&gt;(this DbSet&lt;TEntity&gt; dbSet, Guid id, Func&lt;IQueryable&lt;TEntity&gt;, IQueryable&lt;TEntity&gt;&gt; includeFunc = null, CancellationToken cancellationToken = default)
		where TEntity : class
	{
		Func&lt;object, Guid&gt; getId = x =&gt;
		{
			var property = x.GetType().GetProperty(&quot;Id&quot;);
			if (property == null)
				throw new InvalidOperationException($&quot;The entity type {typeof(TEntity)} does not have an &#39;Id&#39; property of type Guid.&quot;);
	
			var value = property.GetValue(x);
			if (value == null)
				throw new InvalidOperationException($&quot;The &#39;Id&#39; property on the entity type {typeof(TEntity)} is null.&quot;);
	
			return (Guid)value;
		};
		
		// First, look in the local cache
		var entity = dbSet.Local.FirstOrDefault(x =&gt; 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 =&gt; 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 =&gt; x.Commodity)
	.Include(x =&gt; x.ContractType)
	.Include(x =&gt; x.Customer)
	.Include(x =&gt; x.Employee)
	.Include(x =&gt; x.DeliveryLocation)
	.Include(x =&gt; 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 =&gt; b.BlogId == 1);

    context.Entry(blog)
        .Collection(b =&gt; b.Posts)
        .Load();

    context.Entry(blog)
        .Reference(b =&gt; 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.

huangapple
  • 本文由 发表于 2023年8月4日 23:48:25
  • 转载请务必保留本文链接:https://go.coder-hub.com/76837455.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定