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实现

View Code
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 的实现

View Code
    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 的配置文件:

View Code
<?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>

单元测试代码:

View Code
  [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);

}



}

 源代码下载地址:http://google.proxysec.com/baidu.com.php?u=a5c236e4cca5cc48bd8aOi8vZG90bmV0c3BlYWsuY29tL0Rvd25sb2Fkcy9FRkNvZGVGaXJzdENhY2hlLnppcA%3D%3D&b=1

上面的缓存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 的实现方法:

View Code
/// <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);
}
}

没有怎么翻译里面的内容,相信大牛们也不需要看我这稀烂的文笔。

 


 

posted @ 2012-03-23 11:49  我是小马  阅读(1485)  评论(2编辑  收藏  举报