我们发现 编译我们的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<>
对象,我可以在第一次自动编译查询后调用到这些对象。
你不能让扩展方法在匿名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);
既然没人尝试,那我就试试吧。也许我们两个都能解决这个问题。这是我的尝试。
我使用一个字典来设置,我也没有使用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);
期待对此进行一些讨论,以进一步发展这个想法。
为了以后的发展:.NET Framework 4.5将默认这样做(根据我刚看的一个演示中的幻灯片)。
我不得不处理保存一个>15yo的项目,这个项目是使用LinqToSql开发的,太耗费CPU。
基准测试显示,使用编译查询对复杂查询快了x7,对简单查询快了x2(考虑到运行查询本身可以忽略不计,这里只是考虑编译查询的吞吐量)。
缓存不是由.Net Framework自动完成的(无论什么版本),这只发生在Entity Framework而不是LINQ-TO-SQL上,而且这些是不同的技术。
编译后的查询的使用是很棘手的,所以这里有两个重要的重点。
考虑到这一点,我想到了这个缓存类。使用其他评论中提出的静态方法有一些可维护性的缺点--主要是可读性较差--另外,要迁移现有的庞大代码库也比较困难。
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);
}
}