自动编译 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<>
对象,我可以在第一次自动编译查询后调用这些对象。
【问题讨论】:
这是一个令人困惑的问题,因为您似乎将 LINQ 和 LINQ to SQL 混为一谈(每次运行查询时,它都会在后台额外生成、编译和缓存执行计划)。如果您询问 SQL Server 的已编译执行计划,那么(据我所知)除了运行它们之外,无法编译它们并保持缓存。 这与 SQL Server 无关。每次运行这些查询时,LINQ to SQL 都会从两种 LINQ 语法(链式或 SQL 样式)编译查询(这可能需要相当长的时间)到 SQL。阅读顶部的链接以了解更多信息。 我在 Web 应用程序中使用 L2S 编译查询时发现的一个问题是,要编译它,您需要将 DataContext 的实例传递给它 - 对于 Web 应用程序,这意味着您需要一个共享整个站点的 DataContext - 当站点开始有大负载时,这反过来给我带来了一些主要的多线程问题。我真的很不喜欢在编译查询时必须传递 datacontext 实例... 编译查询时不传入 DataContext。请参阅下面的答案;你实际上传入了一个委托,它接受一个 DataContext(和其他参数)并返回一个IQueryable<T>
。
你有没有实现过这条路径?如果是这样,您是如何处理不同的 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(IEnumerableIEnumerable
上有一个Expression
,则表达式.Compile() 与如果您不要求Expression
时将生成的实际Func
IL 代码之间没有区别。事实上,exp.Compile() 很可能会更慢,因为你错过了很多编译器优化。
为了澄清我之前的评论,var results = list.Cache("To", x => x.Where(y => y.Contains("To")))
将比简单地调用var results = list.Where(y => y.Contains("To"))
慢。
@Jason:您可以使用 CompiledQuery.Compile 而不是 expression.Compile,当然这需要重构代码以采用 Func为了未来:.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 查询的主要内容,如果未能解决你的问题,请参考以下文章