文章出處

寫在前面

首先,這篇博文是用博客園新發布的 MarkDown編輯器 編寫的,這也是我第一次使用,語法也不是很熟悉,但我覺得應該會很爽,博文后面再記錄下用過的感受,這邊就不多說。

閱讀目錄:

  1. 上一篇回顧-設計誤區
  2. 值對象映射探討
  3. 走過的坑-正確配置
  4. 后記-附帶(CNBlogs 使用 Mardown 小記)

領域驅動設計中,關于領域模型和 EntityFramework 之間的映射配置,其實之前寫過一篇《死去活來,而不變質:Domain Model(領域模型) 和 EntityFramework 如何正確進行對象關系映射?》博文,因為當時主要精力是在領域模型的設計中,持久化問題考慮的太早,所以在當時領域驅動設計的道路上跑偏了。現在領域模型設計的差不多了,因為之前都是在 Repository(倉儲)中使用靜態集合跑程序,現在持久化的問題是該考慮了。

說真的,其實現在來看,上一篇探討的內容還是蠻有價值的,如果你對領域模型和 EntityFramework 之間映射配置感興趣,最好還是閱讀下上一篇博文,如果沒時間閱讀也沒關系,我來帶你簡單回顧一下。

上一篇回顧-設計誤區

上一篇博文的關鍵字是:死去活來,而不變質,也就是:如何把活的變成死的?又如何把死的變成活的?更重要的是如何保證在這個“死去活來”的過程中,死的和活的是同一個?

活的:Domain Model(領域模型),主要是領域模型中的 Entity(實體)對象。
死的:使用 ORM 工具映射,把領域模型映射到關系型數據庫的表數據。

在領域驅動設計中,數據庫設計的概念是被我們所拋棄的,也就是說,在你領域模型設計的過程中,不應該考慮數據庫的因素,這個過程應該放到最后,也就是我現在所考慮的,這也就是為什么之前探討持久化問題是跑偏的原因了。還有一個重要概念,就是數據庫不是被設計的,而是應該被生成的,當你應用程序設計完成的時候,你只需要配置下倉儲的持久化實現,這樣數據庫就可以使用 Code First 進行生成了。

過程雖然說起來簡單,實現起來卻不是那么容易,因為我們長久以往受數據庫驅動模式的影響,在應用程序開發的時候,就會不自覺的去考慮數據庫。比如一個用戶模塊,按照我們傳統的開發模式,應該是先設計用戶模塊的表結構(用戶表、用戶部門表、用戶權限表等等),然后根據表結構去設計一大堆的 SQL 語句(左關聯、右關聯、自己關聯等等),數據庫訪問層(DAL)就充斥著大量的 SQL 代碼,其實這些代碼就反應了業務需求,以至于我們的業務邏輯層(BLL)變成了一個方法調用者(dal.GetUser....),它確實很薄,薄到可以直接忽略掉,客戶端代碼是怎樣的呢?簡單的來說就是從界面上獲取值,然后 new 一個 bll 對象,調用方法傳入值,沒錯,就是這樣。

那這樣致使的結果是怎樣的呢?比如要該一個需求,麻煩一點的就是,我們需要改表結構,改完表結構,我們需要改數據訪問層的 SQL 代碼,改完 SQL 代碼,我們需要改業務邏輯層中的方法參數,改完方法參數,我們需要改客戶端的調用....沒完沒了,這還只是一個需求的變更,我相信我們每天遇到的不只是一個吧,想想真是太痛苦了。

好像有點偏離主題了,但是體會這個傳統開發模式是很重要的,因為只有體會到它的痛苦,你才會想辦法去改變它,當然除非你是處在一個“溫水煮青蛙”的環境中,這個就沒辦法了。

回到領域驅動設計上來,領域模型(主要是實體,后面用實體表示)如何使用 EntityFramework 進行映射配置?簡單一點,這個實體沒有任何對象的關聯,那我們根根不需要什么映射配置,只需要配置一下主鍵和字段長度就行了。但是如果存在對象關聯,我們怎么配置呢?按照之前數據庫驅動模式的開發,肯定要在相應的關聯表中加入外鍵,那我們的實體就會變成這樣:

namespace MessageManager.Domain.DomainModel
 {
     public class Message : IAggregateRoot
     {
         #region 構造方法
         public Message()
         {
             this.ID = Guid.NewGuid().ToString();
         }
         #endregion
         
         #region 實體成員
         public string FromUserID { get; set; }
         public string FromUserName { get; set; }
         public string ToUserID { get; set; }
         public string ToUserName { get; set; }
         public string Title { get; set; }
         public string Content { get; set; }
         public DateTime SendTime { get; set; }
         public bool IsRead { get; set; }
         public virtual User FromUser { get; set; }
         public virtual User ToUser { get; set; }
         #endregion
 
         #region IEntity成員
         /// <summary>
         /// 獲取或設置當前實體對象的全局唯一標識。
         /// </summary>
         public string ID { get; set; }
         #endregion
     }
 }

按照我們之前數據庫模式,會覺得這樣設計沒錯啊,但是現在是基于領域驅動設計,你會那發現 FromUserIDToUserID 這兩個是什么東西啊?只是為了方便數據庫映射,就加入這兩個“外鍵”,很顯然,這種設計是不合理的。

還有一種設計也是不合理的,就是在實體屬性上面加入 EntityFramework 屬性配置,領域模型中應該是和技術無關的,如果加入技術實現,那這個領域模型就被污染了,像 EntityFramework 的 Attribute 配置應該放在基礎層去實現,當然我個人覺得,這是 EntityFramework 有點誤導人的感覺,因為在實體屬性上面進行配置更方便,但是在領域驅動設計中,這樣實現并不合理,比如下面這段代碼:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace DemoTag.Domain.Entities
{
    [Table("TagUseCount")]
    public class TagUseCount
    {
        [Key]
        [Column(Order = 1)]
        public Guid AppGuid { get; set; }

        [Key]
        [Column(Order = 2)]
        [ForeignKey("Tag")]
        public int TagId { get; set; }

        public int UseCount { get; set; }

        public virtual Tag Tag { get; set; }
    }
}

如果我們不這樣進行實現,那我們如何進行映射配置呢?這個實現在后面有講解,在實現之前,要先明確幾個重要概念:

1,領域模型不參雜任何的技術實現。
2,數據庫的映射配置,不影響領域模型(比如上面的 FromUserIDToUserID,就是很不合理)。
3,數據庫的映射配置,屬于技術實現,應該放在基礎層中。

因為第二點相對比較難理解一點,這邊我就再簡單說明下,數據庫是領域模型存儲數據的一種方式(我們也可以使用其他方式進行存儲),現在的關系型數據庫都是“扁平化”存儲,所以像對象之中關聯對象,我們一般都是要進行外鍵配置,這因為有了 ORM 工具,所以我們可以很方便的進行對象關系映射(ORM 的中文意思),對象指的就是領域模型,關系就是關系型數據庫。所以我們映射配置不應該影響領域模型,具體怎么進行配置?這是 ORM 工具所考慮的問題,上一篇的內容是主要是關于實體映射配置,下面簡單說下領域模型中值對象的映射配置。

值對象映射探討

有人可能有些疑問,值對象需要映射配置嗎?當然,簡單一點的枚舉類型的值對象,是不需要進行映射配置的,比如下面 MessageState 這個值對象:

/**
* author:xishuai
* address:https://www.github.com/yuezhongxin/MessageManager
**/

namespace MessageManager.Domain.ValueObject
{
    public enum MessageState
    {
        Unread,
        Read,
    }
}

在 Message 實體中對應的關聯:

public MessageState State { get; private set; }

上面這段代碼,如果我們使用 EntityFramework,是不需要任何映射配置的,枚舉類型的值對象會自動映射為 int 類型,比如上面 MessageState 的映射結果為:0 代表 Unread,1 代表 Read。這個映射過程,在領域驅動設計中是不關心的,在應用層,我只關心從倉儲中持久化的對象或者獲取的對象,是不是正確的實體對象?是不是正確的值對象?也就是說我現在在應用層中去編寫下面這段代碼:

using (IRepositoryContext repositoryContext = new EntityFrameworkRepositoryContext())
{
    IMessageRepository messageRepository = new MessageRepository(repositoryContext);
    Message message = messageRepository.GetByKey(1);
    if (message.State == MessageState.Unread)
    {
        //默認是未讀
    }
}

message.State == MessageState.Unread 這是我所關心的,我從倉儲中取的是不是我所存儲的正確值對象。其實這也是 EntityFramework 這一類 ORM 工具的強大之處,在領域驅動設計中更能得到體現,它讓我們更專注于領域模型的設計,而不考慮數據是怎樣進行存儲的,那如何進行隔離他們兩者呢?答案就是 Repository(倉儲),很多時候,都是由問題引出概念,這樣理解的才會更加深刻。

如果我們映射的不是枚舉類型的值對象,而是其他類型的值對象,我們怎么進行映射配置呢?比如下面 Contact 值對象:

/**
* author:xishuai
* address:https://www.github.com/yuezhongxin/MessageManager
**/

namespace MessageManager.Domain.ValueObject
{
    public class Contact
    {
        public Contact(string name)
        {
            this.Name = name;
        }

        public Contact(string name, string displayName)
        {
            this.Name = name;
            this.DisplayName = displayName;
        }

        public string Name { get; private set; }
        public string DisplayName { get; private set; }
    }
}

先說一下 Contact 值對象的意思,表示 Message 實體中的抽象“聯系人”標識,說白了就是發送人和接收人的意思,但這個發送人或接收人不一定是“人”,也可能是郵箱等,就是一個標識的意思,這個“標識”從是外部取得的,也就是說在消息這個系統中是不存儲的,我只知道這個標識是什么?那不需要知道它是哪個?這也就是為什么設計成值對象的原因了。

Contact 值對象就不像 MessageState 值對象不需要那樣了,這個就必須在 EntityFramework 進行配置的,具體如何進行映射配置,請看下面,走過的坑

走過的坑-正確配置

首先,我試了下,如果不進行映射配置會是怎樣的結果,比如我們在 MessageConfiguration 映射配置類中(實現在基礎層)配置如下:

using MessageManager.Domain.Entity;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity.ModelConfiguration;

namespace MessageManager.Repositories.EntityFramework.ModelConfigurations
{
    public class MessageConfiguration : EntityTypeConfiguration<Message>
    {
        /// <summary>
        /// Initializes a new instance of <c>MessageConfiguration</c> class.
        /// </summary>
        public MessageConfiguration()
        {
            HasKey(c => c.ID);
            Property(c => c.ID)
                .IsRequired()
                .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
            Property(c => c.Title)
                .IsRequired()
                .HasMaxLength(50);
            Property(c => c.Content)
                .IsRequired()
                .HasMaxLength(2000);
            Property(c => c.SendTime)
                .IsRequired();
        }
    }
}

可以看到,我們只對一些簡單屬性進行了簡單配置,并沒有對 Contact 進行任何的映射配置,那 EntityFramework 生成數據庫會是怎樣呢(使用 Code First 模式)?答案就是:報錯

RepositoryTest_AddMessage 單元測試代碼(一定要先進行單元測試,在領域驅動設計開發過程中,非常重要):

/**
* author:xishuai
* address:https://www.github.com/yuezhongxin/MessageManager
**/

using MessageManager.Domain.Entity;
using MessageManager.Domain.Repositories;
using MessageManager.Domain.ValueObject;
using MessageManager.Repositories.EntityFramework;
using Xunit;

namespace MessageManager.Repositories.Tests
{
    public class MessageRepositoryTest
    {
        [Fact]
        public void RepositoryTest_AddMessage()
        {
            IMessageRepository messsageRepository = new MessageRepository(new EntityFrameworkRepositoryContext());
            messsageRepository.Add(new Message("title", "content", new Sender("1", "小菜"), new Recipient("2", "大神")));
            messsageRepository.Context.Commit();
        }
    }
}

異常信息:

注意紅圈里面的信息,因為我只找到這個異常信息(第一段):在 System.Data.Entity.Utilities.Check.NotNull T (T value, String parameterName),完全不知道是什么原因,NotNull 也就是有一個參數為 NULL,具體是什么,并不知道,怎么辦呢?難道讓我去調試 EntityFramework 源碼?把 Google 給忘了,搜索了一下,在 stackoverflow 中找到了類似問題,解決方案就是:

[NotMapped]
public HttpPostedFileBase Photo { get; set; }

NotMapped 顧名思義,就是忽略映射的意思,也就是說在 EntityFramework 生成數據庫的時候,Photo 這個屬性并不映射。NotMapped 是直接在實體中定義屬性配置,這個我們在上面強調過,這樣設計不是合理的,我們應該在 MessageConfiguration 中進行配置,那就不能使用 NotMapped 屬性了,在 EntityTypeConfiguration 配置中,找到 Ignore 方法,配置如下:

Ignore(c => c.Sender);
Ignore(c => c.Recipient);

配置好了,我們再生成數據庫:

可以看到我們是生成成功的,Message 實體對象的 SenderRecipient 是被忽略的,但是這并不是我們想要的結果,因為我們是要映射配置 Contact,這才是我們的目的,怎么把它給忽略了啊。雖然走了彎路,但是讓我們發現異常問題,確實是 Contact 映射引起的(我之前還懷疑是不是 EntityFramework 配置有什么問題)。

確定了問題的原因,就要找相應的解決辦法。因為值對象強調的是“值”的概念,也就是說映射到數據庫的時候,要把值對象進行“扁平化”處理,Contact 值對象包含 NameDisplayName 兩個屬性(之前還有一個 LoginName 屬性,后來考慮了一下,其實并不需要),也就是說,這兩個屬性都必須映射到 Message 實體中,然后 EntityFramework 進行數據到對象的轉化,我們就可以通過 message.Sender 訪問到 Contact 值對象了,這是我們想要的效果,在倉儲中只需要 Add 和Get`Message 對象,并不需要 Contact 值對象的任何操作,因為 Contact 值對象是依附于 Message 實體的,所以必須通過 Message 實體進行操作。

Google 中搜索“entitytypeconfiguration value object”,在 stackoverflow 中找到相似的解決方法,配置如下:

Property(c => c.Sender.Name)
     .HasColumnName("SenderName")
     .IsRequired()
     .HasMaxLength(36);
Property(c => c.Recipient.Name)
     .HasColumnName("RecipientName")
     .IsRequired()
     .HasMaxLength(36);
Property(c => c.Sender.DisplayName)
     .HasColumnName("SenderDisplayName")
     .HasMaxLength(50);
Property(c => c.Recipient.DisplayName)
     .HasColumnName("RecipientDisplayName")
     .HasMaxLength(50);

生成相應數據庫:

單元測試:

其實在 entitytypeconfiguration 的配置中,不止上面的一些坑,還有很多沒有記錄到,關于 entitytypeconfiguration 的正確配置,請參考 MSDN 中的相關內容

后記-附帶(CNBlogs 使用 Mardown 小記)

CNBlogs 使用 Mardown 使用感受

  1. 寫代碼,寫博文,這種方式很爽。
  2. 以前用其他編輯器寫博文,會有很多樣式干擾,比如復制編輯器中的內容,會把格式也復制進來,造成 html 的臃腫(看著很多重復的 span 標記,就是不爽)。
  3. 修改起來很方便,比如修改插入的代碼,直接在里面修改就可以了。
  4. 方便統一博文內容整體的樣式。
  5. 寫起來超迅速,流暢,這篇博文內容也不是很少,歷時幾個小時(平常會多點),寫起來的“手感”很好。
  6. 當然是簡約了,但不失簡單。
  7. 。。。。。

CNBlogs 使用 Mardown 使用小技巧

  1. 如果博文是使用 Mardown 編寫的,正文的 div 會添加一個 cnblogs-markdown class 樣式,這樣方便我們修改用 Mardown 寫的博文樣式,比如修改字體,就可以添加如下樣式:.cnblogs-markdown p { font-size: 15px; }。
  2. 可以使用 Mardown 在線編輯器,這樣可以一邊寫,一邊查看樣式,然后再復制到 CNBlogs 中。
  3. 暫時發現這么多,后面再補充。。。

回到正題,關于 Value Object(值對象)如何使用 EF 進行正確映射?你會發現,其實也就是這一點內容,但都是踩著坑走過來的,需要注意的是,在進行映射配置的時候,要始終記得:映射配置不能影響到領域模型,也就是說,如果映射配置出現了問題,不能從領域模型中去找解決方案,這是技術問題,不能污染到領域模型。

關于領域驅動設計的實踐-MessageManager,也開發不少時間了,同時也整理了幾篇博文,如果你對領域驅動設計感興趣,可以訪問下 DDD 標簽 進行了解,后面有時間再做個詳細總結,這篇內容就到這里,也感謝你可以看到這。


文章列表




Avast logo

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


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

IT工程師數位筆記本

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