自动编译Linq查询

问题描述 投票:36回答:4

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

c# asp.net-mvc linq linq-to-sql iqueryable
4个回答
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表达式的隐式。

EDIT: 注意,你不能将新的操作符应用到编译后的查询中。具体来说,你不能做这样的事情。

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

所以如果你打算对一个查询进行分页, 你需要在编译操作中进行,而不是以后再做。这不仅是为了避免出现异常,而且也是为了符合SkipTake的整个要点(避免从数据库中返回所有的记录)。这种模式是可行的。

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

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


1
投票

为了以后的发展:.NET Framework 4.5将默认这样做(根据我刚看的一个演示中的幻灯片)。


1
投票

我不得不处理保存一个>15yo的项目,这个项目是使用LinqToSql开发的,太耗费CPU。

基准测试显示,使用编译查询对复杂查询快了x7,对简单查询快了x2(考虑到运行查询本身可以忽略不计,这里只是考虑编译查询的吞吐量)。

缓存不是由.Net Framework自动完成的(无论什么版本),这只发生在Entity Framework而不是LINQ-TO-SQL上,而且这些是不同的技术。

编译后的查询的使用是很棘手的,所以这里有两个重要的重点。

  • 你必须编译阙查询,包括实体化指令(FirstOrDefaultFirstAnyTakeSkipToList),否则你有可能把整个数据库带入内存。LINQ to SQL *compiled* queries and when they execute.
  • 你不能对编译后的查询结果进行DOUBLE迭代(如果它是一个IQueryable),但只要你正确地考虑了前一点,这个问题就基本解决了。

考虑到这一点,我想到了这个缓存类。使用其他评论中提出的静态方法有一些可维护性的缺点--主要是可读性较差--另外,要迁移现有的庞大代码库也比较困难。

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

在非常紧密的循环中,使用来自callee的缓存键而不是查询本身,性能提高了+10%。

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

这里是代码 为了安全起见,缓存可以防止编码者在编译查询中返回一个IQueryable。

public class LinqQueryCache<TContext>
        where TContext : DataContext
    {
        protected static readonly ConcurrentDictionary<string, Delegate> CacheValue = new ConcurrentDictionary<string, Delegate>();

        protected string KeyValue = null;

        protected string Key
        {
            get => this.KeyValue;

            set
            {
                if (this.KeyValue != null)
                {
                    throw new Exception("This object cannot be reused for another key.");
                }

                this.KeyValue = value;
            }
        }

        private LinqQueryCache(string key)
        {
            this.Key = key;
        }

        public static LinqQueryCache<TContext> KeyFromStack(
            [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "",
            [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
        {
            return new LinqQueryCache<TContext>(Encryption.GetMd5(sourceFilePath + "::" + sourceLineNumber));
        }

        public static LinqQueryCache<TContext> KeyFromQuery()
        {
            return new LinqQueryCache<TContext>(null);
        }

        public T Cache<T>(TContext db, Expression<Func<TContext, T>> q)
        {
            if (Debugger.IsAttached && typeof(T).IsAssignableFrom(typeof(IQueryable)))
            {
                throw new Exception("Cannot compiled queries with an IQueryableResult");
            }

            if (this.Key == null)
            {
                this.Key = q.ToString();
            }

            if (!CacheValue.TryGetValue(this.Key, out var result))
            {
                result = CompiledQuery.Compile(q);
                CacheValue.TryAdd(this.Key, result);
            }

            return ((Func<TContext, T>)result)(db);
        }

        public T Cache<T, TArg1>(TContext db, Expression<Func<TContext, TArg1, T>> q, TArg1 param1)
        {
            if (Debugger.IsAttached && typeof(T).IsAssignableFrom(typeof(IQueryable)))
            {
                throw new Exception("Cannot compiled queries with an IQueryableResult");
            }

            if (this.Key == null)
            {
                this.Key = q.ToString();
            }

            if (!CacheValue.TryGetValue(this.Key, out var result))
            {
                result = CompiledQuery.Compile(q);
                CacheValue.TryAdd(this.Key, result);
            }

            return ((Func<TContext, TArg1, T>)result)(db, param1);
        }
    }
© www.soinside.com 2019 - 2024. All rights reserved.