DDD中的EFCore


EFCore在DDD中的使用

在DDD中,我們對聚合根的操作都會通過倉儲去獲取聚合實例。
因為聚合根中可能會含有實體屬性,值對象屬性,並且,在DDD中,我們所設計的領域模型都是充血模型。所以,在對聚合根的持久化中,最方便的還是Mangodb這種KEY-VALUE存儲的NOSQL。

不過,關系型數據庫通過EF也能方便的解決復雜模型的數據庫映射。

本文使用EFCore,部分API不適用於EF;本文不談DDD。

以下引出幾個知識點:

  • backing field
  • releation
  • lazy load
  • data binding
  • navigation property
  • converter

讓我們開始吧

我們首先定義一個復雜關系的 對象模型;

大致上描述下這個BookEntity根實體類的幾個定義:

  • 擁有只讀的屬性 Name
  • 擁有兩個對象屬性AuthorCatalog
  • 枚舉EnumBookType類型屬性Type
  • 擁有兩個私有的列表字段 _chapters_keyWords

簡單映射

class BookEntity{
    private BookEntity(string name){
        Name = name;
    }
    public string Name { get; }
    public string BookCoverImage { get; private set; }
    public EnumBookType Type { get; private set; }
    //...
}

class BookEntityTypeConfiguration : IEntityTypeConfiguration<BookEntity>{
    public void Configure(EntityTypeBuilder<BookEntity> builder){
        builder.Property<string>("Id").HasColumnName("_id_")
            .HasValueGenerator<StringGuidValueGenerator>();
            
        builder.Property(x => x.Name);
        builder.Property(x => x.Type)
                .HasConversion<string>(k => k.ToString(), v => Enum.Parse<EnumBookType>(v));
    }
}

在上述代碼中,我們定義了一個簡單對象類及它的配置項。

  • BookEntity中未定義Id主鍵,我們通過 陰影屬性 的方式指定了一個主鍵,並將它映射到db的_id_列;
  • EF中默認綁定 有 setter 方法的 public getter 屬性,而我們的Name沒有setter方法,我們必須通過在配置中顯示調用 Property() 將其加入到綁定中。
  • EF中可以通過構造函數將字段綁定到實體上。
  • 可以能過調用 HasConversion() 方法顯示指定使用的轉換方法。比如將IDictionary<string,string> 保存為 string

那么我們如何根據 主鍵 查詢呢?

EF 為我們提供了靜態方法EF.Property()

var entity = ctx.Set<BookEntity>().FirstOrDefault(x=>EF.Property<string>(x,"Id") == "1");

關系與固有類型

在官方文檔中,關系主要使用以下幾種方法來配置的。

  • HasOne()
  • HasMany()
  • WithOne()
  • WithMany()

OwnsType (固有類型)是新近推出的API。

  • OwnsOne()
  • OwnsMany()

雖然都會創建導航屬性,但是從定義和使用上來看
,還是有很大區別的。

經過測試,導航屬性不能通過構造函數綁定,所以以下配置中,均使用 private setter

(如果有讀者發現錯誤,歡迎指正。)

下面我們就對兩種API進行配置。

OwnsType的配置

從使用上的角度上來看,OwnsType像其名字一樣,強調的是A擁有B,這個屬性是這個類固有的,沒有懶加載的配置。

擴展我們之前寫義的實體類。

class BookEntity{
    //...略
    public AuthorInfo Author { get; private set; }

    private List<KeyWordInfo> _keyWords = new List<KeyWordInfo>();
    public IEnumerable<KeyWordInfo> KeyWords => _keyWords;
}

class AuthorInfo
{
    public AuthorInfo(string name){
        Name = name;
    }
    public string Name { get; }
}
class KeyWordInfo
{
    public KeyWordInfo(string word){
        Word = word;
    }
    public string Word { get; }
}

擴展配置類

class BookEntityTypeConfiguration : IEntityTypeConfiguration<BookEntity>{
    builder.OwnsOne(x => x.Author, b => {
        b.Property(v => v.Name).HasColumnName("AuthorName");
    });
    builder.OwnsMany(x => x.KeyWords, b =>
    {
        b.ToTable("BookKeyWords");
        b.Property<int>("Id").HasColumnName("_id_");
        b.HasKey("Id");
        b.Property(x => x.Word);
        b.HasForeignKey("BookId");
    });

    builder.Metadata.FindNavigation(nameof(BookEntity.KeyWords))
        .SetPropertyAccessMode(PropertyAccessMode.Field);
}

默認情況下,OwnsOne()會與實體映射在同一張表,OwnsMany()沒有做具體測試。

這里我們對導航屬性KeyWords進行了配置,因為它是只讀的,所以我們將它配置為綁定為字段,這個私有字段叫做backing field(支持字段??),在EF中默認有以下4種格式,當然這是支持自定義的:

  • _< camel-cased property name >
  • _< property name >
  • m_< camel-cased property name >
  • m_< property name >

那什么是backing field ???

ReleationShip 配置

HasOne()這種關系API,更適合於A與B之前的關系,比如 1-* (一對多)的關系、1-1(一對一)的關系等等,所以這種配置必須在不同表中。

class BookEntity{
    //...略
    private IList<BookChapterEntity> _chapters = new List<BookChapterEntity>(); 
    public IEnumerable<BookChapterEntity> Chapters => _chapters;
}

class BookEntityTypeConfiguration : IEntityTypeConfiguration<BookEntity>
{
    public void Configure(EntityTypeBuilder<BookEntity> builder)
    {
        //...略
        builder.HasMany(x => x.Chapters).WithOne().HasForeignKey("BookId");

        builder.Metadata.FindNavigation(nameof(BookEntity.Chapters))
            .SetPropertyAccessMode(PropertyAccessMode.Field);
    }
}
class BookChapterEntityTypeConfiguration : IEntityTypeConfiguration<BookChapterEntity>
{
    public void Configure(EntityTypeBuilder<BookChapterEntity> builder)
    {
        builder.Property(x => x.Title);
        builder.Property(x => x.Index);
        builder.Property<string>("Id");
    }
}

從配置上來看,我們的兩個實體都是分開配置的,而從實體類角度上看,這里是兩個類體的關系,我們配置的是 1-的關系。
使用HasOne()
ReleationShip* Api配置的屬性,默認是不加載的,我們可以通過配置進行立即加載或延遲加載。

我們可以查看官方文檔查看懶加載的方式。

https://docs.microsoft.com/en-us/ef/core/querying/related-data

backing field (支持字段)

我們都知道C#中有這樣子的寫法。

class Foo{
    public string Name{get;set;}
}

寫完整了是這樣的。

class Foo{
    private string _name;
    public string Name{
        get{return _name;}
        set{_name = value;}
    }
}

而在其它語言中,可能是這樣的。

class Foo{
    private string _name;
    public string GetName(){
        return _name;
    }
    public void SetName(value){
        _name = value;
    }
}

我認為以上的_name就是一個backing field; 以字面意思解釋就是屬性底層的字段。

查詢過濾器 Query Filter

我們公司的業務設計上,數據不能真刪,通過一個 IsDeleted 字段進行控制。這樣在有必要的情況下,我們可以將數據進行還原。

class BookEntityTypeConfiguration : IEntityTypeConfiguration<BookEntity>
{
    public void Configure(EntityTypeBuilder<BookEntity> builder)
    {
        //...略
        builder.Property<bool>("IsDeleted");
        builder.HasQueryFilter(x=>!EF.Property<bool>(x,"IsDeleted"));
    }
}

我們通過HasQueryFilter()配置一個全局過濾器。

什么?你說又要查詢的時候要查詢IsDeleted == true的數據??

var allBooks = ctx.Set<BookEntity>()
    .IgnoreQueryFilters()
    .ToList();
//通過 IgnoreQueryFilters 忽視掉全局過濾器;

更多的查看官方文檔

https://docs.microsoft.com/en-us/ef/core/querying/filters

結尾總結

在我們項目切換到DDD模式下開發的時候,使用關系型數據庫作為倉儲的實現真是頭疼。還好,我們有EF,但是如果對EF的API和映射不熟悉的話,會導致出現因技術原因修改領域模型的情況,而這種情況是我們應該避免的。

如發現文中有誤,歡迎指正。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM