Entity Framework之深入分析
EF虽然是一个晚生畸形的ORM框架,但功能强大又具有灵活性的,给了开发人员一定的发挥空间。因为微软出发点总是好的,让开发变得简单,但实际上不是所有的事情都这么理想。这里顺便推荐马丁大叔的书《企业应架构模式》。
本节主要深入分析EF的分层问题,下面是本节的已列出的要探讨内容。
- 领域模型的概念
- DbContext与Unit of Work 的概念
- DbContext 创建实例及线程安全问题
- 不要随便using或Dispose DbContext
- DbContext的SaveChanges事务
- Repository与UnitOfWork引入
- DbContext T4模板的应用
- EDM文件是放在DAL层还是Model层中?
- EF MVC项目分层
一、领域模型的概念
领域模型:是描述业务用例实现的对象模型。它是对业务角色和业务实体之间应该如何联系和协作以执行业务的一种抽象。 业务对象模型从业务角色内部的观点定义了业务用例。定义很商业,很抽象,也很理解。一个商业的概念被引入进来之后,引发很多争议和思考。而DomainObject 在我们实际的项目又演化成大致下面几种
1.纯事务脚本对象(只有字段的set,get),没有任何业务(包括没有导航属性),可以以理解为贫血的领域模型。
2.带有自身业务的对象,例如验证业务,关联导航等。
3.对象包含量了大量的业务,而这些业务中并不是所有业务都和它相关。
尤其是第2种,界限很难划分,怎么判断这个业务是自身的,还是其它的? 或者是否重用度高呢? 第一种和第三种在之前的项目都使用过,目前个人觉得EF现在走的是第2种路线,EF在生成Model模型后,依然可以对模型进行业务修改。我们也不必在这样上面纠结太多,项目怎么做方便就怎么去实现。比如纯净的POCO我可以当DTO或VO使用;而第3种情况,我们在微软的DataSet时,也是大量使用的。想详细了解这段的可以参照这篇讨论
二、DbContext与Unit of Work 的概念
在马丁大叔中书看我们可以准看到Unit of Work 的定义:维护受业务事务影响的对象列表,并协调变化的写入和并发问题的解决。即管理对象的CRUD操作,以及相应的事务与并发问题等。Unit of Work的是用来解决领域模型存储和变更工作,而这些数据层业务并不属于领域模型本身具有的。而DbContext其实就是一个Unit of work ,只是如果直接使用这个DbContext 的话,那DbContext所有的业务都是直接暴露的,当然这是看是否项目需要了。可以看出微软的EF DbContext借用了Unit of work的思想。
三、DbContext 创建实例及线程安全问题
1. DbContext不适合创建成单例模式,例如A对象正在编辑,B对象编辑完了提交,导致正在编辑的A对象也被提交了,但是A的改可能要取消的,但是最终都被提交到数据库中了。
2. 如果DbContext创建过多的实例,就要控制好并发的问题,因为不同实例的DbContext可能会对同一条记录进行修改。
3. DbContext线程安全问题,同一实例的DbContext被不同线程调用会引发第一条场景的情况。不同线程使用不同实例的DbContext时又会引发第二种场景的情况。
第一种情况很难控制,而第二种情况可以采用乐观并发锁来解决,其次就是尽量避免对一记录的写操作。
四、不要随便using或Dispose DbContext
我们先来看一段代码
BlogCategory cate = null;
using (DemoDBEntities context = new DemoDBEntities())
{
//context.Configuration.LazyLoadingEnabled = false;
DbSet<BlogCategory> set = context.Set<BlogCategory>();
cate = set.Find(2);
}
//肯定会出错 因为DbContext被释放了 无法延迟加载对象
BlogArticle blog = cate.BlogArticle.First();
当我们在使用延迟加载的时候,如果使用using或dispose 释放掉DbContext后,就无法延迟加载导航属性。为什么?我们来看一下DbContext是如何加载对象以及导航属性的。
将上面的代码修改一下
static void Main(string[] args)
{
BlogCategory cate = null;
using (DemoDBEntities context = new DemoDBEntities())
{
//context.Configuration.LazyLoadingEnabled = false;
DbSet<BlogCategory> set = context.Set<BlogCategory>();
cate = set.Find(2);
//肯定会出错 因为DbContext被释放了 无法延迟加载对象
BlogArticle blog = cate.BlogArticle.First();
}
Console.ReadLine();
}
我们打开SQL Server Profiler 来监视一上面的代码执行情况
可以看如果DbContext如果在第一次读取BlogCategory被释放后,那在加载导航属性的时候肯定不会执行成功。
另外一点:为什么很多人一定要using 或dispose掉DbContext ?
是担心数据库连接没有释放?还是担心DbContext占用过多资源呢?
首先担心数据库连接没有释放肯定是多余的,因为DbContext在SaveChanges完成后会释放掉打开的数据库连接,我们来反编译一下SaveChages的源码看看
public virtual int SaveChanges(SaveOptions options)
{
this.OnSavingChanges();
if ((SaveOptions.DetectChangesBeforeSave & options) != SaveOptions.None)
{
this.ObjectStateManager.DetectChanges();
}
if (this.ObjectStateManager.SomeEntryWithConceptualNullExists())
{
throw new InvalidOperationException(Strings.ObjectContext_CommitWithConceptualNull);
}
bool flag = false;
int objectStateEntriesCount = this.ObjectStateManager.GetObjectStateEntriesCount(EntityState.Modified | EntityState.Deleted | EntityState.Added);
using (new EntityBid.ScopeAuto("<dobj.ObjectContext.SaveChanges|API> %d#, affectingEntries=%d", this.ObjectID, objectStateEntriesCount))
{
EntityConnection connection = (EntityConnection) this.Connection;
if (0 >= objectStateEntriesCount)
{
return objectStateEntriesCount;
}
if (this._adapter == null)
{
IServiceProvider providerFactory = connection.ProviderFactory as IServiceProvider;
if (providerFactory != null)
{
this._adapter = providerFactory.GetService(typeof(IEntityAdapter)) as IEntityAdapter;
}
if (this._adapter == null)
{
throw EntityUtil.InvalidDataAdapter();
}
}
this._adapter.AcceptChangesDuringUpdate = false;
this._adapter.Connection = connection;
this._adapter.CommandTimeout = this.CommandTimeout;
try
{
this.EnsureConnection();
flag = true;
Transaction current = Transaction.Current;
bool flag2 = false;
if (connection.CurrentTransaction == null)
{
flag2 = null == this._lastTransaction;
}
using (DbTransaction transaction = null)
{
if (flag2)
{
transaction = connection.BeginTransaction();
}
objectStateEntriesCount = this._adapter.Update(this.ObjectStateManager);
if (transaction != null)
{
transaction.Commit();
}
}
}
finally
{
if (flag)
{
this.ReleaseConnection();
}
}
if ((SaveOptions.AcceptAllChangesAfterSave & options) == SaveOptions.None)
{
return objectStateEntriesCount;
}
try
{
this.AcceptAllChanges();
}
catch (Exception exception)
{
if (EntityUtil.IsCatchableExceptionType(exception))
{
throw EntityUtil.AcceptAllChangesFailure(exception);
}
throw;
}
}
return objectStateEntriesCount;
}
可以看到DbContext 每次打开 EntityConnection 最后都会 finally 时 通过this.ReleaseConnection() 释放掉连接,所以这个担心是多余的。
其次DbContext 是否占用过多的资源呢?DbContext确实占用了资源,主要体现在DbContext的Local属性上,每一次的增删改查,Loacl都会从数据库中加载数据,而这些数据在SaveChanges之后并没有释放掉。因此释放DbContext 是需要的,但是这样又会影响到延迟加载。这样的话,我们可以通过重载SaveChanges,在SaveChanges之后清除掉Local中的数据。但是这样做为什么有问题,我也不知道,有待考证。上一节中有介绍重载SaveChanges 清除Local 数据阻止查询数据更新。
五、DbContext的SaveChanges自带事务与分布式事务
通过反编译可以看到单实例DbContext的SaveChanges方式默认开启了事务,当同时更新多条记录时,有一条失败就会RollBack。模拟测试代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using EF.Model;
using EF.DAL;
using System.Data.Entity;
using System.Collections;
using System.Transactions;
namespace EF.Demo
{
class Program
{
static void Main(string[] args)
{
BlogCategory cate = null;
DemoDBEntities context = new DemoDBEntities();
//DemoDBEntities context2 = new DemoDBEntities();
try
{
//using (TransactionScope scope = new TransactionScope())
//{
//context.Configuration.LazyLoadingEnabled = false;
DbSet<BlogCategory> set = context.Set<BlogCategory>();
cate = new BlogCategory();
cate.CateName = "2010-7";
cate.CreateTime = DateTime.Now;
cate.BlogArticle.Add(new BlogArticle() { Title = "2011-7-15" });
set.Add(cate);
//由于没设置Title字段,并且CreateTime字段不能为空,故会引发异常
context.Set<BlogArticle>().Add(new BlogArticle { BlogCategory_CateID = 2 });
int a = context.SaveChanges();
// context2.Set<BlogArticle>().Add(new BlogArticle { BlogCategory_CateID = 2 });
// int b = context2.SaveChanges();
// scope.Complete();
//}
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
}
Console.ReadLine();
}
}
}
通过SQL SERVER Profile 监视到没有一句SQL语句被执行,SaveChanges事务是预执新所有操作成功后才会更新到数据库中。
我们再来测试一下分布式事务,创建的Context2用于模拟代表其它数据库
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using EF.Model;
using EF.DAL;
using System.Data.Entity;
using System.Collections;
using System.Transactions;
namespace EF.Demo
{
class Program
{
static void Main(string[] args)
{
BlogCategory cate = null;
DemoDBEntities context = new DemoDBEntities();
DemoDBEntities context2 = new DemoDBEntities();
try
{
using (TransactionScope scope = new TransactionScope())
{
//context.Configuration.LazyLoadingEnabled = false;
DbSet<BlogCategory> set = context.Set<BlogCategory>();
cate = new BlogCategory();
cate.CateName = "2010-7";
cate.CreateTime = DateTime.Now;
cate.BlogArticle.Add(new BlogArticle() { Title = "2011-7-15" });
set.Add(cate);
//实例1 对数据库执行提交
int a = context.SaveChanges();
//实例2 模拟其它数据库提交 时间字段为空,无法更新成功
context2.Set<BlogArticle>().Add(new BlogArticle { Title="2011-7-16", BlogCategory_CateID = 2 });
int b = context2.SaveChanges();
scope.Complete();
}
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
}
Console.ReadLine();
}
}
}
通过SQL SERVER Profile 监视,虽然context实际执行了两条SQL记录,但是context2的SQL没有执行成功,导致事务回滚,所有操作都被没有执行成功。
六、Repository与UnitOfWork引入
Repository是什么? 马丁大叔的书上同样也有解释:它是衔接数据映射层和域之间的一个纽带,作用相当于一个在内存中的域对象映射集合,它分离了领域对象和数据库访问代码的细节。Repository受DomainObject驱动,Repository用于实现不属于DomainObject的自身相关的,但又受DomainObject约束的业务。如CRUD操作就不是领域模型要关注的业务,但是领域模型最终要映射为数据关系保存到数据库中。一个领域模型要有对应的Repository来处理与数据层衔接过程。但不是所有的DomainObject对Repository约束是相同的,可能这个领域对象没有对应Repository删除操作,而别外一个却有,所以我们经常使用的泛型Repository<T> 是不合适的。但是为了代码简洁重用,大家根据实际情况还是使用了简洁的IRepository<T>接口,就像我们有时为了简单直接把POCO当DTO或VO使用了。如果不引入Repository,我觉得没有必要实现DAL层,因为DbContext本身就是DAL层,然后只要为DbContext定义好接IDAL接口从而必免与BLL层的耦合。从这里就可以看出Repository与DAL的区别,一个受域业务驱动出现的,一个是出于解除耦合出现的。
UnitOfWork 工作单元,前面已经介绍过。为了减少业务层频繁调用DbContext的SaveChanges同步数据库操作(将多个对象的更新一次提交,减少与数据库交互过程),又要保证DbContext对业务层封闭,所以我们要增加一个对业务层开放的接口。想一想如果把SaveChanges的方法下放到每个Repository中或者DAL中,那业务层在协调多个Repository事务操作时,就会频繁的写数据库。而分离了Repository中的所有SaveChanges (或者撤销以及完成单元工作后销毁等操作)后,并通过接口在业务层统一调用,这样既大大提高了效率,也体现了一个完整的单元工作业务。
七、DbContext T4模板的应用
在Model First中,我们借助于EDMX 和T4模板完成了DbContext和Model的初步设计。但是微软提供的这些模板不能满足用户的所有需求,这个时候我就要修改T4 来生成我们想要的代码。
T4模板应用非常广泛,很多ORM工具的模板也在使用的T4模板,T4也可以生成HTML,JS等多种语言。T4模板支持多种语言书写,可读性很强,也容易上手。
DbContext模板 一共分为两个 DemoDB.DbContext.tt (unit of work)和DemoDb.tt (model) 。前一节我们介绍了如何修改DemoDb.tt 模板 使我们POCO模型继承POCOEntity,这一节再修改一下DemoDb.DbContext.tt模板 使其继承IUnitOfWork接口。
首先我们在Model层中增加IUnitOfWork接口如下
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace EF.Model
{
public interface IUnitOfWork
{
//事务提交
int Save();
}
}
我们再修改DemoDb.DbContext.tt模板
<#@ template language="C#" debug="false" hostspecific="true"#>
<#@ include file="EF.Utility.CS.ttinclude"#><#@
output extension=".cs"#><#
var loader = new MetadataLoader(this);
var region = new CodeRegion(this);
//---------------------------------------------------这里导入了DemoDB.edmx映射文件---------------add by mecity
var inputFile = @"DemoDB.edmx";
//---------------------------------------------------映射文件转为集合方便模板篇历生成代码--------add by mecity
var ItemCollection = loader.CreateEdmItemCollection(inputFile);
Code = new CodeGenerationTools(this);
EFTools = new MetadataTools(this);
ObjectNamespace = Code.VsNamespaceSuggestion();
ModelNamespace = loader.GetModelNamespace(inputFile);
EntityContainer container = ItemCollection.GetItems<EntityContainer>().FirstOrDefault();
if (container == null)
{
return string.Empty;
}
#>
//------------------------------------------------------------------------------
// <auto-generated>
// <#=GetResourceString("Template_GeneratedCodeCommentLine1")#>
//
// <#=GetResourceString("Template_GeneratedCodeCommentLine2")#>
// <#=GetResourceString("Template_GeneratedCodeCommentLine3")#>
// </auto-generated>
//------------------------------------------------------------------------------
<#
if (!String.IsNullOrEmpty(ObjectNamespace))
{
#>
namespace <#=Code.EscapeNamespace(ObjectNamespace)#>
{
<#
PushIndent(CodeRegion.GetIndent(1));
}
#>
using System;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
//---------------------------------------------------这加加入对EF.Model命名空间的引用---------------add by mecity
using EF.Model;
<#
if (container.FunctionImports.Any())
{
#>
using System.Data.Objects;
<#
}
#>
//---------------------------------------------------这里加入对IUnitOfWork接口继承---------------add by mecity
<#=Accessibility.ForType(container)#> partial class <#=Code.Escape(container)#> : DbContext,IUnitOfWork
{
public <#=Code.Escape(container)#>()
: base("name=<#=container.Name#>")
{
<#
WriteLazyLoadingEnabled(container);
#>
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
throw new UnintentionalCodeFirstException();
}
//---------------------------------------------------这里加入对IUnitOfWork接口方法的实现---------------add by mecity
public int Save()
{
return base.SaveChanges();
}
注意T4模板中加了注释的地方,保存模板后,就会重新创建DemoDBEntities,看一下模板修改后生成后的代码
//------------------------------------------------------------------------------
// <auto-generated>
// 此代码是根据模板生成的。
//
// 手动更改此文件可能会导致应用程序中发生异常行为。
// 如果重新生成代码,则将覆盖对此文件的手动更改。
// </auto-generated>
//------------------------------------------------------------------------------
namespace EF.DAL
{
using System;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
//---------------------------------------------------这加加入对EF.Model命名空间的引用---------------add by mecity
using EF.Model;
//---------------------------------------------------这里加入对IUnitOfWork接口继承---------------add by mecity
public partial class DemoDBEntities : DbContext,IUnitOfWork
{
public DemoDBEntities()
: base("name=DemoDBEntities")
{
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
throw new UnintentionalCodeFirstException();
}
//---------------------------------------------------这里加入对IUnitOfWork接口方法的实现---------------add by mecity
public int Save()
{
return base.SaveChanges();
}
public DbSet<BlogArticle> BlogArticle { get; set; }
public DbSet<BlogCategory> BlogCategory { get; set; }
public DbSet<BlogComment> BlogComment { get; set; }
public DbSet<BlogDigg> BlogDigg { get; set; }
public DbSet<BlogTag> BlogTag { get; set; }
public DbSet<BlogMaster> BlogMaster { get; set; }
}
}
八、EDM文件是放在DAL层还是Model层中?
记得我第一篇EF介绍中将EDMX文件和Model放在一起,这样做有一定风险,按照领域模型的概念,Model中这些业务对象被修改的可能性非常高,并且每个业务对象的修改的业务都可能不同,因此修改DemoDB.tt模板满足所有对象是不实现的, 并且意外保存EDMX文件时,也会导致Model手动修改的内容丢失。因此EDMX不适合和Model放在一起,最好移至到DAL层或Repository层。DAL中的DemoDb.DbContext.tt模板生成代码是相对固定的(只有一个DemoDBEntities类),因此对DemoDb.DbContext.tt模板的修改基本可以满足要求。见上节T4应用。我们可以先在DAL中的EDMX完成POCO对象的初步生成与映射关系工作后,再移至到Model中处理。
九、EF MVC项目分层
就目前CodePlex上的微软项目NorthwindPoco/Oxite/Oxite2)以及其它开源的.net mvc EF项目分层来看,大致结构如下
View 视图
Controller 控制器
IService Controller调用具体业务的接口
Service IService的具体实现 ,利用IOC注入到Controller
Repository 是IRepository 的具体实现,利用IOC注入到Service
Model+IRepository 因为IRepository接口对应的是DoaminModel约束业务,并且都是直接开放给Service 调用的,所以放在一个类库下也容易理解,当然分开也无影响。
VO/DTO ViewObject与DTO 传输对象类库
当然这只是参考,怎么合理分层还是依项目需求,项目进度,资源情况以及后期维护等情况而定。