前言
哈嘍大家周五好臼寄,我們又見面了霸奕,感謝大家在這個周五讀我的文章,經(jīng)過了三周的時間吉拳,當然每周兩篇的速度的情況下质帅,咱們簡單說了下DDD領域驅(qū)動設計的第一部分,主要包括了留攒,《項目入門DDD架構(gòu)淺析》煤惩,《領域、子領域炼邀、限界上下文》魄揉,《DDD使用意義》,《實體與值對象》拭宁,《聚合與聚合根》這五部分內(nèi)容洛退,主要的是以解釋為主,舉例子Code為輔的形式杰标,總體來說還是得到一些肯定的兵怯,也是我最大的動力了。
上邊這五個知識點是DDD領域驅(qū)動設計的第一部分 —— D領域腔剂;
從今天開始媒区,咱們就說說DDD的第二個D,就是領域服務+領域命令的CQRS掸犬,這些偏重動作的一部分袜漩;
最后就是第三部分,通過 領域事件登渣、事件源與事件回溯噪服,配合著權(quán)限管理,再統(tǒng)一說一下DDD胜茧,這一系列就是結(jié)束了。
其實通過我看到這里仇味,我發(fā)現(xiàn)了呻顽,我們在設計DDD的時候,重要的是思路丹墨,重要的是在如何進行領域設計廊遍,而不是在框架和技術上面,有時候就算是三層也能配和著實現(xiàn)領域設計贩挣,之前有小伙伴說到我些的是OOP喉前,嗯没酣,希望等系列寫完就可以稍微不一樣一些吧。
今天我們的主要工作卵迂,就是把前幾天在講述概念的同時裕便,對搭建的項目進行第一次的合圍,能運行起來见咒,當然這里還會涉及到之前我們第一個系列的知識偿衰,我們也進行復習下,比如:DI依賴注入改览、EFCore下翎、Automapper數(shù)據(jù)傳輸對象,當然還有前幾篇文章中的 實體和值對象的部分概念 宝当, 如果您是第一次看我的文章视事,可能這些今天不會詳細說明,可以去我的第一個系列開始學習庆揩,好啦俐东,馬上開始今天的講解。
零盾鳞、今天實現(xiàn)天青色的部分
一犬性、項目運行、復習系列一相關知識
1腾仅、Automapper定義Config配置文件
1乒裆、我們在項目應用層Christ3D.Application 的 AutoMapper 文件夾下,新建AutoMapperConfig.cs 配置文件推励,
/// <summary>
/// 靜態(tài)全局 AutoMapper 配置文件 /// </summary>
public class AutoMapperConfig
{ public static MapperConfiguration RegisterMappings()
{ //創(chuàng)建AutoMapperConfiguration, 提供靜態(tài)方法Configure鹤耍,一次加載所有層中Profile定義 //MapperConfiguration實例可以靜態(tài)存儲在一個靜態(tài)字段中,也可以存儲在一個依賴注入容器中验辞。 一旦創(chuàng)建稿黄,不能更改/修改。
return new MapperConfiguration(cfg => { //這個是領域模型 -> 視圖模型的映射跌造,是 讀命令
cfg.AddProfile(new DomainToViewModelMappingProfile()); //這里是視圖模型 -> 領域模式的映射杆怕,是 寫 命令
cfg.AddProfile(new ViewModelToDomainMappingProfile());
});
}
}
這里你可能會問了,咱們之前在 Blog.Core 前后端分離中壳贪,為什么沒有配置這個Config文件陵珍,其實我實驗了下,不用配置文件我們也可以達到映射的目的违施,只不過互纯,我們平時映射文件Profile 比較少,項目啟動的時候磕蒲,每次都會調(diào)取下這個配置文件留潦,你可以實驗下只盹,如果幾十個表,上百個數(shù)據(jù)庫表兔院,啟動會比較慢殖卑,可以使用創(chuàng)建AutoMapperConfiguration, 提供靜態(tài)方法Configure,一次加載所有層中Profile定義秆乳,大概就是這個意思懦鼠,這里我先存?zhèn)€疑,有不同意見的歡迎來說我屹堰,哈哈歡迎批評肛冶。
2、上邊代碼中 DomainToViewModelMappingProfile 咱們很熟悉扯键,就是平時用到的睦袖,但是下邊的那個是什么呢,那個就是我們 視圖模型 -> 領域模式 的時候的映射荣刑,寫法和反著的是一樣的馅笙,你一定會說,那為啥不直接這么寫呢厉亏,
你的想法很棒董习!這種平時也是可以的,只不過在DDD領域驅(qū)動設計中爱只,這個是是視圖模型轉(zhuǎn)領域模型皿淋,那一定是對領域模型就行命令操作,沒錯恬试,就是在領域命令中窝趣,會用到這里,所以兩者不能直接寫在一起训柴,這個以后馬上會在下幾篇文章中說到哑舒。
3、將 AutoMapper 服務在 Startup 啟動
在 Christ3D.UI.Web 項目下幻馁,新建 Extensions 擴展文件夾洗鸵,以后我們的擴展啟動服務都寫在這里。
新建 AutoMapperSetup.cs
/// <summary>
/// AutoMapper 的啟動服務 /// </summary>
public static class AutoMapperSetup
{ public static void AddAutoMapperSetup(this IServiceCollection services)
{ if (services == null) throw new ArgumentNullException(nameof(services)); //添加服務
services.AddAutoMapper(); //啟動配置
AutoMapperConfig.RegisterMappings();
}
}
2仗嗦、依賴注入 DI
之前我們在上個系列中预麸,是用的Aufac 將整個層注入,今天咱們換個方法儒将,其實之前也有小伙伴提到了,微軟自帶的 依賴注入方法就可以对蒲。
因為這一塊屬于我們開發(fā)的基礎钩蚊,而且也與數(shù)據(jù)有關贡翘,所以我們就新建一個 IoC 層,來進行統(tǒng)一注入
1砰逻、新建 Christ3D.Infra.IoC 層鸣驱,添加統(tǒng)一注入類 NativeInjectorBootStrapper.cs
public static void RegisterServices(IServiceCollection services)
{ // 注入 Application 應用層
services.AddScoped<IStudentAppService, StudentAppService>(); // 注入 Infra - Data 基礎設施數(shù)據(jù)層
services.AddScoped<IStudentRepository, StudentRepository>();
services.AddScoped<StudyContext>();//上下文
}
具體的使用方法和我們Autofac很類型,這里就不說了蝠咆,相信大家已經(jīng)很了解依賴注入了踊东。
2、在ConfigureServices 中進行服務注入
// .NET Core 原生依賴注入 // 單寫一層用來添加依賴項刚操,可以將IoC與展示層 Presentation 隔離
NativeInjectorBootStrapper.RegisterServices(services);
3闸翅、EFCore Code First
1、相信大家也都用過EF菊霜,這里的EFCore 也是一樣的坚冀,如果我們想要使用 CodeFirst 功能的話,就可以直接對其進行配置鉴逞,
public class StudyContext : DbContext
{ public DbSet<Student> Students { get; set; } /// <summary>
/// 重寫自定義Map配置 /// </summary>
/// <param name="modelBuilder"></param>
protected override void OnModelCreating(ModelBuilder modelBuilder)
{ //對 StudentMap 進行配置
modelBuilder.ApplyConfiguration(new StudentMap()); base.OnModelCreating(modelBuilder);
} /// <summary>
/// 重寫連接數(shù)據(jù)庫 /// </summary>
/// <param name="optionsBuilder"></param>
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{ // 從 appsetting.json 中獲取配置信息
var config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json")
.Build(); //定義要使用的數(shù)據(jù)庫 //正確的是這樣记某,直接連接字符串即可 //optionsBuilder.UseSqlServer(config.GetConnectionString("DefaultConnection")); //我是讀取的文件內(nèi)容,為了數(shù)據(jù)安全
optionsBuilder.UseSqlServer(File.ReadAllText(config.GetConnectionString("DefaultConnection")));
}
}
2构捡、然后我們就可以配置 StudentMap 了液南,針對不同的領域模型進行配置,但是這里有一個重要的知識點勾徽,請往下看:
/// <summary>
/// 學生map類 /// </summary>
public class StudentMap : IEntityTypeConfiguration<Student> { /// <summary>
/// 實體屬性配置 /// </summary>
/// <param name="builder"></param>
public void Configure(EntityTypeBuilder<Student> builder)
{ //實體屬性Map
builder.Property(c => c.Id)
.HasColumnName("Id");
builder.Property(c => c.Name)
.HasColumnType("varchar(100)")
.HasMaxLength(100)
.IsRequired();
builder.Property(c => c.Email)
.HasColumnType("varchar(100)")
.HasMaxLength(11)
.IsRequired();
builder.Property(c => c.Phone)
.HasColumnType("varchar(100)")
.HasMaxLength(20)
.IsRequired(); //處理值對象配置滑凉,否則會被視為實體
builder.OwnsOne(p => p.Address); //可以對值對象進行數(shù)據(jù)庫重命名,還有其他的一些操作捂蕴,請參考官網(wǎng) //builder.OwnsOne( // o => o.Address, // sa => // { // sa.Property(p => p.County).HasColumnName("County"); // sa.Property(p => p.Province).HasColumnName("Province"); // sa.Property(p => p.City).HasColumnName("City"); // sa.Property(p => p.Street).HasColumnName("Street"); // } //); //注意:這是EF版本的寫法譬涡,Core中不能使用!I侗妗涡匀! //builder.Property(c => c.Address.City) // .HasColumnName("City") // .HasMaxLength(20); //builder.Property(c => c.Address.Street) // .HasColumnName("Street") // .HasMaxLength(20); //如果想忽略當前值對象,可直接 Ignore //builder.Ignore(c => c.Address);
}
}
重要知識點:
我們以前用的時候溉知,都是每一個實體對應一個數(shù)據(jù)庫表陨瘩,或者有一些關聯(lián),比如一對多的情況级乍,就拿我們現(xiàn)在項目中使用到的來說舌劳,我們的 Student 實體中,有一個 Address 的值對象玫荣,值對象大家肯定都知道的甚淡,是沒有狀態(tài),保證不變性的一個值捅厂,但是在EFCore 的Code First 中贯卦,系統(tǒng)會需要我們提供一個 Address 的主鍵资柔,因為它會認為這是一個表結(jié)構(gòu),如果我們?yōu)?Address 添加主鍵撵割,那就是定義成了實體贿堰,這個完全不是我們想要的,我們設計的原則是一切以領域設計為核心啡彬,不能為了數(shù)據(jù)庫而修改模型羹与。
如果把 Address 當一個實體,增加主鍵庶灿,就可以Code First通過纵搁,但是這個對我們來說是不行的,我們是從領域設計中考慮跳仿,需要把它作為值對象诡渴,是作為數(shù)據(jù)庫字段,你也許會想著直接把 Address 拆開成多個字段放到 Student 實體類中作為屬性菲语,我感覺這樣也是不好的妄辩,這樣就達不到我們領域模型的作用了。
我通過收集資料山上,我發(fā)現(xiàn)可以用上邊注釋的方法眼耀,直接在 StudentMap 中配置,但是我失敗了佩憾,一直報錯
//builder.Property(c => c.Address.City)
// .HasColumnName("City")
// .HasMaxLength(20);The property 'Student.Address' is of type 'Address' which is not supported by current database provider. Either change the property CLR type or ignore the property using the '[NotMapped]' attribute or by using 'EntityTypeBuilder.Ignore' in 'OnModelCreating'.
本來想放棄的時候哮伟,還是強大的博客園博文功能,讓我找到一個大神妄帘,然后我參考官網(wǎng)楞黄,找到了這個方法。https://docs.microsoft.com/en-us/ef/core/modeling/owned-entities
builder.OwnsOne(p => p.Address);//記得在 Address 值對象上增加一個 [Owned] 特性抡驼。
3鬼廓、Code First 到數(shù)據(jù)庫
我們可以通過以下nuget 命令來控制,這里就不細說了致盟,相信大家用的很多了
//1碎税、初始化遷移記錄 Init 自定義
Add-Migration Init //2、將當前 Init 的遷移記錄更新到數(shù)據(jù)庫
update-database Init
然后就可以看到我們的的數(shù)據(jù)庫已經(jīng)生成:
4馏锡、添加頁面雷蹂,運行
1、到這里我們就已經(jīng)把整體調(diào)通了杯道,然后新建 StudentController.cs 匪煌,添加 CURD 頁面
//還是構(gòu)造函數(shù)注入
private readonly IStudentAppService _studentAppService; public StudentController(IStudentAppService studentAppService)
{
_studentAppService = studentAppService;
} // GET: Student
public ActionResult Index()
{ return View(_studentAppService.GetAll());
}
2、運行項目,就能看到結(jié)果
這個時候虐杯,我們已經(jīng)通過了 DI 進行注入玛歌,然后通過Dtos 將我們的領域模型,轉(zhuǎn)換成了視圖模型擎椰,進行展示,也許這個時候你會發(fā)現(xiàn)创肥,這個很正常呀达舒,平時都是這么做的,也沒有看到有什么高端的地方叹侄,聰明的你一定會想到更遠的地方巩搏,這里我們是用領域模型 -> 視圖模型的DTO,也就是我們平時說的查詢模式趾代,
那有查詢贯底,肯定有編輯模式,我們就會有 視圖模型撒强,傳入禽捆,然后轉(zhuǎn)換領域模型,中間當然還有校驗等等(不是簡單的視圖模型的判空飘哨,還有其他的復雜校驗胚想,比如年齡,字符串)芽隆,這個時候浊服,如果我們直接用 視圖模型 -> 領域模型的話,肯定會有污染胚吁,至少會把讀和寫混合在一起牙躺,
public void Register(StudentViewModel StudentViewModel)
{ //這里引入領域設計中的寫命令 還沒有實現(xiàn) //請注意這里如果是平時的寫法,必須要引入Student領域模型腕扶,會造成污染
_StudentRepository.Add(_mapper.Map<Student>(StudentViewModel));
}
那該怎么辦呢孽拷,這個時候CQRS 就登場了!請往下看蕉毯。
二乓搬、CQRS 讀寫分離初探
從上邊的問題中,我們發(fā)現(xiàn)代虾,在DDD領域驅(qū)動設計中进肯,我們是一起以領域模型為核心的,這個時候出現(xiàn)了幾個概念:
1棉磨、DDD中四種模型
如果你是從我的系列的第一篇開始讀江掩,你應該已經(jīng)對這兩個模型很熟悉了,領域模型,視圖模型环形,當然策泣,還有咱們一直開發(fā)中使用到的數(shù)據(jù)模型,那第四個是什么呢抬吟?
- 數(shù)據(jù)模型:面向持久化萨咕,數(shù)據(jù)的載體。
- 領域模型:面向業(yè)務火本,行為的載體危队。
- 視圖模型:面向UI(向外),數(shù)據(jù)的載體钙畔。
- 命令模型:面向UI(向內(nèi))茫陆,數(shù)據(jù)的載體。
這個命令模型Command擎析,就是解決了我們的 視圖模型到領域模型中簿盅,出現(xiàn)污染的問題。其他 命令模型揍魂,就和我們的領域模型桨醋、視圖模型是一樣的,也是一個數(shù)據(jù)載體愉烙,這不過它可以配和著事件讨盒,進行復雜的操作控制,這個以后會慢慢說到步责。
如果你要問寫到哪里返顺,這里簡單說一下,具體的搭建下次會說到蔓肯,就是在我們的 應用層 AutoMapper 文件夾下遂鹊,我們的 ViewModelToDomainMappingProfile.cs
public class ViewModelToDomainMappingProfile : Profile
{ public ViewModelToDomainMappingProfile()
{ //這里以后會寫領域命令,所以不能和DomainToViewModelMappingProfile寫在一起蔗包。 //學生視圖模型 -> 添加新學生命令模型
CreateMap<StudentViewModel, RegisterNewStudentCommand>()
.ConstructUsing(c => new RegisterNewStudentCommand(c.Name, c.Email, c.BirthDate)); //學生視圖模型 -> 更新學生信息命令模型
CreateMap<StudentViewModel, UpdateStudentCommand>()
.ConstructUsing(c => new UpdateStudentCommand(c.Id, c.Name, c.Email, c.BirthDate));
}
2秉扑、傳統(tǒng) CURD 命令有哪些問題
1、使用同一個對象實體來進行數(shù)據(jù)庫讀寫可能會太粗糙调限,大多數(shù)情況下舟陆,比如編輯的時候可能只需要更新個別字段,但是卻需要將整個對象都穿進去耻矮,有些字段其實是不需要更新的秦躯。在查詢的時候在表現(xiàn)層可能只需要個別字段,但是需要查詢和返回整個實體對象裆装。
2踱承、使用同一實體對象對同一數(shù)據(jù)進行讀寫操作的時候倡缠,可能會遇到資源競爭的情況,經(jīng)常要處理的鎖的問題茎活,在寫入數(shù)據(jù)的時候昙沦,需要加鎖。讀取數(shù)據(jù)的時候需要判斷是否允許臟讀载荔。這樣使得系統(tǒng)的邏輯性和復雜性增加盾饮,并且會對系統(tǒng)吞吐量的增長會產(chǎn)生影響。
3身辨、同步的丐谋,直接與數(shù)據(jù)庫進行交互在大數(shù)據(jù)量同時訪問的情況下可能會影響性能和響應性,并且可能會產(chǎn)生性能瓶頸煌珊。
4、由于同一實體對象都會在讀寫操作中用到泌豆,所以對于安全和權(quán)限的管理會變得比較復雜定庵。
這里面很重要的一個問題是,系統(tǒng)中的讀寫頻率比踪危,是偏向讀蔬浙,還是偏向?qū)懀腿缤话愕臄?shù)據(jù)結(jié)構(gòu)在查找和修改上時間復雜度不一樣贞远,在設計系統(tǒng)的結(jié)構(gòu)時也需要考慮這樣的問題畴博。解決方法就是我們經(jīng)常用到的對數(shù)據(jù)庫進行讀寫分離。 讓主數(shù)據(jù)庫處理事務性的增蓝仲,刪俱病,改操作(Insert,Update,Delete)操作,讓從數(shù)據(jù)庫處理查詢操作(Select操作)袱结,數(shù)據(jù)庫復制被用來將事務性操作導致的變更同步到集群中的從數(shù)據(jù)庫亮隙。這只是從DB角度處理了讀寫分離,但是從業(yè)務或者系統(tǒng)上面讀和寫仍然是存放在一起的垢夹。他們都是用的同一個實體對象溢吻。
要從業(yè)務上將讀和寫分離,就是接下來要介紹的命令查詢職責分離模式果元。
3促王、什么是 CQRS 讀寫分離
以下信息來自@寒江獨釣的博文,我看著寫的很好:
CQRS最早來自于Betrand Meyer(Eiffel語言之父而晒,開-閉原則OCP提出者)提到的一種 命令查詢分離 (Command Query Separation,CQS) 的概念蝇狼。其基本思想在于,任何一個對象的方法可以分為兩大類:
- 命令(Command):不返回任何結(jié)果(void)欣硼,但會改變對象的狀態(tài)题翰。
- 查詢(Query):返回結(jié)果恶阴,但是不會改變對象的狀態(tài),對系統(tǒng)沒有副作用豹障。
根據(jù)CQS的思想冯事,任何一個方法都可以拆分為命令和查詢兩部分,比如:
public StudentViewModel Update(StudentViewModel StudentViewModel)
{ //更新操作
_StudentRepository.Update(_mapper.Map<Student>(StudentViewModel)); //查詢操作
return _mapper.Map<StudentViewModel>(_StudentRepository.GetById(StudentViewModel.Id));
}
這個方法血公,我們執(zhí)行了一個命令即對更新Student昵仅,同時又執(zhí)行了一個Query,即查詢返回了Student的值累魔,如果按照CQS的思想摔笤,該方法可以拆成Command和Query兩個方法,如下:
public StudentViewModel GetById(Guid id)
{ return _mapper.Map<StudentViewModel>(_StudentRepository.GetById(id));
} public void Update(StudentViewModel StudentViewModel)
{
_StudentRepository.Update(_mapper.Map<Student>(StudentViewModel));
}
操作和查詢分離使得我們能夠更好的把握對象的細節(jié)垦写,能夠更好的理解哪些操作會改變系統(tǒng)的狀態(tài)吕世。當然CQS也有一些缺點,比如代碼需要處理多線程的情況梯投。
CQRS是對CQS模式的進一步改進成的一種簡單模式命辖。 它由Greg Young在CQRS, Task Based UIs, Event Sourcing agh! 這篇文章中提出》直停“CQRS只是簡單的將之前只需要創(chuàng)建一個對象拆分成了兩個對象尔艇,這種分離是基于方法是執(zhí)行命令還是執(zhí)行查詢這一原則來定的(這個和CQS的定義一致)”。
CQRS使用分離的接口將數(shù)據(jù)查詢操作(Queries)和數(shù)據(jù)修改操作(Commands)分離開來么鹤,這也意味著在查詢和更新過程中使用的數(shù)據(jù)模型也是不一樣的终娃。這樣讀和寫邏輯就隔離開來了。
使用CQRS分離了讀寫職責之后蒸甜,可以對數(shù)據(jù)進行讀寫分離操作來改進性能棠耕,可擴展性和安全。如下圖:
4迅皇、CQRS 的應用場景
在下場景中昧辽,可以考慮使用CQRS模式:
- 當在業(yè)務邏輯層有很多操作需要相同的實體或者對象進行操作的時候。CQRS使得我們可以對讀和寫定義不同的實體和方法登颓,從而可以減少或者避免對某一方面的更改造成沖突搅荞;
- 對于一些基于任務的用戶交互系統(tǒng),通常這類系統(tǒng)會引導用戶通過一系列復雜的步驟和操作框咙,通常會需要一些復雜的領域模型咕痛,并且整個團隊已經(jīng)熟悉領域驅(qū)動設計技術。寫模型有很多和業(yè)務邏輯相關的命令操作的堆喇嘱,輸入驗證茉贡,業(yè)務邏輯驗證來保證數(shù)據(jù)的一致性。讀模型沒有業(yè)務邏輯以及驗證堆者铜,僅僅是返回DTO對象為視圖模型提供數(shù)據(jù)腔丧。讀模型最終和寫模型相一致放椰。
- 適用于一些需要對查詢性能和寫入性能分開進行優(yōu)化的系統(tǒng),尤其是讀/寫比非常高的系統(tǒng)愉粤,橫向擴展是必須的砾医。比如,在很多系統(tǒng)中讀操作的請求時遠大于寫操作衣厘。為適應這種場景如蚜,可以考慮將寫模型抽離出來單獨擴展,而將寫模型運行在一個或者少數(shù)幾個實例上影暴。少量的寫模型實例能夠減少合并沖突發(fā)生的情況
- 適用于一些團隊中错邦,一些有經(jīng)驗的開發(fā)者可以關注復雜的領域模型,這些用到寫操作型宙,而另一些經(jīng)驗較少的開發(fā)者可以關注用戶界面上的讀模型撬呢。
- 對于系統(tǒng)在將來會隨著時間不段演化,有可能會包含不同版本的模型妆兑,或者業(yè)務規(guī)則經(jīng)常變化的系統(tǒng)
- 需要和其他系統(tǒng)整合倾芝,特別是需要和事件溯源Event Sourcing進行整合的系統(tǒng),這樣子系統(tǒng)的臨時異常不會影響整個系統(tǒng)的其他部分箭跳。
這里我只是把CQRS的初衷簡單說了一下,下一節(jié)我們會重點來講解 讀寫分離 的過程潭千,以及命令是怎么配合著 Validations 進行驗證的谱姓。
三、結(jié)語
今天暫時就寫到這里吧刨晴,通過今天的學習屉来,我們復習了第一系列中的依賴注入DI、DTO數(shù)據(jù)傳輸對象以及EFCore 的相關操作狈癞,重點說明了下茄靠,我們在DDD領域驅(qū)動設計中,如何在領域?qū)嶓w和值對象中蝶桶,通過Code First生成數(shù)據(jù)庫慨绳,并且強調(diào)了在領域設計中,一切要以領域模型為核心真竖。最后簡單引入了 CQRS 讀寫分離模式的簡單概念脐雪,我會在下一節(jié)繼續(xù)深入對其進行研究。