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
我們先來看一段代碼
View Code
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是如何加載對象以及導航屬性的。
將上面的代碼修改一下
View Code
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的源碼看看
View Code
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。模擬測試代碼
View Code
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用于模擬代表其它數據庫
View Code
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接口如下
View Code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace EF.Model
{
public interface IUnitOfWork
{
//事務提交
int Save();
}
}
我們再修改DemoDb.DbContext.tt模板
View Code
<#@ 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,看一下模板修改后生成后的代碼
View Code
//------------------------------------------------------------------------------
// <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 傳輸對象類庫
當然這只是參考,怎么合理分層還是依項目需求,項目進度,資源情況以及后期維護等情況而定。

浙公網安備 33010602011771號