哈嘍你們週五好,咱們又見面了,感謝你們在這個週五讀個人文章,通過了三週的時間,固然每週兩篇的速度的狀況下,我們簡單說了下DDD領域驅動設計的第一部分,主要包括了,《項目入門DDD架構淺析》,《領域、子領域、限界上下文》,《DDD使用意義》,《實體與值對象》,《聚合與聚合根》這五部份內容,主要的是以解釋爲主,舉例子Code爲輔的形式,整體來講仍是獲得一些確定的,也是我最大的動力了。html
上邊這五個知識點是DDD領域驅動設計的第一部分 —— D領域;git
從今天開始,我們就說說DDD的第二個D,就是領域服務+領域命令的CQRS,這些偏重動做的一部分;github
最後就是第三部分,經過 領域事件、事件源與事件回溯,配合着權限管理,再統一說一下DDD,這一系列就是結束了。web
其實經過我看到這裏,我發現了,咱們在設計DDD的時候,重要的是思路,重要的是在如何進行領域設計,而不是在框架和技術上面,有時候就算是三層也能配和着實現領域設計,以前有小夥伴說到我些的是OOP,嗯,但願等系列寫完就能夠稍微不同一些吧。數據庫
今天咱們的主要工做,就是把前幾天在講述概念的同時,對搭建的項目進行第一次的合圍,能運行起來,固然這裏還會涉及到以前咱們第一個系列的知識,咱們也進行復習下,好比:DI依賴注入、EFCore、Automapper數據傳輸對象,固然還有前幾篇文章中的 實體和值對象的部分概念 , 若是您是第一次看個人文章,可能這些今天不會詳細說明,能夠去個人第一個系列開始學習,好啦,立刻開始今天的講解。json
一、咱們在項目應用層Christ3D.Application 的 AutoMapper 文件夾下,新建AutoMapperConfig.cs 配置文件,後端
/// <summary> /// 靜態全局 AutoMapper 配置文件 /// </summary> public class AutoMapperConfig { public static MapperConfiguration RegisterMappings() { //建立AutoMapperConfiguration, 提供靜態方法Configure,一次加載全部層中Profile定義 //MapperConfiguration實例能夠靜態存儲在一個靜態字段中,也能夠存儲在一個依賴注入容器中。 一旦建立,不能更改/修改。 return new MapperConfiguration(cfg => { //這個是領域模型 -> 視圖模型的映射,是 讀命令 cfg.AddProfile(new DomainToViewModelMappingProfile()); //這裏是視圖模型 -> 領域模式的映射,是 寫 命令 cfg.AddProfile(new ViewModelToDomainMappingProfile()); }); } }
這裏你可能會問了,我們以前在 Blog.Core 先後端分離中,爲何沒有配置這個Config文件,其實我實驗了下,不用配置文件咱們也能夠達到映射的目的,只不過,咱們平時映射文件Profile 比較少,項目啓動的時候,每次都會調取下這個配置文件,你能夠實驗下,若是幾十個表,上百個數據庫表,啓動會比較慢,可使用建立AutoMapperConfiguration, 提供靜態方法Configure,一次加載全部層中Profile定義,大概就是這個意思,這裏我先存個疑,有不一樣意見的歡迎來講我,哈哈歡迎批評。安全
二、上邊代碼中 DomainToViewModelMappingProfile 我們很熟悉,就是平時用到的,可是下邊的那個是什麼呢,那個就是咱們 視圖模型 -> 領域模式 的時候的映射,寫法和反着的是同樣的,你必定會說,那爲啥不直接這麼寫呢,數據結構
你的想法很棒!這種平時也是能夠的,只不過在DDD領域驅動設計中,這個是是視圖模型轉領域模型,那必定是對領域模型就行命令操做,沒錯,就是在領域命令中,會用到這裏,因此二者不能直接寫在一塊兒,這個之後立刻會在下幾篇文章中說到。多線程
三、將 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(); } }
以前咱們在上個系列中,是用的Aufac 將整個層注入,今天我們換個方法,其實以前也有小夥伴提到了,微軟自帶的 依賴注入方法就能夠。
由於這一塊屬於咱們開發的基礎,並且也與數據有關,因此咱們就新建一個 IoC 層,來進行統一注入
一、新建 Christ3D.Infra.IoC 層,添加統一注入類 NativeInjectorBootStrapper.cs
更新:已經把該注入文件統一放到了web層:
public static void RegisterServices(IServiceCollection services) { // 注入 Application 應用層 services.AddScoped<IStudentAppService, StudentAppService>(); // 注入 Infra - Data 基礎設施數據層 services.AddScoped<IStudentRepository, StudentRepository>(); services.AddScoped<StudyContext>();//上下文 }
具體的使用方法和咱們Autofac很類型,這裏就不說了,相信你們已經很瞭解依賴注入了。
二、在ConfigureServices 中進行服務注入
// .NET Core 原生依賴注入 // 單寫一層用來添加依賴項,能夠將IoC與展現層 Presentation 隔離 NativeInjectorBootStrapper.RegisterServices(services);
一、相信你們也都用過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> /// 重寫鏈接數據庫 /// </summary> /// <param name="optionsBuilder"></param> protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { // 從 appsetting.json 中獲取配置信息 var config = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json") .Build(); //定義要使用的數據庫 //正確的是這樣,直接鏈接字符串便可 //optionsBuilder.UseSqlServer(config.GetConnectionString("DefaultConnection")); //我是讀取的文件內容,爲了數據安全 optionsBuilder.UseSqlServer(File.ReadAllText(config.GetConnectionString("DefaultConnection"))); } }
二、而後咱們就能夠配置 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); //能夠對值對象進行數據庫重命名,還有其餘的一些操做,請參考官網 //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中不能使用!!! //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); } }
重要知識點:
咱們之前用的時候,都是每個實體對應一個數據庫表,或者有一些關聯,好比一對多的狀況,就拿咱們如今項目中使用到的來講,咱們的 Student 實體中,有一個 Address 的值對象,值對象你們確定都知道的,是沒有狀態,保證不變性的一個值,可是在EFCore 的Code First 中,系統會須要咱們提供一個 Address 的主鍵,由於它會認爲這是一個表結構,若是咱們爲 Address 添加主鍵,那就是定義成了實體,這個徹底不是咱們想要的,咱們設計的原則是一切以領域設計爲核心,不能爲了數據庫而修改模型。
若是把 Address 當一個實體,增長主鍵,就能夠Code First經過,可是這個對咱們來講是不行的,咱們是從領域設計中考慮,須要把它做爲值對象,是做爲數據庫字段,你也許會想着直接把 Address 拆開成多個字段放到 Student 實體類中做爲屬性,我感受這樣也是很差的,這樣就達不到咱們領域模型的做用了。
我經過收集資料,我發現能夠用上邊註釋的方法,直接在 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'.
原本想放棄的時候,仍是強大的博客園博文功能,讓我找到一個大神,而後我參考官網,找到了這個方法。https://docs.microsoft.com/en-us/ef/core/modeling/owned-entities
builder.OwnsOne(p => p.Address);//記得在 Address 值對象上增長一個 [Owned] 特性。
三、Code First 到數據庫
咱們能夠經過如下nuget 命令來控制,這裏就不細說了,相信你們用的不少了
//一、初始化遷移記錄 Init 自定義 Add-Migration Init //二、將當前 Init 的遷移記錄更新到數據庫 update-database Init
而後就能夠看到咱們的的數據庫已經生成:
之後你們在遷移數據庫的時候,可能會遇到問題,由於本項目有兩個上下文,你們能夠指定其中的操做
一、到這裏咱們就已經把總體調通了,而後新建 StudentController.cs ,添加 CURD 頁面
//仍是構造函數注入 private readonly IStudentAppService _studentAppService; public StudentController(IStudentAppService studentAppService) { _studentAppService = studentAppService; } // GET: Student public ActionResult Index() { return View(_studentAppService.GetAll()); }
二、運行項目,就能看到結果
這個時候,咱們已經經過了 DI 進行注入,而後經過Dtos 將咱們的領域模型,轉換成了視圖模型,進行展現,也許這個時候你會發現,這個很正常呀,平時都是這麼作的,也沒有看到有什麼高端的地方,聰明的你必定會想到更遠的地方,這裏咱們是用領域模型 -> 視圖模型的DTO,也就是咱們平時說的查詢模式,
那有查詢,確定有編輯模式,咱們就會有 視圖模型,傳入,而後轉換領域模型,中間固然還有校驗等等(不是簡單的視圖模型的判空,還有其餘的複雜校驗,好比年齡,字符串),這個時候,若是咱們直接用 視圖模型 -> 領域模型的話,確定會有污染,至少會把讀和寫混合在一塊兒,
public void Register(StudentViewModel StudentViewModel) { //這裏引入領域設計中的寫命令 尚未實現 //請注意這裏若是是平時的寫法,必需要引入Student領域模型,會形成污染 _StudentRepository.Add(_mapper.Map<Student>(StudentViewModel)); }
那該怎麼辦呢,這個時候CQRS 就登場了!請往下看。
從上邊的問題中,咱們發現,在DDD領域驅動設計中,咱們是一塊兒以領域模型爲核心的,這個時候出現了幾個概念:
若是你是從個人系列的第一篇開始讀,你應該已經對這兩個模型很熟悉了,領域模型,視圖模型,固然,還有我們一直開發中使用到的數據模型,那第四個是什麼呢?
這個命令模型Command,就是解決了咱們的 視圖模型到領域模型中,出現污染的問題。其餘 命令模型,就和咱們的領域模型、視圖模型是同樣的,也是一個數據載體,這不過它能夠配和着事件,進行復雜的操做控制,這個之後會慢慢說到。
若是你要問寫到哪裏,這裏簡單說一下,具體的搭建下次會說到,就是在咱們的 應用層 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)); }
一、使用同一個對象實體來進行數據庫讀寫可能會太粗糙,大多數狀況下,好比編輯的時候可能只須要更新個別字段,可是卻須要將整個對象都穿進去,有些字段實際上是不須要更新的。在查詢的時候在表現層可能只須要個別字段,可是須要查詢和返回整個實體對象。
二、使用同一實體對象對同一數據進行讀寫操做的時候,可能會遇到資源競爭的狀況,常常要處理的鎖的問題,在寫入數據的時候,須要加鎖。讀取數據的時候須要判斷是否容許髒讀。這樣使得系統的邏輯性和複雜性增長,而且會對系統吞吐量的增加會產生影響。
三、同步的,直接與數據庫進行交互在大數據量同時訪問的狀況下可能會影響性能和響應性,而且可能會產生性能瓶頸。
四、因爲同一實體對象都會在讀寫操做中用到,因此對於安全和權限的管理會變得比較複雜。
這裏面很重要的一個問題是,系統中的讀寫頻率比,是偏向讀,仍是偏向寫,就如同通常的數據結構在查找和修改上時間複雜度不同,在設計系統的結構時也須要考慮這樣的問題。解決方法就是咱們常常用到的對數據庫進行讀寫分離。 讓主數據庫處理事務性的增,刪,改操做(Insert,Update,Delete)操做,讓從數據庫處理查詢操做(Select操做),數據庫複製被用來將事務性操做致使的變動同步到集羣中的從數據庫。這只是從DB角度處理了讀寫分離,可是從業務或者系統上面讀和寫仍然是存放在一塊兒的。他們都是用的同一個實體對象。
要從業務上將讀和寫分離,就是接下來要介紹的命令查詢職責分離模式。
如下信息來自@寒江獨釣的博文,我看着寫的很好:
CQRS最先來自於Betrand Meyer(Eiffel語言之父,開-閉原則OCP提出者)提到的一種 命令查詢分離 (Command Query Separation,CQS) 的概念。其基本思想在於,任何一個對象的方法能夠分爲兩大類:
根據CQS的思想,任何一個方法均可以拆分爲命令和查詢兩部分,好比:
public StudentViewModel Update(StudentViewModel StudentViewModel) { //更新操做 _StudentRepository.Update(_mapper.Map<Student>(StudentViewModel)); //查詢操做 return _mapper.Map<StudentViewModel>(_StudentRepository.GetById(StudentViewModel.Id)); }
這個方法,咱們執行了一個命令即對更新Student,同時又執行了一個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)); }
操做和查詢分離使得咱們可以更好的把握對象的細節,可以更好的理解哪些操做會改變系統的狀態。固然CQS也有一些缺點,好比代碼須要處理多線程的狀況。
CQRS是對CQS模式的進一步改進成的一種簡單模式。 它由Greg Young在CQRS, Task Based UIs, Event Sourcing agh! 這篇文章中提出。「CQRS只是簡單的將以前只須要建立一個對象拆分紅了兩個對象,這種分離是基於方法是執行命令仍是執行查詢這一原則來定的(這個和CQS的定義一致)」。
CQRS使用分離的接口將數據查詢操做(Queries)和數據修改操做(Commands)分離開來,這也意味着在查詢和更新過程當中使用的數據模型也是不同的。這樣讀和寫邏輯就隔離開來了。
使用CQRS分離了讀寫職責以後,能夠對數據進行讀寫分離操做來改進性能,可擴展性和安全。以下圖:
在下場景中,能夠考慮使用CQRS模式:
這裏我只是把CQRS的初衷簡單說了一下,下一節咱們會重點來說解 讀寫分離 的過程,以及命令是怎麼配合着 Validations 進行驗證的。
今天暫時就寫到這裏吧,經過今天的學習,咱們複習了第一系列中的依賴注入DI、DTO數據傳輸對象以及EFCore 的相關操做,重點說明了下,咱們在DDD領域驅動設計中,如何在領域實體和值對象中,經過Code First生成數據庫,而且強調了在領域設計中,一切要以領域模型爲核心。最後簡單引入了 CQRS 讀寫分離模式的簡單概念,我會在下一節繼續深刻對其進行研究。