Подтвердить что ты не робот

Автоматическая компиляция запросов Linq

Мы обнаружили, что компиляция наших запросов Linq намного быстрее, чем их приходится собирать каждый раз, поэтому мы хотели бы начать используя скомпилированные запросы. Проблема в том, что он делает код более трудным для чтения, потому что фактический синтаксис запроса отключен в другом файле, вдали от того, где он используется.

Мне пришло в голову, что может быть возможно написать метод (или метод расширения), который использует отражение, чтобы определить, какие запросы передаются, и кэшировать скомпилированные версии автоматически для использования в будущем.

var foo = (from f in db.Foo where f.ix == bar select f).Cached();

Cached() должен был бы отражать переданный объект запроса и определять выбранные таблицы и типы параметров для запроса. Очевидно, что отражение немного медленное, поэтому было бы лучше использовать имена для объекта кеша (но вам все равно придется использовать отражение в первый раз для компиляции запроса).

var foo = (from f in db.Foo where f.ix == bar select f).Cached("Foo.ix");

Есть ли у кого-нибудь опыт в этом, или знаете, возможно ли это?

UPDATE:. Для тех, кто этого не видел, вы можете скомпилировать запросы LINQ к SQL со следующим кодом:

public static class MyCompiledQueries
{
    public static Func<DataContext, int, IQueryable<Foo>> getFoo =
        CompiledQuery.Compile(
            (DataContext db, int ixFoo) => (from f in db.Foo
                                            where f.ix == ixFoo
                                            select f)
        );
}

То, что я пытаюсь сделать, это иметь кэш этих объектов Func<>, которые я могу вызвать после автоматической компиляции запроса в первый раз.

4b9b3361

Ответ 1

У вас не может быть методов расширения, вызываемых на анонимных лямбда-выражениях, поэтому вы захотите использовать класс Cache. Чтобы правильно кэшировать запрос, вам также нужно "поднять" любые параметры (включая ваш DataContext) в параметры для вашего лямбда-выражения. Это приводит к очень подробному использованию, например:

var results = QueryCache.Cache((MyModelDataContext db) => 
    from x in db.Foo where !x.IsDisabled select x);

Чтобы очистить это, мы можем создать экземпляр QueryCache для каждого контекста, если мы сделаем его нестатичным:

public class FooRepository
{
    readonly QueryCache<MyModelDataContext> q = 
        new QueryCache<MyModelDataContext>(new MyModelDataContext());
}

Затем мы можем написать метод Cache, который позволит нам написать следующее:

var results = q.Cache(db => from x in db.Foo where !x.IsDisabled select x);

Любые аргументы в вашем запросе также должны быть отменены:

var results = q.Cache((db, bar) => 
    from x in db.Foo where x.id != bar select x, localBarValue);

Здесь реализована реализация QueryCache:

public class QueryCache<TContext> where TContext : DataContext
{
    private readonly TContext db;
    public QueryCache(TContext db)
    {
        this.db = db;
    }

    private static readonly Dictionary<string, Delegate> cache = new Dictionary<string, Delegate>();

    public IQueryable<T> Cache<T>(Expression<Func<TContext, IQueryable<T>>> q)
    {
        string key = q.ToString();
        Delegate result;
        lock (cache) if (!cache.TryGetValue(key, out result))
        {
            result = cache[key] = CompiledQuery.Compile(q);
        }
        return ((Func<TContext, IQueryable<T>>)result)(db);
    }

    public IQueryable<T> Cache<T, TArg1>(Expression<Func<TContext, TArg1, IQueryable<T>>> q, TArg1 param1)
    {
        string key = q.ToString();
        Delegate result;
        lock (cache) if (!cache.TryGetValue(key, out result))
        {
            result = cache[key] = CompiledQuery.Compile(q);
        }
        return ((Func<TContext, TArg1, IQueryable<T>>)result)(db, param1);
    }

    public IQueryable<T> Cache<T, TArg1, TArg2>(Expression<Func<TContext, TArg1, TArg2, IQueryable<T>>> q, TArg1 param1, TArg2 param2)
    {
        string key = q.ToString();
        Delegate result;
        lock (cache) if (!cache.TryGetValue(key, out result))
        {
            result = cache[key] = CompiledQuery.Compile(q);
        }
        return ((Func<TContext, TArg1, TArg2, IQueryable<T>>)result)(db, param1, param2);
    }
}

Это может быть расширено для поддержки большего количества аргументов. Великий бит заключается в том, что, передавая значения параметров самому методу Cache, вы получаете неявное типирование для выражения лямбда.

EDIT: Обратите внимание, что вы не можете применять новые операторы к скомпилированным запросам. В частности, вы не можете сделать что-то вроде этого:

var allresults = q.Cache(db => from f in db.Foo select f);
var page = allresults.Skip(currentPage * pageSize).Take(pageSize);

Итак, если вы планируете подкачку запроса, вам нужно сделать это в операции компиляции, а не делать это позже. Это необходимо не только для исключения исключения, но и в соответствии со всей точкой Skip/Take (чтобы не возвращать все строки из базы данных). Этот шаблон будет работать:

public IQueryable<Foo> GetFooPaged(int currentPage, int pageSize)
{
    return q.Cache((db, cur, size) => (from f in db.Foo select f)
        .Skip(cur*size).Take(size), currentPage, pageSize);
}

Другим подходом к поисковому вызову будет возврат Func:

public Func<int, int, IQueryable<Foo>> GetPageableFoo()
{
    return (cur, size) => q.Cache((db, c, s) => (from f in db.foo select f)
        .Skip(c*s).Take(s), c, s);
}

Этот шаблон используется как:

var results = GetPageableFoo()(currentPage, pageSize);

Ответ 2

Поскольку никто не пытается, я сделаю это. Может быть, мы сможем как-то это разобраться. Вот моя попытка.

Я установил это с помощью словаря, я также не использую DataContext, хотя это тривиально, я считаю.

public static class CompiledExtensions
    {
        private static Dictionary<string, object> _dictionary = new Dictionary<string, object>();

        public static IEnumerable<TResult> Cache<TArg, TResult>(this IEnumerable<TArg> list, string name, Expression<Func<IEnumerable<TArg>, IEnumerable<TResult>>> expression)
        {
            Func<IEnumerable<TArg>,IEnumerable<TResult>> _pointer;

            if (_dictionary.ContainsKey(name))
            {
                _pointer = _dictionary[name] as Func<IEnumerable<TArg>, IEnumerable<TResult>>;
            }
            else
            {
                _pointer = expression.Compile();
                _dictionary.Add(name, _pointer as object);
            }

            IEnumerable<TResult> result;
            result = _pointer(list);

            return result;
        }
    }

теперь это позволяет мне сделать это

  List<string> list = typeof(string).GetMethods().Select(x => x.Name).ToList();

  IEnumerable<string> results = list.Cache("To",x => x.Where( y => y.Contains("To")));
  IEnumerable<string> cachedResult = list.Cache("To", x => x.Where(y => y.Contains("To")));
  IEnumerable<string> anotherCachedResult = list.Cache("To", x => from item in x where item.Contains("To") select item);

с нетерпением ожидая некоторого обсуждения этого вопроса, для дальнейшего развития этой идеи.

Ответ 3

Для будущих потомков:.NET Framework 4.5 сделает это по умолчанию (в соответствии с слайдом в презентации, которую я только что наблюдал).