天天看點

領域驅動設計(DDD)的實踐經驗分享之ORM的思考

大家都知道,一般我們在做ddd架構的應用時,一般會分成四層:

presentation layer:展現層,負責顯示和接受輸入;

application layer:應用層,很薄的一層,隻包含工作流控制邏輯,不包含業務邏輯;

domain layer:領域層,包含整個應用的所有業務邏輯;

infrastructure layer:基礎層,提供整個應用的基礎服務;

 一開始接觸這樣的架構時,覺得确實很好。但後來在不斷實踐中遇到了不少問題,下面幾個就是我所遇到的幾個關鍵問題,在接下來的随筆中我将會一一和大家分享我是如何思考和解決這些問題的。

是否應該使用orm;

持久化透明;

高性能;

事務支援;

是否應該使用orm?

大家都知道orm能将對象和資料庫進行映射,它可以讓開發人員完全按照面向對象的思維去設計實作軟體,并且你無需關心對象如何從資料庫取出來或儲存到資料庫。但是我發現幾乎我所見過的所有orm架構都基于同一個前提,那就是将對象的屬性映射到資料庫字段,将對象之間的引用映射到資料庫表的關系。然後如果當你需要一些進階功能支援時,orm會要求你對你的對象做一些設定,比如在nhibernate中,你如果要進行lazy load,你就必須将屬性設定為virtual,如果是一對多,好像還有個叫iset的東東吧,我不是太了解。還有,像linq to sql,如果要支援lazy load,需要使用entityset或entityref,這兩個類型的侵入性太強了,搞的你的模型不像模型。對于微軟最新的orm架構ado.net entity framework,我還沒有真正用過,是以不怎麼知道它的明顯缺點,但據說orm的映射能力目前還不如nhibernate。撇開orm的進階特性不說,我們隻談基本特性我想我就能問倒你。是誰告訴你orm的映射就是對象的屬性和資料庫表字段的映射?面向對象博大精深,繼承,多态,重載,重寫,等等。很多對象可能隻有方法而沒有屬性,不要問我為什麼。至少隻要用屬性可以實作的地方一般用方法也能做到,使用方法合适還是屬性合适應該根據你目前的具體情況而定。但你跟我說,如果你要用orm,那你就必須用屬性,否則不能做到orm的自動映射!那我豈不是很郁悶。另外,對于領域驅動設計來說,領域對象都是很豐富的,是充血的。對象不僅有很多的屬性也會有很多的方法。可以說是“活”的東西,是能完成很多職責以及和别的對象進行交流的東西。但資料庫中的資料是“死”的,是靜态的。資料隻有狀态而沒有行為。我很難想象為什麼大家一定要将這種“活”的有生命的對象映射到一個“死”的資料庫記錄上去。在我看來,對象和資料是兩個不同的概念,它們不應該被直接進行映射。

但是問題是,我們無法規避兩個問題:1)将對象的狀态儲存到資料庫;2)從資料庫擷取對象的狀态并重新建構出一個對象;這是任何一個基于關系型資料庫的面向對象應用程式無法回避的兩個問題。不知道大家注意到了沒有,我特地将對象的狀态進行了加粗。目的就是想強調,真正應該需要映射的是:對象和狀态和資料庫之間的映射,而不是對象和資料庫之間的映射。弄清楚了這個概念後,我就可以談一下我對orm映射的具體做法的觀點。

在我看來對象由狀态和行為組成,所謂的狀态可以這樣來形容:什麼東西的什麼是什麼。比如人是一個對象,一個人的身高是什麼,體重是什麼;在比如一個訂單,它的建立日期是什麼,它有那些訂單明細,他的訂貨人是誰,這些都是對象的狀态。可以看出對象的狀态在某個特定的時刻一定是靜态的,沒有任何動态的成分,可以用某種标準進行衡量。是以,我覺得我們應該将對象的狀态和資料庫中的表進行映射。那如何從對象的狀态進一步建立出一個對象呢?這個應該很簡單吧,雖然簡單,但是目前來說要做這樣的事情,隻能人工去做,因為目前世界上還沒有所謂的工具能将對象的狀态映射到對象的。

如果用c#語言,該怎樣表示對象的狀态以及對象呢?這個問題對大家來說實在是在簡單了,對吧!你肯定會說,對象的狀态就是一些簡單的類,這些類隻有簡單的屬性,或者是其他一個簡單的類,并且也不需要實作什麼接口或繼承什麼基類。每個這種簡單的類型和一個資料庫表對應。而對象就是面向對象分析與設計中的一個概念。雖然它也是用類來表示,但它的實作并不是隻有簡單的屬性。而是可能會包含很多豐富的元素,比如它的構造函數也可能很複雜,會接受很多參數,它可能會包含事件、方法、屬性、内部類,也可能會實作多個接口,等等。總之按照我的話來說,它就是一個“活”的東西。

現在大家了解了什麼是對象的狀态,什麼是對象了。那麼該怎樣根據對象的狀态來建立一個對象呢?剛才說了,你自己能做到,工具做不到。這裡涉及到兩個問題:

如何根據多個狀态類建立出一個對象,或者根據一個狀态類建立出多個對象;

如何将一個對象的狀态拆分為多個狀态類,或者将多個對象的狀态儲存到一個狀态類中;

了解了這兩點後,我們就能很容易的按照這個思路來建立或持久化對象了。前面說了一大段一大段的廢話,該是舉個例子的時候了。

假設我現在有一個論壇,論壇有版塊分組和版塊兩個概念。就像csdn這個論壇一樣,.net就是一個版塊分組,而asp.net或c#等就是版塊。其中,一個版塊分組可以包含多個版塊,一個版塊隻能屬于一個版塊分組;也就是一對多的關系。

資料庫表的設計:

版塊分組表:tb_groups,包含這些字段:id, subject, enabled

版塊表:tb_sections,包含這些字段:id, subject, enabled, groupid

這些字段應該不用解釋了吧,很容易了解。其中groupid是一個外鍵,指向tb_groups表中的id.

關于對象的設計,以版塊為例,可能會設計成下面這樣:

領域驅動設計(DDD)的實踐經驗分享之ORM的思考

  1     public class section : aggregateroot<guid>

  2     {

  3         #region private variables

  4 

  5         private group group;

  6         private int? totalthreadcount;

  7         private list<user> adminuserlist;

  8 

  9         #endregion

 10 

 11         #region constructors

 12 

 13         public section(guid id, group group) : base(id)

 14         {

 15             this.group = group;

 16         }

 17 

 18         #endregion

 19 

 20         #region public properties

 21 

 22         [trackingproperty]

 23         public string subject { get; set; }

 24         [trackingproperty]

 25         public bool enabled { get; set; }

 26         public group group

 27         {

 28             get

 29             {

 30                 return group;

 31             }

 32             set

 33             {

 34                 if (group != value && value != null)

 35                 {

 36                     group = value;

 37                     raiseevent(new sectiongroupchangedevent { id = id, group = group });

 38                 }

 39             }

 40         }

 41         public int totalthreadcount

 42         {

 43             get

 44             {

 45                 if (totalthreadcount == null)

 46                 {

 47                     raiseevent(new sectiontotalthreadcountqueryevent

 48                     {

 49                         id = id,

 50                         settotalthreadcount = new action<int>(count => totalthreadcount = count)

 51                     });

 52                 }

 53                 return totalthreadcount.value;

 54             }

 55         }

 56         public readonlycollection<user> adminusers

 57         {

 58             get

 59             {

 60                 return adminuserlist.asreadonly();

 61             }

 62         }

 63 

 64         #endregion

 65 

 66         #region public methods

 67 

 68         public void addadminuser(user user)

 69         {

 70             if (!adminuserlist.contains(user))

 71             {

 72                 adminuserlist.add(user);

 73                 raiseevent(new sectionadminuseraddedevent { id = id, user = user });

 74             }

 75         }

 76         public void removeadminuser(user user)

 77         {

 78             if (adminuserlist.contains(user))

 79             {

 80                 adminuserlist.remove(user);

 81                 raiseevent(new sectionadminuserremovedevent { id = id, user = user });

 82             }

 83         }

 84 

 85         #endregion

 86 

 87         #region private properties

 88 

 89         private list<user> adminuserlist

 90         {

 91             get

 92             {

 93                 if (adminuserlist == null)

 94                 {

 95                     raiseevent(

 96                         new sectionadminusersqueryevent

 97                         {

 98                             id = id,

 99                             setusers = new action<ienumerable<user>>(users => adminuserlist = users.tolist())

100                         });

101                 }

102                 return adminuserlist;

103             }

104         }

105 

106         #endregion

107     }

領域驅動設計(DDD)的實踐經驗分享之ORM的思考

先不說這個對象設計的好壞,但至少在我看來,他至少已經不是一個簡單的狀态類型了。比如它的構造函數接收一個其他的對象(group對象),它還有一些方法,它的某些屬性的值被修改後會觸發事件,它有良好的封裝性,即不能對外界公開的就聲明為私有,可以被外界通路但不能被修改的就設計成隻讀。還有因為一個版塊必須要有一個所屬的版塊組,如果不提供版塊組就不允許被建立。是以告訴我們必須在構造函數中傳遞給它一個group對象。等等這些都說明它是一個既有狀态也有行為的對象。

接下來我們再來看看對象狀态類型是怎麼樣的?

領域驅動設計(DDD)的實踐經驗分享之ORM的思考

 1     public class groupobject

 2     {

 3         public guid id { get; set; }

 4         public string subject { get; set; }

 5         public bool enabled { get; set; }

 6     }

 7     public class sectionobject

 8     {

 9         public guid id { get; set; }

10         public string subject { get; set; }

11         public bool enabled { get; set; }

12         public guid groupid { get; set; }

13     }

14     public class sectionandgroupobject

15     {

16         public sectionobject sectionobject { get; set; }

17         public groupobject groupobject { get; set; }

18     }

領域驅動設計(DDD)的實踐經驗分享之ORM的思考

上面的代碼中,groupobject代表group對象的狀态,sectionobject代表section對象的狀态,而sectionandgroupobject則是一個組合狀态對象。但是大家不要誤會,它也隻是一個狀态類型。之是以我搞出一個組合狀态類型,是為了可以避免重複寫屬性而已,沒有其他特别目的。

好了,現在對象有了,表示對象的狀态的類型有了,資料庫表也有了。接下來談談該如何來有效的組織這三者,讓對象的狀态能進行儲存和恢複。

前面大家看了我的觀點後,可能會覺得,orm我們不應該用,其實不是這樣,雖然不能直接用它來将對象和資料庫之間進行映射,但卻可以用它來将對象狀态和資料庫進行映射。比如上面的例子中,groupobject和sectionobject是兩個狀态對象,他們都是扁平的,僅僅儲存了對象的全部或部分的狀态,也就是說狀态對象儲存的都是狀态,都是資料,是靜态的。它的這個特性正好和資料庫表完全一緻。是以,我覺得我們可以将狀态對象和資料庫表通過任意一種成熟的orm工具來建立映射。當然,如果你想人工去建立狀态對象和資料庫表之間的映射也沒有問題,就是寫起來比較麻煩,很多繁瑣的工作都要你自己去完成。比如你用ado.net去寫,那麼你必須自己去建立connection,command,commanparameter,等等繁瑣累人的本不應該由你去做的任務,或者如果你連ado.net都不用,直接自己寫sql,那就更麻煩了。最後,如果你用orm映射來實作,那麼由于狀态對象設計的時候往往已經兼顧了表的結構以及如何比較好的儲存對象狀态的兩方面的問題,是以當我們在進行映射時,往往會非常簡單直接,基本上狀态對象的每個屬性都是和資料庫表一一對應的。以linq to sql為例:

領域驅動設計(DDD)的實踐經驗分享之ORM的思考

 1     <table name="tb_groups">

 2         <type name="companyname.productname.modules.forum.linqtosqldataprovider.groupobject">

 3             <column name="id" member="id" dbtype="uniqueidentifier not null" isprimarykey="true" />

 4             <column name="subject" member="subject" dbtype="nvarchar(128) not null" canbenull="false" />

 5             <column name="enabled" member="enabled" dbtype="bit not null" />

 6         </type>

 7     </table>

 8     <table name="tb_sections">

 9         <type name="companyname.productname.modules.forum.linqtosqldataprovider.sectionobject">

10             <column name="id" member="id" dbtype="uniqueidentifier not null" isprimarykey="true" />

11             <column name="subject" member="subject" dbtype="nvarchar(128) not null" canbenull="false" />

12             <column name="enabled" member="enabled" dbtype="bit not null" />

13             <column name="groupid" member="groupid" dbtype="uniqueidentifier not null" />

14         </type>

15     </table>

領域驅動設計(DDD)的實踐經驗分享之ORM的思考

大家可以看到上面的映射非常清晰明了,可以說,隻要是個orm架構,就一定能完成這樣簡單的映射。

好了,現在一切就緒,我們來看看如何來實作對象的狀态的儲存(也就是對象持久化)以及如何從資料庫中儲存的狀态重建一個對象。

對象持久化:

領域驅動設計(DDD)的實踐經驗分享之ORM的思考

 1         public void persistanewsection(section section)

 2         {

 3             //首先,将section的部分狀态儲存到sectionobject中

 4             sectionobject sectionobject = new sectionobject

 5             {

 6                 id = section.id,

 7                 subject = section.subject,

 8                 enabled = section.enabled,

 9                 groupid = section.group.id

10             };

11 

12             //然後,利用linq to sql将狀态對象(sectionobject)持久化到資料庫

13             datacontext.gettable<sectionobject>().insertonsubmit(sectionobject);

14             datacontext.submitchanges();

15         }

領域驅動設計(DDD)的實踐經驗分享之ORM的思考

上面的這個函數可以将一個section對象的部分狀态持久化到資料庫中。之是以說是部分,是因為section對象還包含了一些其他的狀态,不如版主資訊等。大家可以看到,我們先section對象的狀态儲存到一個sectionobject的狀态對象中,然後在利用orm架構(linq to sql)來完成持久化。

從資料庫中儲存的狀态重建一個對象:

領域驅動設計(DDD)的實踐經驗分享之ORM的思考

 1         public section getsectionfromdatabase(guid sectionid)

 3             //首先,利用linq to sql這個牛逼的orm工具來定義一個linq查詢,它相當于是一個sql語句,

 4             //隻不過我們用的不是表的字段,而是狀态對象的屬性而已。

 5             var query = from s in datacontext.gettable<sectionobject>()

 6                         join g in datacontext.gettable<groupobject>()

 7                         on s.groupid equals g.id

 8                         where s.id == sectionid

 9                         select new sectionandgroupobject { sectionobject = s, groupobject = g };

10 

11             //執行查詢

12             sectionandgroupobject sectionandgroupobject = query.firstordefault();

13 

14             //先根據groupobject狀态對象建立一個group對象

15             group group = new group(sectionandgroupobject.groupobject.id)

16             {

17                 subject = sectionandgroupobject.groupobject.subject,

18                 enabled = sectionandgroupobject.groupobject.enabled

19             };

20             //然後根據sectionobject狀态對象和group對象建立一個section對象

21             section section = new section(sectionandgroupobject.sectionobject.id, group)

22             {

23                 subject = sectionandgroupobject.sectionobject.subject,

24                 enabled = sectionandgroupobject.sectionobject.enabled

25             };

26 

27             //最後傳回section對象

28             return section;

29         }

領域驅動設計(DDD)的實踐經驗分享之ORM的思考

上面的代碼裡面都已經加了注釋了,我想已經比較清楚了,是以就不做更多解釋了。

好了,到這裡,我想大家應該都已經比較清楚我想表達的意思了。就是我們可以用orm架構,但不是用它來直接将對象和資料庫進行映射,而是我們需要先定義一些描述對象狀态的狀态對象,然後将狀态對象和資料庫進行映射。

你可能還會問,我不想這麼麻煩,我還是想直接将對象和資料庫進行映射。這樣我就可以不用再去重複定義狀态對象了。沒錯,也許這樣更好,但你必須做出一些犧牲,比如你的對象的狀态必須用屬性實作,或者如果要實作某些進階功能比如延遲加載,則可能還要按照某些特定orm架構的要求将屬性聲明為virtual或entityset/entityref/iset之類的。其實應該還有很多其他的限制,本人對orm架構不太熟悉,也隻能想到這些了。如果你都能接受這些限制出現在你的那些可愛的,活的,被報以厚望的,想要被重用n多次的對象中的話。那沒有問題,你可以直接将對象和資料庫進行映射。對了,我還想到一個不應該直接将對象映射到資料庫而應該将狀态對象映射到資料庫的理由。就是對象之間往往很複雜,比如在領域驅動設計中,一個聚合對象(aggregate root)可能會聚合很多子對象(aggregate child),它們之間往往會有各種各樣複雜的關聯。是以當你在做orm映射時,你會發現将對象映射到資料庫表也不是一件那麼容易的事情,一大堆的xml檔案,搞的你眼睛看花。當然如果用工具自動生成映射那會簡單很多,但我表示很感慨那個工具盡然能将如此複雜的具有各種内部關聯的對象映射到資料庫,并且是自動的,我想要想讓工具可以自動完成那樣的事情,不知道該告訴它多少資訊啊!表示感慨!

相反,如果你設計一個扁平的靜态的一維的狀态對象類,設計的時候盡量多兼顧考慮表結構和對象狀态拆分這兩個因素,那我想設計出來的狀态對象類一定可以很容易的和資料庫進行映射,而且基本上99%的情況下都是類和表一對一映射。我現在在開發蜘蛛俠論壇的第三個版本,關于對象持久化和重建或建立的設計就是上面這些了。目前自我感覺良好,因為無論是多麼複雜的對象,即使它沒有一個屬性而隻有方法,我也能夠很輕而易舉的将它的狀态儲存到資料庫或者從資料庫取出來。

好了,以上就是我對orm的一些個人觀點。歡迎大家批評指正。本來還想把接下來的三個問題一起寫完的,但感覺如果把所有的内容都寫在一起,會太長,影響大家閱讀,是以決定分成不同的随筆來寫。我會盡快在接下來的随筆中和大家分享其他的幾個問題以及我的解決方案。