我可以克隆一个 IQueryable 以在另一个 DbContext 的 DbSet 上运行吗?

Posted

技术标签:

【中文标题】我可以克隆一个 IQueryable 以在另一个 DbContext 的 DbSet 上运行吗?【英文标题】:Can I clone an IQueryable to run on a DbSet for another DbContext? 【发布时间】:2018-12-07 01:25:59 【问题描述】:

假设我已经通过一些条件逻辑在许多步骤中建立了一个IQueryable<T> 实例,我们将调用query

我想获取总记录数和一页数据,所以我想调用query.CountAsync()query.Skip(0).Take(10).ToListAsync()。我不能连续调用它们,因为在它们都尝试同时在同一个DbContext 上运行查询时会出现竞争条件。这是不允许的:

“在前一个异步操作完成之前,在此上下文上启动了第二个操作。使用'await'确保在调用此上下文上的另一个方法之前已完成任何异步操作。不保证任何实例成员都是线程安全的。”

我不想在开始第二个之前“等待”第一个。我想尽快触发这两个查询。这样做的唯一方法是从单独的 DbContext 运行它们。我可能不得不从 DbSet 的不同实例开始并排构建整个查询(或 2 个或 3 个),这似乎很荒谬。有什么方法可以克隆或更改IQueryable<T>(不一定是那个接口,但它是底层实现),这样我就可以拥有一个在DbContext“A”上运行的副本,另一个将在DbContext“上运行” B",以便两个查询可以同时执行?我只是想避免从头开始重新组合查询 X 次,只是为了在 X 上下文中运行它。

【问题讨论】:

【参考方案1】:

没有标准的方法来做到这一点。问题是 EF6 查询表达式树包含持有ObjectQuery 实例的常量节点,这些实例绑定到创建查询时使用的DbContext(实际上是底层ObjectContext)。如果有这样的表达式绑定到与执行查询不同的上下文,那么在执行查询之前还会进行运行时检查。

我想到的唯一想法是使用ExpressionVisitor 处理查询表达式树,并将这些ObjectQuery 实例替换为绑定到新上下文的新实例。

这是上述想法的可能实现:

using System.Data.Entity.Core.Objects;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Linq.Expressions;

namespace System.Data.Entity

    public static class DbQueryExtensions
    
        public static IQueryable<T> BindTo<T>(this IQueryable<T> source, DbContext target)
        
            var binder = new DbContextBinder(target);
            var expression = binder.Visit(source.Expression);
            var provider = binder.TargetProvider;
            return provider != null ? provider.CreateQuery<T>(expression) : source;
        

        class DbContextBinder : ExpressionVisitor
        
            ObjectContext targetObjectContext;
            public IQueryProvider TargetProvider  get; private set; 
            public DbContextBinder(DbContext target)
            
                targetObjectContext = ((IObjectContextAdapter)target).ObjectContext;
            
            protected override Expression VisitConstant(ConstantExpression node)
            
                if (node.Value is ObjectQuery objectQuery && objectQuery.Context != targetObjectContext)
                    return Expression.Constant(CreateObjectQuery((dynamic)objectQuery));
                return base.VisitConstant(node);
            
            ObjectQuery<T> CreateObjectQuery<T>(ObjectQuery<T> source)
            
                var parameters = source.Parameters
                    .Select(p => new ObjectParameter(p.Name, p.ParameterType)  Value = p.Value )
                    .ToArray();
                var query = targetObjectContext.CreateQuery<T>(source.CommandText, parameters);
                query.MergeOption = source.MergeOption;
                query.Streaming = source.Streaming;
                query.EnablePlanCaching = source.EnablePlanCaching;
                if (TargetProvider == null)
                    TargetProvider = ((IQueryable)query).Provider;
                return query;
            
        
    

与标准 EF6 LINQ 查询的一个不同之处在于,这会生成 ObjectQuery&lt;T&gt; 而不是 DbQuery&lt;T&gt;,尽管除了 ToString() 不返回生成的 SQL 之外,我没有注意到进一步构建查询的任何差异 /执行。它似乎有效,但请谨慎使用,风险自负。

【讨论】:

【参考方案2】:

您可以编写一个函数来构建您的查询,将 DbContext 作为参数。

 public IQueryable<T> MyQuery(DbContext<T> db)
 
     return db.Table
              .Where(p => p.reallycomplex)
              ....
              ...
              .OrderBy(p => p.manythings);
 

我已经做过很多次了,效果很好。 现在很容易使用两种不同的上下文进行查询:

IQueryable<T> q1 = MyQuery(dbContext1);
IQueryable<T> q2 = MyQuery(dbContext2);

如果您关心的是构建 IQueryable 对象所花费的执行时间,那么我唯一的建议是不要担心。

【讨论】:

【参考方案3】:

因此,您有一个IQueryable&lt;T&gt;,一旦执行查询,就会在 DbContext A 上执行,并且您希望在执行查询时在 DbContext B 上运行相同的查询。

为此,您必须了解IEnumerable&lt;T&gt;IQueryable&lt;T&gt; 之间的区别。

IEnumerable&lt;T&gt; 包含所有代码以枚举可枚举表示的元素。枚举在调用 GetEnumeratorMoveNext 时开始。这可以明确地完成。然而,它通常由 foreachToListFirstOrDefault 等函数隐式完成。

IQueryable 不包含要枚举的代码,它包含 ExpressionProvider。 Provider 知道谁将执行查询,并且知道如何将Expression 翻译成查询执行者可以理解的语言。

由于这种分离,可以让不同的数据源执行相同的表达式。它们甚至不必属于同一类型:一个数据源可以是理解 SQL 的数据库管理系统,另一个可以是逗号分隔的文件。

只要将返回 IQueryable 的 Linq 语句连接起来,就不会执行查询,只会更改表达式。

一旦枚举开始,通过调用 GetEnumerator / MoveNext,或者通过使用 foreach 或不返回 IQueryable 的 LINQ 函数之一,提供程序将表达式转换为数据源可以理解并与之通信的语言执行查询的数据源。查询的结果是一个 IEnumerable,可以像所有数据都在本地代码中一样进行枚举。

有些 Provider 很聪明,会使用一些缓冲,所以不是所有的数据都被传输到本地内存,而只是部分数据。需要时查询新数据。因此,如果您在包含无数元素的数据库中执行 foreach,则只会查询前几个(数千个)元素。如果您的 foreach 用完获取的数据,则会查询更多数据。

所以你已经有一个IQueryable&lt;T&gt;,因此你有一个Expression、一个Provider和一个ElementType。在执行之前,您需要相同的 Expression / ElementType to be executed by a differentProvider. You even want to change theExpression`。

因此,您需要能够创建一个实现IQueryable&lt;T&gt; 的对象,并且您希望能够设置ExpressionElementTypeProvider

class MyQueryable<T> : IQueryable<T>

     public type ElementType get; set;
     public Expression Expression get; set;
     public Provider Provider get; set;
 

IQueryable<T> queryOnDbContextA= dbCotextA ...
IQueryable<T> setInDbContextB = dbContextB.Set<T>();

IQueryable<T> queryOnDbContextB = new MyQueryable<T>()

     ElementType = queryOnDbContextA.ElementType,
     Expression = queryOnDbContextB.Expression,
     Provider = setInDbContextB.Provider,

如果需要,您可以在执行之前在其他上下文中调整查询:

var getPageOnContextB = queryOnDbContextB
    .Skip(...)
    .Take(...);

这两个查询仍未执行。执行它们:

var countA = await queryOnContextA.CountAsync();
var fetchedPageContextB = await getPageOnContextB.ToListAsync();

【讨论】:

以上是关于我可以克隆一个 IQueryable 以在另一个 DbContext 的 DbSet 上运行吗?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 GIT 扩展在另一台 PC 中克隆另一个 repo?

如何从网站中提取数据计数器以在另一个 HTML 项目中用作 JS 变量

Oracle - 授予非 dba 用户权限以在另一个用户对象上创建授权

iOS - 从一个视图控制器传递数据以在另一个视图控制器的 init 方法中使用

悬停 div 以在另一个 div 中显示更多内容

从命名空间调用方法以在另一个 javascript 文件中使用