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
- 擁有兩個對象屬性
Author
和Catalog
- 枚舉
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 忽視掉全局過濾器;
更多的查看官方文檔
結尾總結
在我們項目切換到DDD模式下開發的時候,使用關系型數據庫作為倉儲的實現真是頭疼。還好,我們有EF,但是如果對EF的API和映射不熟悉的話,會導致出現因技術原因修改領域模型的情況,而這種情況是我們應該避免的。
如發現文中有誤,歡迎指正。