文章出處

寫在前面

  閱讀目錄:

  在上一篇《一縷陽光:DDD(領域驅動設計)應對具體業務場景,如何聚焦 Domain Model(領域模型)?》博文中,探討的是如何聚焦領域模型(拋開一些干擾因素,才能把精力集中在領域模型的設計上)?需要注意的是,上一篇我講的并不是如何設計領域模型(本篇也是)?而是如何聚焦領域模型,領域模型的設計是個迭代過程,不能一概而論,還在路上。

  當有一個簡單的領域模型用例,完成一個從上而下過程的時候,就需要對領域模型和數據庫進行對象關系映射(ORM),首先,在領域驅動設計中,領域模型是活的(具有自己的行為和狀態),而映射到數據庫中所謂的表是死的(只是一些字段),如何把活的變成死的?又如何把死的變成活的?更重要的是如何保證在這個“死去活來”的過程中,死的和活的是同一個?

  轉換過程很簡單,使用 ORM(對象關系映射)工具就很方便的完成這個“死去活來”的過程,但是有時候我們在這個轉換過程中,可能會失去轉換對象的本質,以致活的會變成死的,最后轉換過程就只有死的變成死的(反復循環)。

設計誤區

  在 MessageManager 項目的上一個版本中,主要存在兩個領域模型:Messgae 和 User,他們數據庫之間的映射關系是一對多的關系,就是一個用戶擁有多個消息,但是一個消息只能對應一個用戶(發件人或收件人),我們看下領域模型的設計(暫不包含業務邏輯)。

  Domain Model-Message:

 1 namespace MessageManager.Domain.DomainModel
 2 {
 3     public class Message : IAggregateRoot
 4     {
 5         #region 構造方法
 6         public Message()
 7         {
 8             this.ID = Guid.NewGuid().ToString();
 9         }
10         #endregion
11         
12         #region 實體成員
13         public string FromUserID { get; set; }
14         public string FromUserName { get; set; }
15         public string ToUserID { get; set; }
16         public string ToUserName { get; set; }
17         public string Title { get; set; }
18         public string Content { get; set; }
19         public DateTime SendTime { get; set; }
20         public bool IsRead { get; set; }
21         public virtual User FromUser { get; set; }
22         public virtual User ToUser { get; set; }
23         #endregion
24 
25         #region IEntity成員
26         /// <summary>
27         /// 獲取或設置當前實體對象的全局唯一標識。
28         /// </summary>
29         public string ID { get; set; }
30         #endregion
31     }
32 }

  Domain Model-User:

 1 namespace MessageManager.Domain.DomainModel
 2 {
 3     public class User : IAggregateRoot
 4     {
 5         #region 構造方法
 6         public User()
 7         {
 8             this.ID = Guid.NewGuid().ToString();
 9         }
10         #endregion
11         
12         #region 實體成員
13         public string Name { get; set; }
14         public virtual ICollection<Message> SendMessages { get; set; }
15         public virtual ICollection<Message> ReceiveMessages { get; set; }
16         #endregion
17 
18         #region IEntity成員
19         /// <summary>
20         /// 獲取或設置當前實體對象的全局唯一標識。
21         /// </summary>
22         public string ID { get; set; }
23         #endregion
24     }
25 }

  乍一看,Message 和 User 領域模型并沒有什么問題,只是設計的太貧血(只是包含一些屬性字段),拋開業務邏輯,我們看下 Message 和 User 之間的關聯,Message 模型中擁有 FromUserID,FromUserName,ToUserID,ToUserName 字段,用來表示和 User 模型的關聯,Navigation Properties(導航屬性)為:FromUser 和 ToUser,類型為 User,再看一下 User 模型的導航屬性:SendMessages 和 ReceiveMessages,類型為 ICollection<Message>,我們如果按照平常的開發模式(腳本驅動模式),這樣設計沒有一點問題,很方便對 ORM 進行配置:

 1         /// <summary>
 2         /// Initializes a new instance of <c>MessageConfiguration</c> class.
 3         /// </summary>
 4         public MessageConfiguration()
 5         {
 6             HasKey(c => c.ID);
 7             Property(c => c.ID)
 8                 .IsRequired()
 9                 .HasMaxLength(36)
10                 .HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
11             Property(c => c.FromUserID)
12                 .IsRequired()
13                 .HasMaxLength(36);
14             Property(c => c.ToUserID)
15                 .IsRequired()
16                 .HasMaxLength(36);
17             Property(c => c.Title)
18                 .IsRequired()
19                 .HasMaxLength(50);
20             Property(c => c.Content)
21                 .IsRequired()
22                 .HasMaxLength(2000);
23             Property(c => c.SendTime)
24                 .IsRequired();
25             Property(c => c.IsRead)
26                 .IsRequired();
27             ToTable("Messages");
28 
29             // Relationships
30             this.HasRequired(t => t.FromUser)
31                 .WithMany(t => t.SendMessages)
32                 .HasForeignKey(t => t.FromUserID)
33                 .WillCascadeOnDelete(false);
34             this.HasRequired(t => t.ToUser)
35                 .WithMany(t => t.ReceiveMessages)
36                 .HasForeignKey(t => t.ToUserID)
37                 .WillCascadeOnDelete(false);
38         }

  上面代碼表示 Message 的映射配置,如果外鍵可以為 NULL,則使用 HasOptional 方法,多對對則使用 HasMany 和 WithMany,WillCascadeOnDelete 用來級聯刪除配置,EntityTypeConfiguration 的具體詳細配置,請參照:http://msdn.microsoft.com/zh-cn/data/jj591620.aspx

  上面的設計到底有沒有問題?我們來分析一下,首先 User 領域模型中的 SendMessages 和 ReceiveMessages 屬性,如果單獨作為導航屬性,這是沒有什么問題的,因為我們可以使用導航屬性很方便的進行映射配置(比如上面代碼),但是放在領域模型中就有點不倫不類了,User 是一個用戶對象,我們不能在它的身上來掛一些屬于它的東西,因為這些并不是用戶本身所具有的,這就好像我設計一個用戶模型,它擁有手機,電腦,背包,房子,車子等等,然后就必須在這個用戶模型中添加這個屬性,這樣設計就會很不合理,這個應該設計在它所擁有的物品上,因為只有這些物品擁有用戶,這些物品相對于用戶來說,才有真正的存在意義。

  再來看 Message 領域模型,首先 FromUserID,FromUserName,ToUserID,ToUserName 這四個字段就讓我們看得很不順眼,因為這些都是已死的字段,Message 應該關聯的是活的 User,而并不是在它身上打上幾個 User 的標簽,這個表現應該在數據庫中(因為數據庫中就是存的這些已死的字段),而并不是在活的 Message 領域模型中,FromUser 和 ToUser 的設計是沒有問題的,因為關聯的就是活的 User 對象。

  為什么有了 FromUser 和 ToUser 對象,Message 領域模型中還要添加上面那四個字段呢?主要原因還是受思維模式的影響(腳本驅動模式),雖然是基于領域模型設計,但是在設計過程中就會不自覺的往腳本驅動模式上套,為什么?因為我們要使用數據庫,不管怎么設計,這些對象都是要存在數據庫中的,而數據庫存的都是一些已死的對象(只是包含字段),對象死了,那怎么來表示 Message 和 User 對象之間的關聯呢?答案就是 FromUserID 和 ToUserID,因為只有通過這兩個字段,才能在數據庫中體現 Message 和 User 對象之間的關聯,數據庫存儲中確實是這么做的,但是我們把數據庫中的關聯表現在領域模型中就很不合適了,最后的結果就是 FromUser 和 ToUser 對象的作用只是用來映射配置,Message 領域模型變成和數據庫中的 Message 表一樣,狀態都是已死,轉換也就是死的對象轉換為死的對象。

  那到底怎么設計?答案就是把 Message 領域模型中的 FromUserID,FromUserName,ToUserID,ToUserName 四個屬性去掉,User 領域模型中的 SendMessages 和 ReceiveMessages 屬性也去掉,讓領域模型變得干凈。那有人會問了,你把這些關聯字段去掉了,怎么去映射數據庫呢?天無絕人之路,使用 EntityFramework(ORM 工具之一)就很方便的進行映射配置,具體配置,可以看下枚舉映射和關聯映射兩個節點。

數據庫已死

  本節點純屬扯淡,兄臺們不感興趣的話,可以直接略過。

  “數據庫已死”的這個概念,并不是本人提出的,早在六年前在解道中就有人提出,具體可以參考:

  首先,強調一點,數據庫已死的概念,并不是說我們項目中不使用數據庫(想想應用程序不使用數據庫也不可能),只是說應用程序設計的核心不再是基于數據庫設計的,而應該是基于面向對象設計,數據庫只是存儲數據的一種方式,當然也可以配置文件存儲或者內存存儲。以往我們進行應用程序設計的時候,都是先根據業務需求定義表結構,然后根據表結構用“面向對象”的語言去傳遞 SQL 放到數據庫中執行,這樣面向對象語言就成了所謂的 SQL 搬運工,這樣造成的問題就是非常難維護,牽一發而動全身,而且性能瓶頸也主要體現在數據庫方面,想想應用程序的性能問題(排除代碼問題),我們可以使用負載均衡增加服務器,來分擔所帶來的壓力,而應對數據庫性能問題呢?從“MySpace”的經歷上就可以看出,那是相當的難處理,而且性能問題主要集中在數據庫方面,也是設計的不合理所造成的。

  我們來看一下 MySqace 的信息系統發展歷程:

  • 第一代架構—添置更多的Web服務器:因為用戶量小,所以我們一般部署應用程序的時候,都是應用程序和數據庫各部署一臺,當用戶暴增之后,我們就開始部署更多的應用程序服務器(數據庫服務器還是一臺),但是當用戶量達到一定的程度后,部署再多的應用程序服務器已沒有什么用,因為數據庫服務器就一臺。
  • 第二代架構—增加數據庫服務器:與增加 Web 服務器不同,增加數據庫并沒那么簡單。如果一個站點由多個數據庫支持,設計者必須考慮的是,如何在保證數據一致性的前提下讓多個數據庫分擔壓力。MySpace 運行在三個 SQL Server 數據庫服務器上—一個為主,所有的新數據都向它提交,然后由它復制到其它兩個;另兩個數據庫服務器全力向用戶供給數據,用以在博客和個人資料欄顯示。這種方式在一段時間內效果很好—只要增加數據庫服務器,加大硬盤,就可以應對用戶數和訪問量的增加。說到低,這種方式就是拆分數據庫,不同的應用程序使用數據庫不同,然后部署再不同的服務器上,但是當用戶再次達到一定程度后,這種方案也不太適合了。
  • 第三代架構—轉到布式計算架構:MySpace 將目光移到分布式計算架構——它在物理上分布的眾多服務器,整體必須邏輯上等同于單臺機器。拿數據庫來說,就不能再像過去那樣將應用拆分,再以不同數據庫分別支持,而必須將整個站點看作一個應用。現在,數據庫模型里只有一個用戶表,支持博客、個人資料和其他核心功能的數據都存儲在相同數據庫。既然所有的核心數據邏輯上都組織到一個數據庫,那么 MySpace 必須找到新的辦法以分擔負荷——顯然,運行在普通硬件上的單個數據庫服務器是無能為力的。這次,不再按站點功能和應用分割數據庫,MySpace 開始將它的用戶按每百萬一組分割,然后將各組的全部數據分別存入獨立的SQL Server實例。可以看出這種方式顯然也不能滿足高用戶量的需求。
  • 第四代架構—求助于微軟方案:2005年早期,賬戶達到九百萬,MySpace 開始用微軟的 C# 編寫 ASP.NET 程序。在收到一定成效后,MySpace 開始大規模遷移到 ASP.NET。用戶達到一千萬時,MySpace 再次遭遇存儲瓶頸問題。SAN 的引入解決了早期一些性能問題,但站點目前的要求已經開始周期性超越 SAN 的 I/O 容量——即它從磁盤存儲系統讀寫數據的極限速度。
  • 第五代架構—增加數據緩存層并轉到支持 64 位處理器的 SQL Server 2005:MySpace 賬戶達到一千七百萬,MySpace 又啟用了新的策略以減輕存儲系統壓力,即增加數據緩存層——位于 Web 服務器和數據庫服務器之間,其唯一職能是在內存中建立被頻繁請求數據對象的副本,如此一來,不訪問數據庫也可以向 Web 應用供給數據。2005年中期,服務賬戶數達到兩千六百萬時,MySpace 因為我們對內存的渴求而切換到了還處于 beta 測試的支持 64 位處理器的 SQL Server 2005。升級到 SQL Server 2005 和 64 位 Windows Server 2003 后,MySpace 每臺服務器配備了 32G 內存,后于 2006 年再次將配置標準提升到 64G。
  • 。。。。

  總結:從 MySpace 看更加驗證,數據庫是軟件系統的瓶頸,而且最不可伸縮,一旦數據庫成為系統瓶頸,就得動大手術,實現架構上的變遷,這是傷筋動骨,變遷人員壓力巨大的。另外由于是社區,就是變遷數據丟失也沒什么大不了,如果是企業那就......

  如果我們從軟件系統開始之初,就使用對象分析設計,不與數據庫沾邊,整個流程就完全 OO,分析設計直至代碼都擺脫了數據庫影響,這個流程如下:

  1. 分析建模(基于領域驅動設計的業務建模)
  2. 細化設計(基于領域驅動設計的架構設計)
  3. 代碼實現
  4. 調試測試
  5. 部署運行

  那么數據庫在什么時候建立呢?數據庫表結構的創建可以延緩到部署運行時,這樣,整個上游環節就不涉及數據庫技術,而是使用更符合自然的表達 OO 方式,軟件質量就更高了。現在,很多人已經理解,分析設計要用 OO,但是數據庫是運行階段缺少不了的,確實,這是正確觀點,我們奪取數據庫的王位,不是將它打倒,只是理性和平移交權力重心而已,數據庫退出主角地位,讓位于中間件,也預示著過去數據庫為王的時代的結束, 但是數據庫會和操作系統一樣,成為我們現代軟件系統一個不可缺少重要的基礎環節。

  了解了這么多,回到”設計誤區“這一節點,你會發現,造成設計誤區的主要原因還是,在設計的時候不自覺以數據庫為中心了,而并非領域模型。

枚舉映射

  在 Message 領域模型中,有個 MessageState 枚舉類型,用來表示消息的狀態,當然我們也可以使用 Bool 類型的字段來表示,但是消息狀態是消息本身的一種狀態,用對象來表示更為合適,MessageState 枚舉定義如下:

1 namespace MessageManager.Domain.DomainModel
2 {
3     public enum MessageState
4     {
5         Read,
6         NoRead
7     }
8 }

  我們使用單元測試對映射轉換進行測試,也就是 Code First 模式,測試代碼:

 1 namespace MessageManager.Repositories.Tests
 2 {
 3     public class UserRepositoryTest
 4     {
 5         [Fact]
 6         public void AddUserRepository()
 7         {
 8             IUserRepository userRepository = new UserRepository(new EntityFrameworkRepositoryContext());
 9             User user1 = new User("小菜");
10             User user2 = new User("大神");
11             userRepository.Add(user1);
12             userRepository.Add(user2);
13             userRepository.Context.Commit();
14         }
15     }
16 }

  生成數據庫發生異常:

  這個主要原因是當前 EntityFramework 版本不支持枚舉類型映射,當前使用的 EntityFramework 版本為 4.3.1:

1 <?xml version="1.0" encoding="utf-8"?>
2 <packages>
3   <package id="EntityFramework" version="4.3.1" targetFramework="net40" />
4 </packages>

  EntityFramework 的版本太老了,更新版本為 6.1.1,NuGet 更新命令:update-package EntityFramework

  EntityFramework 從 4.3.1 升級到 6.1.1 更改的地方(http://msdn.microsoft.com/en-us/data/upgradeef6.aspx):

  1. System.Data.EntityState.Modified; 更改為 System.Data.Entity.EntityState.Modified;
  2. DatabaseGeneratedOption 需要添加 System.ComponentModel.DataAnnotations.Schema 命名空間。

  在升級完 EntityFramework 版本后,重新運行單元測試,但是發現又報如下錯誤:

  解決方案:

  在 MessageManagerDbContext 構造函數中添加如下代碼:

1         public MessageManagerDbContext()
2             : base("MessageManagerDB")
3         {
4             var ensureDLLIsCopied = System.Data.Entity.SqlServer.SqlProviderServices.Instance; 
5             this.Configuration.LazyLoadingEnabled = true;
6         }

  重新運行單元測試,測試成功,就會發現在 MessageManagerDB 數據庫的 Messages 表中已生成 State 字段,類型為 Int,當然也可以通過 EntityTypeConfiguration 中的 HasColumnType 進行自定義字段類型。

關聯映射

  先看一下,如果我們沒有進行任何的 EntityTypeConfiguration 關聯設置,生成數據庫會是怎樣?MessageConfiguration 和 UserConfiguration 配置如下:

 1     public class MessageConfiguration : EntityTypeConfiguration<Message>
 2     {
 3         /// <summary>
 4         /// Initializes a new instance of <c>MessageConfiguration</c> class.
 5         /// </summary>
 6         public MessageConfiguration()
 7         {
 8             HasKey(c => c.ID);
 9             Property(c => c.ID)
10                 .IsRequired()
11                 .HasMaxLength(36)
12                 .HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
13             Property(c => c.Title)
14                 .IsRequired()
15                 .HasMaxLength(50);
16             Property(c => c.Content)
17                 .IsRequired()
18                 .HasMaxLength(2000);
19             Property(c => c.SendTime)
20                 .IsRequired();
21         }
22     }
 1     /// <summary>
 2     /// Represents the entity type configuration for the <see cref="Customer"/> entity.
 3     /// </summary>
 4     public class UserConfiguration : EntityTypeConfiguration<User>
 5     {
 6         #region Ctor
 7         /// <summary>
 8         /// Initializes a new instance of <c>UserConfiguration</c> class.
 9         /// </summary>
10         public UserConfiguration()
11         {
12             HasKey(c => c.ID);
13             Property(c => c.ID)
14                 .IsRequired()
15                 .HasMaxLength(36)
16                 .HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
17             Property(c => c.Name)
18                 .IsRequired()
19                 .HasMaxLength(20);
20         }
21         #endregion
22     }

  上面代碼中我們并沒有進行關聯配置,生成 MessageManagerDB 數據庫中 Messages 表結構:

  可以看到我們雖然沒有進行任何的關聯設置,Code First 會自動為我們創建外鍵關聯,僅僅是在 Message 領域模型中添加:

1         public virtual User SendUser { get; set; }
2         public virtual User ReceiveUser { get; set; }

  以上效果是我們想要的,這也是 EntityFramework 的進步之處,符合領域驅動設計的思想,領域模型中沒有數據庫中所謂的主外鍵關聯,有的只是對象之間的關聯,而數據庫只是存儲數據的一種表現,這樣數據庫設計的概念就不存在了,也讓我們忘了數據庫的存在,而把更多的精力放在領域模型的設計上,這就是領域驅動設計關鍵所在。

  除了 EntityFramework 默認生成關聯配置,我們也可以進行自定義配置,比如,上面生成外鍵字段為:SendUser_ID 和 ReceiveUser_ID,也可以自定義字段名稱:

1             HasRequired(x => x.SendUser)
2                 .WithMany()
3                 .Map(x => x.MapKey("SendUserID"))
4                 .WillCascadeOnDelete(false);
5             HasRequired(x => x.ReceiveUser)
6                 .WithMany()
7                 .Map(x => x.MapKey("ReceiveUserID"))
8                 .WillCascadeOnDelete(false);

  上面就是自定義外鍵字段為:SendUserID 和 ReceiveUserID,關于 EntityTypeConfiguration 的配置,比如一對一,一對多,多對多,聯合主外鍵等等,可以參考:

后記

  更新項目:MessageManager.Domain,MessageManager.Domain.Tests,MessageManager.Repositories 和 MessageManager.Repositories.Tests,其他未更新,獲取生成會報錯。

  幻想下,如果存在對象性數據庫,存儲的都是“活生生”的對象,那是怎樣的一種情形?咳咳,只是幻想。

  如果你覺得本篇文章對你有所幫助,請點擊右下部“推薦”,^_^

  參考資料:


文章列表




Avast logo

Avast 防毒軟體已檢查此封電子郵件的病毒。
www.avast.com


arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()