0%

原文地址: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

<configuration>

<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(IQueryable query)
{
string key = string.Concat(query.ToString(), “\n\r”,
typeof(T).AssemblyQualifiedName);
return key;
}

改进后的:

复制代码

public static string GetKey(IQueryable 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

///

/// subclass which encapsulates logic to /// traverse an expression tree and resolve all the query parameter values ///
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);   
}   

}

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