Могу ли я клонировать IQueryable для запуска на DbSet для другого DbContext? - PullRequest
0 голосов
/ 28 июня 2018

Предположим, я с помощью некоторой условной логики за несколько шагов создал экземпляр IQueryable<T>, который мы назовем query.

Я хочу получить общее количество записей и страницу данных, поэтому я хочу позвонить query.CountAsync() и query.Skip(0).Take(10).ToListAsync(). Я не могу назвать их по очереди, потому что возникает состояние гонки, когда они оба пытаются выполнить запрос к одному и тому же DbContext в одно и то же время. Это не разрешено:

"Вторая операция началась в этом контексте до завершения предыдущей асинхронной операции. Используйте 'await', чтобы убедиться, что любые асинхронные операции завершены перед вызовом другого метода в этом контексте. Не гарантируется, что любые члены экземпляра будут потокобезопасными."

Я не хочу «ждать» первого, даже не начав второго. Я хочу снять оба запроса как можно скорее. Единственный способ сделать это - запустить их из отдельных DbContexts. Кажется смешным, что мне, возможно, придется строить весь запрос (или 2, или 3) бок о бок, начиная с разных экземпляров DbSet. Есть ли способ клонировать или изменить IQueryable<T> (не обязательно этот интерфейс, но его базовая реализация), чтобы у меня была одна копия, работающая на DbContext "A", и другая, которая будет работать на DbContext " B ", так что оба запроса могут выполняться одновременно? Я просто пытаюсь избежать перекомпоновки запроса X раз с нуля, просто чтобы запустить его в X контекстах.

Ответы [ 3 ]

0 голосов
/ 28 июня 2018

Нет стандартного способа сделать это. Проблема состоит в том, что деревья выражений запросов 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;
            }
        }
    }
}

Одно отличие от стандартных запросов LINQ EF6 состоит в том, что это производит ObjectQuery<T>, а не DbQuery<T>, хотя, за исключением того, что ToString() не возвращает сгенерированный SQL, я не заметил никакой разницы в дальнейшем построении запроса / выполнение. Кажется, это работает, но используйте это с осторожностью и на свой страх и риск.

0 голосов
/ 02 июля 2018

Таким образом, у вас есть IQueryable<T>, которое будет выполнено для DbContext A, как только запрос будет выполнен, и вы хотите, чтобы тот же запрос выполнялся на DbContext B, когда запрос выполняется.

Для этого вам нужно понять разницу между IEnumerable<T> и IQueryable<T>.

IEnumerable<T> содержит весь код для перечисления элементов, представляемых перечисляемым. Перечисление начинается при вызове GetEnumerator и MoveNext. Это можно сделать явно. Однако это обычно делается неявно такими функциями, как foreach, ToList, FirstOrDefault и т. Д.

IQueryable не содержит код для перечисления, он содержит Expression и Provider. Поставщик знает, кто будет выполнять запрос, и знает, как перевести Expression на язык, понятный исполнителю запроса.

Благодаря такому разделению можно разрешить выполнение одного и того же выражения разными источниками данных. Они даже не должны относиться к одному и тому же типу: один источник данных может быть системой управления базой данных, которая понимает SQL, а другой может быть файлом, разделенным запятыми.

Пока вы объединяете операторы Linq, которые возвращают IQueryable, запрос не выполняется, изменяется только выражение.

Как только начинается перечисление, либо вызывая GetEnumerator / MoveNext, либо используя foreach или одну из функций LINQ, которые не возвращают IQueryable, поставщик будет переводить выражение на язык, который понимает источник данных и взаимодействует с ним источник данных для выполнения запроса. Результатом запроса является IEnumerable, который можно перечислять, как если бы все данные были в локальном коде.

Некоторые провайдеры умны и используют некоторую буферизацию, так что не все данные переносятся в локальную память, а только часть данных. Новые данные запрашиваются при необходимости. Поэтому, если вы выполняете foreach в базе данных с миллиардом элементов, запрашиваются только первые несколько (тысячи) элементов. Запрашиваются дополнительные данные, если в вашем foreach заканчиваются извлеченные данные.

Итак, у вас уже есть один IQueryable<T>, поэтому у вас есть Expression a Provider и ElementType. Вы хотите, чтобы тот же Expression / ElementType to be executed by a different Провайдер . You even want to change the Выражение` немного перед его выполнением.

Следовательно, вам нужно иметь возможность создать объект, который реализует IQueryable<T>, и вы хотите иметь возможность установить Expression, ElementType и Provider

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();
0 голосов
/ 28 июня 2018

Вы можете написать функцию для построения вашего запроса, взяв 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, тогда я могу только рекомендовать не беспокоиться об этом.

...