2009-08-03 4 views
36

Мы обнаружили, что compiling our Linq queries намного, намного быстрее, чем их приходится скомпилировать каждый раз, поэтому мы хотели бы начать использовать скомпилированные запросы. Проблема в том, что он делает код более трудным для чтения, потому что фактический синтаксис запроса отключен в другом файле, вдали от того, где он используется.Автоматическая компиляция запросов 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<> объекты, которые я могу вызвать после автоматической компиляции запроса в первый раз.

+1

Это запутанный вопрос, потому что вы, кажется, объединяете LINQ и LINQ to SQL (который дополнительно генерирует, компилирует и кэширует планы выполнения за кулисами при каждом запуске запроса). Если вы спрашиваете о скомпилированных планах выполнения SQL Server, я не знаю (что я знаю), чтобы скомпилировать их и сохранить их в кэше, кроме их запуска. – 48klocs

+0

Это не имеет никакого отношения к SQL Server. LINQ to SQL компилирует запросы, которые могут занимать довольно много времени - от обоих синтаксисов LINQ (цепочка или SQL-стиль) до SQL при каждом запуске этих запросов. Прочтите ссылку наверху, чтобы узнать больше. – tghw

+0

Одна из проблем, которые я нашел с использованием скомпилированных запросов с L2S в веб-приложении, заключается в том, что для ее компиляции вам необходимо передать экземпляр DataContext - для веб-приложения это означает, что вам нужен один общий DataContext для всего сайта - который в результате вызвало несколько серьезных проблем с многопоточными проблемами, когда сайт начал иметь большую нагрузку. Мне действительно очень не нравится, как вы должны передать экземпляр datacontext при компиляции запроса ... – kastermester

ответ

18

У вас не может быть методов расширения, вызываемых на анонимных лямбда-выражениях, поэтому вы захотите использовать класс 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()); 
} 

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

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); 
+0

Это почти то же самое, что я начал работать. Единственная проблема, которую я вижу, заключается в том, что вызов q.ToString() приведет к компиляции запроса в любом случае, поскольку ToString() выводит параметризованный SQL. Я что-то упускаю? – tghw

+1

будьте осторожны с .ToString(), если вы измените имена переменных, но выражение LINQ будет таким же, оно изменит ToString, и поэтому ключ будет другим. Поэтому он скомпилирует новый запрос. –

+2

@tghw: .ToString() не проблема; он строит выражение лямбда, а не результирующий SQL, т. е. «db => db.Foo.Where (x =>! x.IsDisabled)». Я проверил это локально в проекте MVC. @Stan: это не вызывает особой озабоченности, так как у вас, вероятно, будет N буквальных запросов в вашем коде по сравнению с M * N раз, когда будут вызваны эти запросы. – Jason

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); 

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

+0

default (IEnumerable ); == null; в любом случае. Вы по умолчанию используете интерфейс. – Dykam

+0

oops..honest ошибка.У меня был другой код, и я просто скопировал и вставил свой код и не проверял его дважды. спасибо, что заметили. –

+0

Это не делает то, что вы думаете. Если у вас есть 'Expression' на' IEnumerable', нет никакой разницы между expression.Compile() и фактическим кодом ILF Func, который был бы сгенерирован, если бы вы не запрашивали выражение «Expression». Фактически, exp.Compile(), скорее всего, будет медленнее, так как вам не хватает большого количества оптимизаций компилятора. – Jason

1

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

+0

Не могли бы вы предоставить соответствующий источник этого заявления? –

+0

Извините, я не уверен, почему я не предоставлял источник в то время - я предполагаю, что это было то, что я наблюдал недавно в то время, и у меня не было ссылки. –

+0

Хорошо, это правда, что вы сказали на время? Все ли запросы LINQ скомпилированы в .NET 4.5? –

Смежные вопросы