自动编译 Linq 查询

Posted

技术标签:

【中文标题】自动编译 Linq 查询【英文标题】:Automatically Compile Linq Queries 【发布时间】:2009-08-03 18:07:31 【问题描述】:

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

【问题讨论】:

这是一个令人困惑的问题,因为您似乎将 LINQ 和 LINQ to SQL 混为一谈(每次运行查询时,它都会在后台额外生成、编译和缓存执行计划)。如果您询问 SQL Server 的已编译执行计划,那么(据我所知)除了运行它们之外,无法编译它们并保持缓存。 这与 SQL Server 无关。每次运行这些查询时,LINQ to SQL 都会从两种 LINQ 语法(链式或 SQL 样式)编译查询(这可能需要相当长的时间)到 SQL。阅读顶部的链接以了解更多信息。 我在 Web 应用程序中使用 L2S 编译查询时发现的一个问题是,要编译它,您需要将 DataContext 的实例传递给它 - 对于 Web 应用程序,这意味着您需要一个共享整个站点的 DataContext - 当站点开始有大负载时,这反过来给我带来了一些主要的多线程问题。我真的很不喜欢在编译查询时必须传递 datacontext 实例... 编译查询时不传入 DataContext。请参阅下面的答案;你实际上传入了一个委托,它接受一个 DataContext(和其他参数)并返回一个 IQueryable&lt;T&gt; 你有没有实现过这条路径?如果是这样,您是如何处理不同的 DataLoadOptions 的? 【参考方案1】:

您不能在匿名 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);

【讨论】:

这与我开始工作时几乎完全一样。我看到的唯一问题是调用 q.ToString() 无论如何都会导致查询被编译,因为 ToString() 输出参数化的 SQL。我错过了什么吗? 小心 .ToString() 如果您更改变量名称但 LINQ 表达式相同,它将更改 ToString,因此键将不同。因此它将编译一个新的查询。 @tghw: .ToString() 不是问题;它将 lambda 表达式字符串化,而不是生成的 SQL,即“db => db.Foo.Where(x => !x.IsDisabled)”。我在 MVC 项目中本地验证了这一点。 @Stan:这不是一个真正的问题,因为您的代码中可能会有 N 个文字查询,而这些查询被调用的次数是 M*N 次。 您是在编译后的 IQueryable 还是原始 IQueryable 上调用 ToString?我在这里有测试代码调用QueryCache,就像我上面的示例代码一样,key 本地变量设置为没有 SQL 的 LINQ 查询的字面解释,就像我上面的评论一样。在 QueryCache 中的 lock 行上设置断点,看看你得到了什么。 -1 ToString() 是不够的。 2 个不同表的相同 WHERE 条件返回相同的字符串表示形式。例如: (x, y) => x.GetTable().Where(ag => (ag.Code = y)).FirstOrDefault() 可能代表对不同表的 2 个查询,具有相同的“Code = 'xxx '" 条件。【参考方案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);

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

【讨论】:

default(IEnumerable); == 空;在所有情况下。您正在默认一个接口。 哎呀..诚实的错误。我在那里有不同的代码,我只是复制并粘贴了我的代码,并没有仔细检查它。感谢您的关注。 这并不像你认为的那样。如果您在IEnumerable 上有一个Expression,则表达式.Compile() 与如果您不要求Expression 时将生成的实际Func IL 代码之间没有区别。事实上,exp.Compile() 很可能会更慢,因为你错过了很多编译器优化。 为了澄清我之前的评论,var results = list.Cache("To", x =&gt; x.Where(y =&gt; y.Contains("To"))) 将比简单地调用var results = list.Where(y =&gt; y.Contains("To")) @Jason:您可以使用 CompiledQuery.Compile 而不是 expression.Compile,当然这需要重构代码以采用 Func 并且需要您传递 DataContext到方法。由于需要进行大量重构,我将保持原样。【参考方案3】:

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

【讨论】:

您能否提供此声明的适当来源? 对不起,我不知道为什么我当时没有提供来源——我想这是我当时最近看过的东西,没有方便的链接 好的,你当时说的是真的吗?是否所有 LINQ 查询都在 .NET 4.5 中编译? 我希望如此 :) 特别是如果你是我曾经合作过的 Nick N。目前无法验证,但除非他们撤回该功能,否则这似乎是合理的【参考方案4】:

我不得不处理保存一个使用 LinqToSql 开发的超过 15 年/o 的项目,而且 CPU 太耗电了。

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

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

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

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

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

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

【讨论】:

以上是关于自动编译 Linq 查询的主要内容,如果未能解决你的问题,请参考以下文章

是否可以为 linq-to-objects 编译查询

是否可以为 linq-to-objects 编译查询

@Entity注解的类编译后未自动生成动态查询类的解决办法

ts 准备工作,及自动编译

EF编译查询偶尔会导致SqlException

linq