Автоматически компилировать запросы Linq - PullRequest
36 голосов
/ 03 августа 2009

Мы обнаружили, что компиляция наших запросов 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");

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

ОБНОВЛЕНИЕ: Для тех, кто его не видел, вы можете скомпилировать запросы 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<> объектов, к которым я могу обращаться после автоматической компиляции запроса в первый раз.

Ответы [ 3 ]

18 голосов
/ 04 августа 2009

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

РЕДАКТИРОВАТЬ: обратите внимание, что вы не можете применять новые операторы к скомпилированным запросам. В частности, вы не можете делать что-то вроде этого:

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 голосов
/ 04 августа 2009

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

Я настроил это с помощью словаря, я также не использую 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);

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

1 голос
/ 16 октября 2011

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...