EF雖然是一個晚生畸形的ORM框架,但功能強大又具有靈活性的,給了開發人員一定的發揮空間。因為微軟出發點總是好的,讓開發變得簡單,但實際上不是所有的事情都這麼理想。這裡順便推薦馬丁大叔的書《企業應架構模式》。 本節主要深入分析EF的分層問題,下麵是本節的已列出的要探討內容。 領域模型的概念 DbCo ...
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
我們先來看一段代碼
![](http://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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是如何載入對象以及導航屬性的。
將上面的代碼修改一下
![](http://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
staticvoid 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的源碼看看
![](http://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
publicvirtualint SaveChanges(SaveOptions options)
{
this.OnSavingChanges();
if ((SaveOptions.DetectChangesBeforeSave & options) != SaveOptions.None)
{
this.ObjectStateManager.DetectChanges();
}
if (this.ObjectStateManager.SomeEntryWithConceptualNullExists())
{
thrownew 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。模擬測試代碼
![](http://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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
{
staticvoid 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用於模擬代表其它資料庫
![](http://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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
{
staticvoid 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介面如下
![](http://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace EF.Model
{
publicinterface IUnitOfWork
{
//事務提交
int Save();
}
}
我們再修改DemoDb.DbContext.tt模板
![](http://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<#@ 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)
{
returnstring.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)#>partialclass<#=Code.Escape(container)#> : DbContext,IUnitOfWork
{
public<#=Code.Escape(container)#>()
: base("name=<#=container.Name#>")
{
<#
WriteLazyLoadingEnabled(container);
#>
}
protectedoverridevoid OnModelCreating(DbModelBuilder modelBuilder)
{
thrownew UnintentionalCodeFirstException();
}
//---------------------------------------------------這裡加入對IUnitOfWork介面方法的實現---------------add by mecity
publicint Save()
{
returnbase.SaveChanges();
}
註意T4模板中加了註釋的地方,保存模板後,就會重新創建DemoDBEntities,看一下模板修改後生成後的代碼
![](http://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
//------------------------------------------------------------------------------
// <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
publicpartialclass DemoDBEntities : DbContext,IUnitOfWork
{
public DemoDBEntities()
: base("name=DemoDBEntities")
{
}
protectedoverridevoid OnModelCreating(DbModelBuilder modelBuilder)
{
thrownew UnintentionalCodeFirstException();
}
//---------------------------------------------------這裡加入對IUnitOfWork介面方法的實現---------------add by mecity
publicint Save()
{
returnbase.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 傳輸對象類庫
當然這隻是參考,怎麼合理分層還是依項目需求,項目進度,資源情況以及後期維護等情況而定。
參考頁面:
http://www.yuanjiaocheng.net/entity/database-first.html
http://www.yuanjiaocheng.net/entity/choose-development-approach.html
http://www.yuanjiaocheng.net/entity/query-with-edm.html
http://www.yuanjiaocheng.net/entity/linq-to-entities-projection.html
http://www.yuanjiaocheng.net/entity/dbset-class.html