Entity Framework Code First Caching
Entity Framework Code First Caching
原文地址:http://dotnetspeak.com/index.php/2011/03/entity-framework-code-first-caching/最近团队改为Entity FrameWork 和ASP.NET MVC进行项目开发,为了提高访问速度必须缓存EF的查询结果,在网上查找了两种Cache的缓存方法,一种是基于EF Caching with Jarek Kowalski's Provider,博客园中已经有很多前辈们已经有了详细的介绍。还有一种方法,就是本文作者的实现方法,没有找到园子里有人翻译,就作为开博第一篇吧。
第一种方法在Code First实现下会有小问题,改造后也可以应用,稍后再整理。
原文作者需要实现一个方法明确哪些内容需要缓存,定义如下的扩展方法:
public static IEnumerable<T> AsCacheable<T>(this IQueryable<T> query)
{
if (cacheProvider == null)
{
throw new InvalidOperationException("Please set cache provider (call SetCacheProvider) before using caching");
}
return cacheProvider.GetOrCreateCache<T>(query);
}
最终的调用代码如下:
EFCacheExtensions.SetCacheProvider(MemoryCacheProvider.GetInstance());
using (ProductContext context = new ProductContext())
{
var query = context.Products.OrderBy(one => one.ProductNumber).
Where(one => one.IsActive).AsCacheable();
}
很简单吧。
在下面的例子里,作者实现了一个基于内存的 Cache Provider。使用全局静态变量来实现缓存。
作者定义了如下接口:
public interface IEFCacheProvider
{
IEnumerable<T> GetOrCreateCache<T>(IQueryable<T> query);
IEnumerable<T> GetOrCreateCache<T>(IQueryable<T> query, TimeSpan cacheDuration);
bool RemoveFromCache<T>(IQueryable<T> query);
}
下面的关键的实现代码,首先看Memory Provider实现
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections.Concurrent;
namespace EFCodeFirstCacheExtensions
{
public class MemoryCacheProvider : IEFCacheProvider
{
private MemoryCacheProvider() { }
public static MemoryCacheProvider GetInstance()
{
lock (locker)
{
if (dictionary == null)
{
dictionary = new ConcurrentDictionary<string, CacheItem>();
}
if (instance == null)
{
instance = new MemoryCacheProvider();
}
}
return instance;
}
private static ConcurrentDictionary<string, CacheItem> dictionary;
private static MemoryCacheProvider instance;
private static object locker = new object();
public IEnumerable<T> GetOrCreateCache<T>(IQueryable<T> query, TimeSpan cacheDuration)
{
string key = GetKey<T>(query);
CacheItem item = dictionary.GetOrAdd(
key,
(keyToFind) => { return new CacheItem()
{ Item = query.ToList(), AdditionTime = DateTime.Now }; });
if (DateTime.Now.Subtract(item.AdditionTime) > cacheDuration)
{
item = dictionary.AddOrUpdate(
key,
new CacheItem() { Item = item.Item, AdditionTime = DateTime.Now },
(keyToFind, oldItem) => { return new CacheItem()
{ Item = query.ToList(), AdditionTime = DateTime.Now }; });
}
foreach (var oneItem in ((List<T>)item.Item))
{
yield return oneItem;
}
}
public IEnumerable<T> GetOrCreateCache<T>(IQueryable<T> query)
{
string key = GetKey<T>(query);
CacheItem item = dictionary.GetOrAdd(
key,
(keyToFind) => { return new CacheItem()
{ Item = query.ToList(), AdditionTime = DateTime.Now }; });
foreach (var oneItem in ((List<T>)item.Item))
{
yield return oneItem;
}
}
public bool RemoveFromCache<T>(IQueryable<T> query)
{
string key = GetKey<T>(query);
CacheItem item = null;
return dictionary.TryRemove(key, out item);
}
private static string GetKey<T>(IQueryable<T> query)
{
string key = string.Concat(query.ToString(), "\n\r",
typeof(T).AssemblyQualifiedName);
return key;
}
}
}
Memory Provider 实现了IEFCacheProvider 接口,并实现了自动过期和不过期缓存。作者使用了EF Code First 的一个实用的功能 IQueryable 的ToString() 方法,得到执行的T-SQL语句。并且将SQL语句结合类名(查询语句返回的泛型结果T)作为缓存的KEY(这里有bug 下面有解决方案)。在执行插入缓存前执行了ToList 方法,因为Entity FrameWork是延迟执行的,直至调用结果前,查询不会执行。
接下来是AppFabric provider 的实现,要想使用AppFabric,首先要在本机安装AppFabric服务,然后添加以下引用:
Microsoft.ApplicationServer.Caching.Client
Microsoft.ApplicationServer.Caching.Core
下面是AppFabric 的实现
public class AppFabricCacheProvider : IEFCacheProvider
{
private AppFabricCacheProvider() { }
private static object locker = new object();
private static AppFabricCacheProvider instance;
private static DataCache cache;
public static AppFabricCacheProvider GetInstance()
{
lock (locker)
{
if (instance == null)
{
instance = new AppFabricCacheProvider();
DataCacheFactory factory = new DataCacheFactory();
cache = factory.GetCache("Default");
}
}
return instance;
}
public override IEnumerable<T> GetOrCreateCache<T>(IQueryable<T> query, TimeSpan cacheDuration)
{
string key = GetKey<T>(query);
var cacheItem = cache.Get(key);
if (cacheItem == null)
{
cache.Put(key, query.ToList(), cacheDuration);
foreach (var oneItem in query)
{
yield return oneItem;
}
}
else
{
foreach (var oneItem in ((List<T>)cacheItem))
{
yield return oneItem;
}
}
}
public override IEnumerable<T> GetOrCreateCache<T>(IQueryable<T> query)
{
string key = GetKey<T>(query);
var cacheItem = cache.Get(key);
if (cacheItem == null)
{
cache.Put(key, query.ToList());
foreach (var oneItem in query)
{
yield return oneItem;
}
}
else
{
foreach (var oneItem in ((List<T>)cacheItem))
{
yield return oneItem;
}
}
}
public override bool RemoveFromCache<T>(IQueryable<T> query)
{
string key = GetKey<T>(query);
CacheItem item = null;
return cache.Remove(key);
}
}
AppFabric 中已经内置了缓存依赖,不需要再自己计算缓存时间。上面使用DataCacheFactory方法来创建一个名为“Default”的实例。
单元测试中的AppFabric 的配置文件:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<!--configSections must be the FIRST element -->
<configSections>
<section name="dataCacheClient"
type="Microsoft.ApplicationServer.Caching.DataCacheClientSection, Microsoft.ApplicationServer.Caching.Core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
allowLocation="true"
allowDefinition="Everywhere"/>
</configSections>
<dataCacheClient>
<hosts>
<host
name="SERGEYB-PC1"
cachePort="22233"/>
</hosts>
<localCache
isEnabled="true"
sync="TimeoutBased"
objectCount="100000"
ttlValue="300" />
</dataCacheClient>
<connectionStrings>
<add name="ProductConnection"
connectionString="Server=(local);Database=Products;Trusted_Connection=True;"
providerName="System.Data.SqlClient"/>
</connectionStrings>
</configuration>
单元测试代码:
[TestMethod]
public void MemoryCacheProviderGetOrCreateCacheUsageTest()
{
EFCacheExtensions.SetCacheProvider(MemoryCacheProvider.GetInstance());
using (ProductContext context = new ProductContext())
{
var query = context.Products
.OrderBy(one => one.ProductNumber)
.Where(one => one.IsActive).AsCacheable();
Assert.AreEqual(2, query.Count(), "Should have 2 rows");
SQLCommandHelper.ExecuteNonQuery("Update Products Set IsActive = 0");
query = context.Products
.OrderBy(one => one.ProductNumber)
.Where(one => one.IsActive).AsCacheable();
Assert.AreEqual(2, query.Count(), "Should have 2 rows");
IQueryable<Product> cleanupQuery = context.Products
.OrderBy(one => one.ProductNumber)
.Where(one => one.IsActive);
EFCacheExtensions.RemoveFromCache<Product>(cleanupQuery);
query = context.Products
.OrderBy(one => one.ProductNumber)
.Where(one => one.IsActive).AsCacheable();
Assert.AreEqual(0, query.Count(), "Should have 0 rows");
EFCacheExtensions.RemoveFromCache<Product>(cleanupQuery);
}
}
上面的缓存Key生成代码有个BUG ,SQL查询的参数没有作为缓存Key的一部分,因此会导致查询结果一致,如下代码
var isActive = true;
var query = context.Products
.OrderBy(one => one.ProductNumber)
.Where(one => one.IsActive == isActive).AsCacheable();
var isActive = false;
var query = context.Products
.OrderBy(one => one.ProductNumber)
.Where(one => one.IsActive == isActive).AsCacheable();
查询结果相同。
stackoverflow上的解决方法 http://stackoverflow.com/questions/8275881/generating-cache-keys-from-iqueryable-for-caching-results-of-ef-code-first-queri原来的缓存KEY生成方法很简单:
private static string GetKey<T>(IQueryable<T> query)
{
string key = string.Concat(query.ToString(), "\n\r",
typeof(T).AssemblyQualifiedName);
return key;
}
改进后的:
public static string GetKey<T>(IQueryable<T> query)
{
var keyBuilder = new StringBuilder(query.ToString());
var queryParamVisitor = new QueryParameterVisitor(keyBuilder);
queryParamVisitor.GetQueryParameters(query.Expression);
keyBuilder.Append("\n\r");
keyBuilder.Append(typeof (T).AssemblyQualifiedName);
return keyBuilder.ToString();
}
QueryParameterVisitor 的实现方法:
/// <summary>
/// <see cref="ExpressionVisitor"/> subclass which encapsulates logic to
/// traverse an expression tree and resolve all the query parameter values
/// </summary>
internal class QueryParameterVisitor : ExpressionVisitor
{
public QueryParameterVisitor(StringBuilder sb)
{
QueryParamBuilder = sb;
Visited = new Dictionary<int, bool>();
}
protected StringBuilder QueryParamBuilder { get; set; }
protected Dictionary<int, bool> Visited { get; set; }
public StringBuilder GetQueryParameters(Expression expression)
{
Visit(expression);
return QueryParamBuilder;
}
private static object GetMemberValue(MemberExpression memberExpression, Dictionary<int, bool> visited)
{
object value;
if (!TryGetMemberValue(memberExpression, out value, visited))
{
UnaryExpression objectMember = Expression.Convert(memberExpression, typeof (object));
Expression<Func<object>> getterLambda = Expression.Lambda<Func<object>>(objectMember);
Func<object> getter = null;
try
{
getter = getterLambda.Compile();
}
catch (InvalidOperationException)
{
}
if (getter != null) value = getter();
}
return value;
}
private static bool TryGetMemberValue(Expression expression, out object value, Dictionary<int, bool> visited)
{
if (expression == null)
{
// used for static fields, etc
value = null;
return true;
}
// Mark this node as visited (processed)
int expressionHash = expression.GetHashCode();
if (!visited.ContainsKey(expressionHash))
{
visited.Add(expressionHash, true);
}
// Get Member Value, recurse if necessary
switch (expression.NodeType)
{
case ExpressionType.Constant:
value = ((ConstantExpression) expression).Value;
return true;
case ExpressionType.MemberAccess:
var me = (MemberExpression) expression;
object target;
if (TryGetMemberValue(me.Expression, out target, visited))
{
// instance target
switch (me.Member.MemberType)
{
case MemberTypes.Field:
value = ((FieldInfo) me.Member).GetValue(target);
return true;
case MemberTypes.Property:
value = ((PropertyInfo) me.Member).GetValue(target, null);
return true;
}
}
break;
}
// Could not retrieve value
value = null;
return false;
}
protected override Expression VisitMember(MemberExpression node)
{
// Only process nodes that haven't been processed before, this could happen because our traversal
// is depth-first and will "visit" the nodes in the subtree before this method (VisitMember) does
if (!Visited.ContainsKey(node.GetHashCode()))
{
object value = GetMemberValue(node, Visited);
if (value != null)
{
QueryParamBuilder.Append("\n\r");
QueryParamBuilder.Append(value.ToString());
}
}
return base.VisitMember(node);
}
}
没有怎么翻译里面的内容,相信大牛们也不需要看我这稀烂的文笔。