36

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

4

4 回答 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 表达式的隐式类型。

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

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

另一种分页方法是返回 a 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);
于 2009-08-04T11:16:40.267 回答
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);

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

于 2009-08-03T21:20:45.877 回答
1

对于未来的后代:.NET Framework 4.5 默认会执行此操作(根据我刚刚观看的演示文稿中的幻灯片)。

于 2011-10-16T18:24:32.823 回答
1

我不得不处理保存使用 LinqToSql 开发的 > 15y/o 项目并且 CPU 太耗电。

基准测试表明,使用编译查询对于复杂查询要快 7 倍,对于简单查询要快 2 倍(考虑到运行查询本身可以忽略不计,这里只考虑编译查询的吞吐量)。

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

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

  • 您必须编译 que 查询,包括具体化指令(FirstOrDefault/First/Any/Take/Skip/ToList),否则您可能会将整个数据库放入内存:LINQ to SQL *compiled* 查询以及它们何时执行
  • 您不能对已编译查询的结果进行 DOUBLE 迭代(如果它是 IQueryable),但是一旦您正确考虑了前一点,这基本上可以解决

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

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

在非常紧凑的循环中,使用来自被调用方的缓存键而不是查询本身可以提高 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);
        }
    }
于 2020-05-01T06:09:14.490 回答