Entity Framework - 使用DbContext的二级缓存
在使用 DbContext 的 Entity Framework 中启用二级缓存的方法。
引言
Entity Framework 开箱即用并不支持二级缓存。Jarek Kowalski 的 EFCachingProvider 项目提供了一种创建提供程序包装器以支持缓存的方法,但大多数现有示例都使用 ObjectContext 而不是 DbContext 来创建包装器。这要求您有一个 edmx 文件 作为您的模型。
本文的目的是展示如何使用 EntityConfiguration
映射来创建 DbContext
的包装器,从而支持您 EF 应用程序中的二级缓存。
背景
最基本地说,二级缓存是一种查询缓存。SQL 命令的结果存储在缓存中,这样相同的 SQL 命令就会从缓存中检索其数据,而不是再次针对底层提供程序执行查询。这可以为您的应用程序带来性能提升,并减少与数据库的交互,但会增加内存消耗。
在使用 ORM 时,关于缓存应该在哪里发生存在一些争论。缓存数据是 ORM 的职责,还是它应该只关注数据任务,而缓存完全是您在应用程序域内的责任?我个人喜欢有选择的权利 - NHibernate 和其他 ORM 允许您通过指定自己的缓存实现来做到这一点。
Entity Framework 缓存的另一个问题是,您不能直接缓存查询结果,因为它们将与 Context 相关联,并且对象不能同时与多个 Context 相关联,您会看到类似“ObjectStateManager
中已存在具有相同键的对象。ObjectStateManager
无法跟踪具有相同键的多个对象”的异常。
这通常会导致开发人员创建“可缓存的 DTO”,然后将实体 POCO 对象转换为这些 DTO,以便他们可以缓存数据。(更多信息 可在此处获得。)然后,您必须实现缓存失效,以确保您的缓存对象与任何数据更新保持同步。这无疑增加了应用程序的复杂性,而这些复杂性本可以由 ORM 来处理。
有关二级缓存的更多信息,Ayende Rahien 的一篇非常好的文章 可在此处找到。
必备组件
二进制文件
为了减小下载大小,我只包含了让项目运行所需的最低限度。我建议您使用 NuGet 来自行刷新引用。
- 我包含了 EFCachingProvider 项目的略微修改过的二进制文件,但您可以自行下载。我将在文章中进一步描述我所做的更改。
- 演示应用程序使用的是 NuGet 包
EntityFramework.5.0.0-rc
。 - 该应用程序还使用 Castle Windsor 进行一些基本的依赖注入。
数据库
演示应用程序使用 SQL Server 和大家最喜欢的数据库 Northwind
。如果您没有
Northwind
的副本,可以从 Codeplex 下载创建数据库的 SQL。
Using the Code
首先,请修改 Web.Config 文件,并将连接字符串更改为您环境中有效的 Northwind
连接。
当您在 config 文件中时,请注意我们需要在 config 中注册我们的自定义数据提供程序。
<system.data>
<DbProviderFactories>
<add name="EF Caching Data Provider" invariant="EFCachingProvider"
description="Caching Provider Wrapper"
type="EFCachingProvider.EFCachingProviderFactory, EFCachingProvider" />
<add name="EF Tracing Data Provider" invariant="EFTracingProvider"
description="Tracing Provider Wrapper"
type="EFTracingProvider.EFTracingProviderFactory, EFTracingProvider" />
<add name="EF Generic Provider Wrapper" invariant="EFProviderWrapper"
description="Generic Provider Wrapper"
type="EFProviderWrapperToolkit.EFProviderWrapperFactory, EFProviderWrapperToolkit" />
</DbProviderFactories>
</system.data>
当 Entity Framework 尝试创建 DbContext
时,它使用工厂模式来生成 DbConnection
对象。工厂可以通过使用 Database.DefaultConnectionFactory 以编程方式设置,或者您可以在 web.config 中指定。我们只需要告诉框架在创建我们上下文的实例时使用我们的自定义工厂。
<entityFramework>
<defaultConnectionFactory type="SecondLevelCaching.Data.CachedContextConnectionFactory,
SecondLevelCaching.Data">
</defaultConnectionFactory>
<contexts>
<context type="SecondLevelCaching.Data.Models.NorthwindContext,
SecondLevelCaching.Data">
</context>
</contexts>
</entityFramework>
Connection factories 必须实现 System.Data.Entity.Infrastructure.IDbConnectionFactory
。我们的自定义实现对于本示例很简单,我们只需要创建并返回一个使用 EFCachingConnection
对象的 DbConnection
。
public class CachedContextConnectionFactory :
System.Data.Entity.Infrastructure.IDbConnectionFactory
{
public DbConnection CreateConnection(string nameOrConnectionString)
{
var providerInvariantName = "System.Data.SqlClient";
var wrappedConnectionString = "wrappedProvider=" +
providerInvariantName + ";" +
nameOrConnectionString;
return new EFCachingConnection
{
ConnectionString = wrappedConnectionString,
CachingPolicy = CachingPolicy.CacheAll,
Cache = EntityCache.Instance
};
}
}
缓存对象
数据上下文是短暂的,在 Web 应用程序中,它们将在每个 HttpRequest
的生命周期内创建。但是,我们需要我们的缓存机制超越这个生命周期,以便后续的 HTTP 请求可以访问之前缓存的数据。我正在使用一个简单的 Singleton
对象,该对象实现在 EntityCache
类中。
注意: EFCachingConnection
要求您设置一个实现 ICache
的对象,在实际应用程序中,您肯定会使用 ICache
而不是这里的具体实现。我仅出于演示目的使用 InMemoryCache
来在 UI 中访问缓存统计信息。
public class EntityCache
{
private static InMemoryCache cacheInstance;
private static object lockObject = new object();
public static InMemoryCache Instance
{
get
{
if (cacheInstance == null)
{
lock (lockObject)
{
if (cacheInstance == null)
cacheInstance = new InMemoryCache();
}
}
return cacheInstance;
}
}
}
简化 DbContext
为我们的 DbContext
创建一个接口,然后我们可以将其用于依赖注入,这始终是一个好主意。接口和实现如下所示
public interface IDbContext
{
IQueryable<T> Table<T>() where T : class;
int SaveChanges();
}
public class NorthwindContext : DbContext, IDbContext
{
static NorthwindContext()
{
Database.SetInitializer<NorthwindContext>(null);
}
public NorthwindContext(string nameOrConnectionString)
: base(nameOrConnectionString)
{
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
System.Type configType = typeof(CategoryMap);
var typesToRegister = Assembly.GetAssembly(configType).GetTypes()
.Where(type => !String.IsNullOrEmpty(type.Namespace))
.Where(type => type.BaseType != null && type.BaseType.IsGenericType &&
type.BaseType.GetGenericTypeDefinition() == typeof(EntityTypeConfiguration<>));
foreach (var type in typesToRegister)
{
dynamic configurationInstance = Activator.CreateInstance(type);
modelBuilder.Configurations.Add(configurationInstance);
}
}
public IQueryable<T> Table<T>() where T : class
{
return this.Set<T>();
}
}
上面的代码只是在 OnModelCreating
事件中添加了所有 Mapping
类,并通过 Table
方法提供了对 Queryable
源的访问。
将它们整合在一起
现在我们可以整合我们的依赖项并实际查看它了。本示例使用 Windsor Castle,但它适用于您选择的任何 DI 库。我们只需要告知容器一个接口 - IDbContext
。
var connectionString = System.Configuration.ConfigurationManager.ConnectionStrings
["Northwind"].ConnectionString;
if (string.IsNullOrEmpty(connectionString))
throw new Exception("The connection string for Northwind
could not be found in the configuration, please make sure you have set this");
container.Register(
Component.For<SecondLevelCaching.Data.IDbContext>()
.ImplementedBy<SecondLevelCaching.Data.Models.NorthwindContext>()
.LifeStyle.PerWebRequest
.Named("NorthwindCachedContext")
.DependsOn(
Parameter.ForKey("nameOrConnectionString").Eq(connectionString))
);
我们的 HomeController
现在可以解析此依赖项并使用它来查询数据库。
public class HomeController : Controller
{
private readonly IDbContext dataContext;
public HomeController(IDbContext context)
{
this.dataContext = context;
}
主视图
此演示中只有一个视图,即 Northwind
的 customer
数据列表。视图中还显示了缓存统计信息,它将显示何时从缓存中检索到某个项(CacheHit
)或从数据库中检索到(CacheMiss
、CacheAdd
)。搜索框允许您按客户名称过滤数据。
尝试输入一些不同的搜索词。您会注意到 CacheMiss
和 CacheAdd
的数量会增加。现在尝试输入您以前使用过的搜索词,您应该会看到 CacheHit
的数量增加。
当您看到这一点时,您已从 EFCachingProvider
检索到查询数据,并且没有 SQL 语句在您的数据库上执行。这就是二级缓存的作用!
关注点
为了使 EFCachingProvider
与 Code First 一起工作,需要进行一些修改。在此项目中,我使用了“逆向工程 Code First”工具来从现有数据库创建我的模型,但如果您想在域模型优先的应用程序中使用它,那么当提供程序尝试创建 Command
对象时,您会收到一个异常。这是因为在 DbConnectionWrapper
类中,CreateDbCommand 方法会抛出“Not Supported
”异常。一个简单的修复方法是实现该方法...
protected override DbCommand CreateDbCommand()
{
return wrappedConnection.CreateCommand();
}
摘要
此演示项目展示了如何使用数据库连接工厂提供一个自定义工厂,该工厂将缓存连接注入到 DbContext
管道中。
有关缓存解决方案的更多信息,请访问开头段落中链接的 Codeplex 页面。
历史
- 2012 年 8 月 6 日:初始版本