看到一些工作單元的介紹,有兩種感覺,第一種是很學院,說了等於沒說,我估計很多都是沒有自己引入到實際的項目中去,第二種是告訴我一種結果,說這就是工作單元,但是沒說為什么要這么使用。所以,本篇想要探討的是:為什么工作單元要這么用。首先,要想將工作單元引入到自己的項目中去,急需要解決的一個問題是:工作單元的生命周期,即:
1:工作單元被誰創建,何時消亡?
2:工作單元被誰們持有?
3:工作單元的 4 個 API( MakeNew、MakeDirty、MakeRemoved、Commit )被誰調用?
一:工作單元(Unit Of Work)
在《架構模式數據源模式之:數據映射器(Data Mapper)中的代碼中,最大的問題的是,如果對象有變化,我們直接進行數據庫更新,這樣以來,會產生很多的數據庫操作。工作單元模式就是避免這種情況發生的。它記錄對象的變化,然后在需要操作數據庫的時候,才去 Commit。
一般而言,有三種方式可以實現讓工作單元感知對象發生變化了:
1:調用者注冊(call registration),即:用戶改變了某個對象就必須把它注冊到工作單元;
2:對象注冊(object registration),即:領域對象或者服務腳本改變了對象,就將本身或者服務中的對象注冊到工作單元;
3:工作單元控制器(unit of work controller),即:工作單元在讀操作時候產生對象拷貝,在更新時候比較拷貝。
在這里我們使用的是 對象注冊 這種方式。接下來,我們就要考慮:
怎么樣讓 對象注冊 變得簡單,即:將本身或者服務中的對象注冊到工作單元變得簡單。
但是,在考慮這個問題之前,我們顯然還漏掉了一個細節,細究此話“對象注冊到工作單元”,此話意味着首先已經存在了 工作單元 了,那么,工作單元 這個對象本身又是什么時候產生的,又存儲在哪里呢?
二:工作單元的創建
好的,我們還要弄明白的一個概念就是:
工作單元的生命周期?即:工作單元對應應用程序來說,是全局的,還是屬於“會話”的,還是屬於領域對象的?所謂屬於,指與后者同消亡。
現在,我們來看看工作單元的實質,這在本文一開頭的時候描述過:它記錄對象的變化,然后在需要操作數據庫的時候,才去 Commit。
那么,誰會需要記錄對象的變化呢?顯而易見,是事務本身(事務太抽象了,那就理解為執行的那段業務代碼吧)。業務代碼屬於誰,屬於領域對象(或者貧血模式下,屬於領域服務,這里,我們只說充血),但工作單元的生命周期往往要比具體的某個領域對象寬泛,它絕不僅僅隨着某個領域對象產生並消亡。它應該是屬於當前 事務 的(一個事務可能會牽扯很多的領域對象)。在這個毫無疑問的基礎下,我們可以假定其生命周期屬於:
1:事務本身
事務,即業務代碼,業務代碼寫在方法內部,所以這里的工作單元的生命周期即方法內部。
事務本身創建和擁有工作單元,還有一種變體。事務為誰所擁有?為客戶端端代碼,如 UI 層,如 MVC 中的控制器等,MSDN 官方有兩篇博文各自提到這種方式,可參考:
把工作單元交給事務(即:顯式創建工作單元),優點是明確且靈活,缺點是系統當中就會到處存在工作單元對象的創建與消亡;
2:當前工作線程
將生命周期擴大化,Martin Fowler 在 《企業應用架構模式》 中的示例代碼中提到:工作單元屬於當前工作線程(使用 ThreadLocal 實現)。優點是業務代碼自身不用創建工作單元。缺點是什么呢?如網站系統,多個客戶端用戶往往會共用一個工作線程,你和我之間的那些變化,如果你不及時 Commit,就可能會被我 Commit。要避免這點,可以使用鎖定,但鎖定可不利於高並發情景。當然,如果我們是在開發一個桌面應用程序,則不會如此。
3:全局
如果第二點可以被接受,那么,實際上工作單元還可以是全局的,即同一個應用程序域的,即意味着,你 Commit 了,就會把系統中所有會話(如果有會話的話)未及時 Commit 的會話都 Commit 了。
4:“會話”
Martin 在闡述 “屬於當前工作線程” 的時候,談到:工作單元邏輯上屬於會話對象。會話在從技術上表達來說,有不同的含義。比如,在網站系統中,通常我們指那個 Session 對象。但由於 ASP.NET 引擎默認 Session對象 在高並發時候的易失性,通常我們不會將 工作單元 放在它那里。於是,我們可以維護自己的 會話對象。
這在邏輯上是優選。這種方式的優點是:最大化隔離工作單元,“你”和“我”之間不會相互干擾,並且如同 2 和 3 一樣, 工作單元不需要傳遞,你只要獲取就行。缺點是:維護自己的會話對象,很麻煩哦,如果放在內存中的話,在高並發的時候,跟 ASP.NET 的 SESSION 一樣,莫名丟失了就會產生莫名的 BUG。
5:包含事務代碼的領域模型
還有一種做法。再次看上文“它應該是屬於當前 事務 的(一個事務可能會牽扯很多的領域對象)”。在領域模型設計中,所有的事務,即所有的行為,都是屬於領域模型的,這句話就意味着:工作單元還可以屬於領域模型本身,領域模型中領域根在構造器中負責創造工作單元並傳遞給非根們。
在《.NET Domain-Driven Design with C#: Problem - Design - Solution》(該書有個坑爹的中文名:《領域驅動設計C# 2008實現》)這本書中就是這樣實現的,它使用的是貧血模式,故對應在模型所在的 Service 中可以看到創建了 工作單元(同時工作單元又被傳遞到 Repository 中,題外話,該書有個最大的問題是,它將 SQL 查詢拼接的到處都是)。
這最后一種方式的優點是:編碼很簡單,缺點是和 1:事務腳本 是一樣的,系統中到處存在工作單元的創建和傳遞,即:
領域根負責將自己的 工作單元 傳遞給非根們;
該做如何選擇?
如果你有一個高效且穩定的 會話對象,你選擇 4 來創建和使用 工作單元;
如果沒有足夠的信心自己寫一個高效並穩定的 會話對象,那么,我們應該選擇 1 或者 5。畢竟頻繁的創建和消亡 工作單元,也沒什么大不了,總比出錯好,MS的官方博客都這樣推薦哦,雖然沒告訴你干嘛我們要這么用;
如果我們在寫桌面應用,則可以選擇最簡單的 2 和 3;
三:工作單元的持有
在上面一節中,我們已經看到了 工作單元 的創建,從中也窺探到了誰持有 工作單元。最后一個問題是:倉儲庫有無必要持有 工作單元。這其實依賴於 MakeNew MakeDirty MakeRemove 這些功能由誰負責。如果我們由領域模型負責干這件事情,那么工作單元就沒有必要傳遞給倉儲庫。我個人傾向於由領域模型干這件事情,如果領域模型的某個屬性被 Modify 之后,自然而然需要 MakeDirty,當然,這里的壞處是,你得為每個屬性的 SET 方法 MakeDirty。另外一種做法是,不公開屬性的 set 方法,使用方法接受模型的屬性值的變更,然后在方法中 MakeDirty。我個人傾向於后者,往往領域模型屬性有很多,但是修改屬性的做法不會對所有屬性進行修改,這大大節約了我的代碼行數,並且用方法來接受屬性的變更也可以從方法命名上來看到更明確的業務功能。舉例來說: aUser.ReName(“zhangsan”) 比 aUser.Name=”zhangsan” 語義要明確多了。
擴展閱讀:
REP 及 context 的注入,在Control中;http://www.asp.net/mvc/tutorials/getting-started-with-ef-5-using-mvc-4/implementing-the-repository-and-unit-of-work-patterns-in-an-asp-net-mvc-application
這里面最重要的一句話是:The unit of work class serves one purpose: to make sure that when you use multiple repositories, they share a single database context.
三:簡版會話對象
由於領域根可能是易變的,並且最重要,領域層本身沒有標識哪些模型是根,哪些不是,所以最終的結果是:工作單元與領域模型(領域根)是一起。即:
public abstract class DomainObj
{
public string Id {get; set;}public string Name {get; set;}
protected UnitOfWork uow = new UnitOfWork();
protected void MakeNew()
{
uow.RegisterNew(this);
}
protected void MakeDirty()
{
uow.RegisterDirty(this);
}
protected void MakeRemoved()
{
uow.RegisterRemoved(this);
}
}
完整代碼示例
使用對象注冊的方式的示例代碼如下:
void Main()
{
SqlHelper.ConnectionString = new MyConnections().Conn;
var user1 = User.FindUser("6f7ff44435f3412cada61898bcf0df6c");
var user2 = User.FindUser("6f7ff44435f3412cada61898bcf0df6c");
(user1 == user2).Dump();
SomeRootDomain root = new SomeRootDomain();
root.DoSomething();
"END".Dump();
}public class SomeRootDomain : DomainObj
{
public void DoSomething()
{
// 創建一個用戶並改名,然后查看是否 newObjects 有它,dirtyObjects 無它,removedObjects 無它
User luminji = User.CreateNew("luminjiAdded");
luminji.Id.Dump();
luminji.Dump();
luminji.ReName("luminjiAddedModified");
luminji.Dump();
uow.CheckObj(luminji);
// 創建一個用戶並刪除,然后查看是否在 UT 的 newObjects,dirtyObjects,removedObjects 都沒有它
User luminji2 = User.CreateNew("luminjiAdded2");
luminji2.Id.Dump();
luminji2.Dump();
uow.CheckObj(luminji2);
luminji2.Delete();
luminji2.Dump();
uow.CheckObj(luminji2);
// 創建一個用戶並改名,繼而刪除,然后查看是否在 UT 的 newObjects,dirtyObjects,removedObjects 都沒有它
User luminji3 = User.CreateNew("luminjiAdded3");
luminji3.Id.Dump();
luminji3.Dump();
luminji3.ReName("luminjiAdded3Renamed");
uow.CheckObj(luminji3);
luminji3.Delete();
luminji3.Dump();
uow.CheckObj(luminji3);
// 獲取一個用戶,然后查看是否在 UT 的 newObjects,dirtyObjects,removedObjects 都沒有它
User luminji4 = User.FindUser("6f7ff44435f3412cada61898bcf0df6c");
uow.CheckObj(luminji4);
// 獲取一個用戶並改名,然后查看是否在 UT 的 newObjects 無它,dirtyObjects 有它,removedObjects 無它
User luminji5 = User.FindUser("6f7ff44435f3412cada61898bcf0df6c");
uow.CheckObj(luminji5);
luminji5.Dump();
luminji5.ReName("luminjiAdded3Renamed");
uow.CheckObj(luminji5);
// 獲取一個用戶並並刪除,然后查看是否在 UT 的 newObjects 無它,dirtyObjects 無它,removedObjects 有它
User luminji6 = User.FindUser("6f7ff44435f3412cada61898bcf0df6c");
uow.CheckObj(luminji6);
luminji6.Delete();
uow.CheckObj(luminji6);
uow.Commit();
// 綜合例子
// 1: 創建用戶
// 2: 獲取用戶修改用戶
// 3:刪除用戶
User luminjiTestAdd = User.CreateNew("luminjiTestAdd");
User luminji7 = User.FindUser("6f7ff44435f3412cada61898bcf0df6c");
luminji7.ReName("luminji unit");
User gaoyuan = User.FindUser("91f610bf01e540dbade4825d6f05f1ee");
gaoyuan.Delete();
uow.CheckAll();
uow.Commit();
}
}
public abstract class DomainObj
{
public string Id {get; set;}public string Name {get; set;}
protected UnitOfWork uow = new UnitOfWork();
protected void MakeNew()
{
uow.RegisterNew(this);
}
protected void MakeDirty()
{
uow.RegisterDirty(this);
}
protected void MakeRemoved()
{
uow.RegisterRemoved(this);
}
}public class Organization : DomainObj
{
public static Organization Get(string id)
{
var org = OrganizationMap.GetInstance()
.Find(id);
return org;
}
}
public class User : DomainObj
{
public string OrganizitionId;
public static User CreateNew(string name)
{
User user = new User(){ Id = Guid.NewGuid().ToString(), Name = name};
user.MakeNew();
return user;
}
private Organization organization;
public Organization Organization
{
set
{
organization = value;
}
get
{
if(this.organization == null)
{
this.organization = Organization.Get(this.OrganizitionId);
}
return this.organization;
}
}
static UserMap map = UserMap.GetInstance();
private List<Course> courses;
public List<Course> Courses
{
get
{
if(this.courses == null)
{
this.courses = Course.GetListByUserId(this.Id);
}
return this.courses;
}
}
public static User FindUser(string id)
{
var user = map.Find(id);
return user;
}
public void Delete()
{
this.MakeRemoved();
}
public void ReName(string name)
{
this.Name = name;
this.MakeDirty();
}}
public class Course : DomainObj
{
public int Duration;
static CourseMap map = CourseMap.GetInstance();
public static List<Course> GetListByUserId(string userId)
{
var courses = map.GetListByUserId(userId);
return courses;
}
}public class OrganizationMap : AbstractMapper<Organization>
{
private OrganizationMap(){}
private static OrganizationMap map;
public static OrganizationMap GetInstance()
{
if(map == null)
{
map = new OrganizationMap();
}
return map;
}
public Organization Find(string id)
{
return (Organization)AbstractFind(id);
}
public override Organization AbstractFind(string id)
{
var organization = base.AbstractFind(id);
Load(organization);
return organization;
}
public override void Update(Organization organization)
{
"UPDATE Organization SET ....".Dump();
}
public override void Insert(Organization organization)
{
"INSERT INTO Organization ....".Dump();
}
public override void Delete(Organization organization)
{
"DELETE Organization ....".Dump();
}
public override void Update(DomainObj organization)
{
Update(organization as Organization);
}
public override void Insert(DomainObj organization)
{
Insert(organization as Organization);
}
public override void Delete(DomainObj organization)
{
Delete(organization as Organization);
}
}public class UserMap : AbstractMapper<User>
{
private UserMap(){}
private static UserMap map;
public static UserMap GetInstance()
{
if(map == null)
{
map = new UserMap();
}
return map;
}
public User Find(string id)
{
return (User)AbstractFind(id);
}
public override User AbstractFind(string id)
{
var user = base.AbstractFind(id);
if( user == null )
{
//
string sql = @"
DECLARE @ORGID VARCHAR(32)='';
SELECT @ORGID=OrganizationId FROM [EL_Organization].[USER] WHERE ID=@Id
SELECT * FROM [EL_Organization].[USER] WHERE ID=@Id
SELECT * FROM [EL_Organization].[ORGANIZATION] WHERE ID=@ORGID";
var pms = new SqlParameter[]
{
new SqlParameter("@Id", id)
};
var ds = SqlHelper.ExecuteDataset(CommandType.Text, sql, pms);
user = DataTableHelper.ToList<User>(ds.Tables[0]).FirstOrDefault();
user.Organization = DataTableHelper.ToList<Organization>(ds.Tables[1]).FirstOrDefault();
if(user == null)
{
return null;
}user = Load(user);
// 注意,除了 Load User 還需要 Load Organization
user.Organization = Load(user.Organization) as Organization;
return user;
}
return user;
}
public List<User> FindList(string name)
{
// SELECT * FROM USER WHERE NAME LIKE NAME
List<User> users = null;
return LoadAll(users);
}
public override void Update(User user)
{
"UPDATE USER SET ....".Dump();
}
public override void Insert(User user)
{
"INSERT INTO USER ....".Dump();
}
public override void Delete(User user)
{
"DELETE USER ....".Dump();
}
public override void Update(DomainObj user)
{
Update(user as User);
}
public override void Insert(DomainObj user)
{
Insert(user as User);
}
public override void Delete(DomainObj user)
{
Delete(user as User);
}
}
public class CourseMap : AbstractMapper<Course>
{
private CourseMap(){}
private static CourseMap map;
public static CourseMap GetInstance()
{
if(map == null)
{
map = new CourseMap();
}
return map;
}
public Course Find(string id)
{
return (Course)AbstractFind(id);
}
public override Course AbstractFind(string id)
{
var Course = base.AbstractFind(id);
Load(Course);
return Course;
}
public override void Update(Course course)
{
// UPDATE USER SET ....
}
public override void Insert(Course course)
{
// INSERT INTO USER ....
}
public override void Delete(Course course)
{
// DELETE USER ....
}
public override void Update(DomainObj course)
{
Update(course as Course);
}
public override void Insert(DomainObj course)
{
Insert(course as Course);
}
public override void Delete(DomainObj course)
{
Delete(course as Course);
}
public List<Course> GetListByUserId(string userId)
{
List<Course> courses = null;
// SELECT * FROM ...
LoadAll(courses);
return courses;
}
}
public abstract class AbstractMapper
{
protected static Dictionary<string, DomainObj> loadedMap = new Dictionary<string, DomainObj>();public abstract void Insert(DomainObj t);
public abstract void Update(DomainObj t);
public abstract void Delete(DomainObj t);
}
public abstract class AbstractMapper<T> : AbstractMapper where T : DomainObj
{
protected AbstractMapper(){}
public void CheckLoaedDomains()
{
foreach(var m in loadedMap)
{
m.Value.Dump();
}
}
public DomainObj Load(DomainObj t)
{
if(loadedMap.ContainsKey(t.Id) )
{
return loadedMap[t.Id];
}
else
{
loadedMap.Add(t.Id, t);
return t;
}
}
protected T Load(T t)
{
if(loadedMap.ContainsKey(t.Id) )
{
return loadedMap[t.Id] as T;
}
else
{
loadedMap.Add(t.Id, t);
return t;
}
}
protected List<T> LoadAll(List<T> ts)
{
for(int i=0; i < ts.Count; i++)
{
ts[i] = Load(ts[i]);
}
return ts;
}
public virtual T AbstractFind(string id)
{
if(loadedMap.ContainsKey(id))
{
return loadedMap[id] as T;
}
else
{
return null;
}
}
public virtual void Insert(T t){}
public virtual void Update(T t){}
public virtual void Delete(T t){}
}
public class UnitOfWork
{
private List<DomainObj> newObjects = new List<DomainObj>();
private List<DomainObj> dirtyObjects = new List<DomainObj>();
private List<DomainObj> removedObjects = new List<DomainObj>();
public void Clear()
{
newObjects.Clear();
dirtyObjects.Clear();
removedObjects.Clear();
}
public void CheckAll()
{
foreach(var m in newObjects)
{
("newObjects:" + m.Name).Dump();
}
foreach(var m in dirtyObjects)
{
("dirtyObjects:" + m.Name).Dump();
}
foreach(var m in removedObjects)
{
("removedObjects:" + m.Name).Dump();
}
}
public void CheckObj(DomainObj obj)
{
("newObjects中有:" + newObjects.Count()).Dump();
("dirtyObjects中有:" + dirtyObjects.Count()).Dump();
("removedObjects中有:" + removedObjects.Count()).Dump();
if(newObjects.Contains(obj))
{
("newObjects中有" + obj.Id).Dump();
}
else
{
("newObjects中沒有" + obj.Id).Dump();
}
if(dirtyObjects.Contains(obj))
{
("dirtyObjects中有" + obj.Id).Dump();
}
else
{
("dirtyObjects中沒有" + obj.Id).Dump();
}
if(removedObjects.Contains(obj))
{
("removedObjects中有" + obj.Id).Dump();
}
else
{
("removedObjects中沒有" + obj.Id).Dump();
}
}
public void RegisterNew(DomainObj obj)
{
if(string.IsNullOrEmpty(obj.Id))
{
throw new Exception("Id不能為空");
}
if(dirtyObjects.Contains(obj))
{
throw new Exception("新對象不能為一個臟對象");
}
if(removedObjects.Contains(obj))
{
throw new Exception("新對象不能為一個要刪除的對象");
}
if(newObjects.Contains(obj))
{
throw new Exception("新對象已經注冊過了");
}
newObjects.Add(obj);
"newObjects添加成功".Dump();
}
public void RegisterDirty(DomainObj obj)
{
if(string.IsNullOrEmpty(obj.Id))
{
throw new Exception("Id不能為空");
}
if(removedObjects.Contains(obj))
{
throw new Exception("臟對象已被列入到刪除對象中");
}
if(!newObjects.Contains(obj) && !dirtyObjects.Contains(obj))
{
dirtyObjects.Add(obj);
"dirtyObjects添加成功".Dump();
}
}
public void RegisterRemoved(DomainObj obj)
{
if(string.IsNullOrEmpty(obj.Id))
{
throw new Exception("Id不能為空");
}
if(newObjects.Remove(obj))
{
return;
}
if(!removedObjects.Contains(obj))
{
removedObjects.Add(obj);
"removedObjects添加成功".Dump();
}
}
public void Commit()
{
InsertNew();
UpdateDirty();
DeleteRemoved();
}
private void InsertNew()
{
foreach(var obj in newObjects)
{
MapperRegistry.GetMapper(obj).Insert(obj);
}
}
private void UpdateDirty()
{
foreach(var obj in dirtyObjects)
{
MapperRegistry.GetMapper(obj).Update(obj);
}
}
private void DeleteRemoved()
{
foreach(var obj in removedObjects)
{
MapperRegistry.GetMapper(obj).Delete(obj);
}
}
}
public class MapperRegistry
{
private List<AbstractMapper> mappers = new List<AbstractMapper>();
public static AbstractMapper GetMapper<T>(T t) where T : DomainObj
{
if(t is User)
{
// if(!mappers.Contains(UserMap.GetInstance()))
// {
// mappers.Add(UserMap.GetInstance());
// }
return UserMap.GetInstance();
}
throw new Exception("t type is valid");
}
}
注意:領域模型自身應該負責通知 UnitOfWork 變化了;
四:標識映射(Identity Map)
在這段代碼中,我們看到了標識映射的概念。
標識映射是指:記錄從數據庫中讀出的所有對象,當要用到一個對象的時候,先檢查標識映射,看需要的對象是否已經存在其中。基於這種特點,標識映射成為了數據庫讀取的高速緩存。並且,我們把它放在了 數據映射器 中,且標識為靜態的,即:
public abstract class AbstractMapper
{
protected Dictionary<string, DomainObj> loadedMap = new Dictionary<string, DomainObj>();public abstract void Insert(DomainObj t);
public abstract void Update(DomainObj t);
public abstract void Delete(DomainObj t);
}
其它擴展閱讀:
http://www.joel.net/repository-and-unit-of-work-for-entity-framework-ef4-for-unit-testing
The job of the Unit of Work class is to manage the lifecycle and context of your Repositories. The class is pretty simple to create, implement IDisposable, add all your Repositories and give it a Save method.