自动编译Linq查询

时间:2009-08-03 18:07:32

标签: c# asp.net-mvc linq linq-to-sql iqueryable

我们发现compiling our Linq queries比每次编译要快得多,所以我们想开始使用编译查询。问题在于它使代码更难阅读,因为查询的实际语法在其他文件中是关闭的,远离它的使用位置。

我想到有可能编写一个方法(或扩展方法),它使用反射来确定传入的查询,并自动缓存已编译的版本以供将来使用。

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<>对象的缓存,我可以在第一次自动编译查询后调用这些对象。

4 个答案:

答案 0 :(得分:18)

您不能在匿名lambda表达式上调用扩展方法,因此您将需要使用Cache类。为了正确缓存查询,您还需要将任何参数(包括DataContext)“提升”为lambda表达式的参数。这导致非常详细的用法,如:

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方法本身,您可以获得lambda表达式的隐式类型。

编辑:请注意,您无法将新操作符应用于已编译的查询。具体而言,您无法执行以下操作:

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

答案 1 :(得分: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);

期待对此进行一些讨论,以进一步发展这一想法。

答案 2 :(得分:1)

对于未来的后代:.NET Framework 4.5默认会这样做(根据我刚看过的演示文稿中的幻灯片)。

答案 3 :(得分:1)

我不得不处理保存使用LinqToSql开发的并且过于占用CPU的15y / o项目。

基准测试表明,对于复杂查询,使用编译查询的速度是x7的速度,对于简单查询,使用x2的速度是x2的速度(考虑到查询本身的运行是可以忽略的,这里只涉及编译查询的吞吐量)。

.Net Framework(无论什么版本)不会自动完成缓存,这仅对于Entity Framework而不是LINQ-TO-SQL才发生,这是不同的技术。

使用已编译查询非常棘手,因此有两个重要的亮点:

  • 您必须编译查询,其中包括具体化的说明(FirstOrDefault / First / Any / Take / Skip / ToList),否则您将冒着将整个数据库带入内存的风险:LINQ to SQL *compiled* queries and when they execute
  • 您不能对已编译查询的结果(如果它是IQueryable)进行双重迭代,但是一旦您正确考虑了上一点,这基本上就可以解决

考虑到这一点,我想出了这个缓存类。使用其他评论中所建议的静态方法存在一些可维护性方面的缺点-主要是可读性较差-而且很难迁移现有的庞大代码库。

> environment(a)$montePython
[1] "I'm not dead yet!!"

在非常紧密的循环中,使用被调用者的缓存键而不是查询本身可以提高+ 10%的性能:

                LinqQueryCache<VCDataClasses>
                    .KeyFromQuery()
                    .Cache(
                        dcs.CurrentContext, 
                        (ctx, courseId) => 
                            (from p in ctx.COURSEs where p.COURSEID == courseId select p).FirstOrDefault(), 
                        5);

这是代码。出于安全考虑,缓存可防止编码器在已编译查询中返回IQueryable。

                LinqQueryCache<VCDataClasses>
                    .KeyFromStack()
                    .Cache(
                        dcs.CurrentContext, 
                        (ctx, courseId) => 
                            (from p in ctx.COURSEs where p.COURSEID == courseId select p).FirstOrDefault(), 
                        5);