從壹開始微服務 [ DDD ] 之終篇 ║當事件溯源 遇上 粉絲活動
回首
哈嘍~大家好,時間過的真快,關於DDD領域驅動設計的講解基本就差不多了,本來想著週四再開一篇,感覺沒有太多的內容了,剩下的一個就是驗證的問題,就和之前的JWT很類似,就不開啟一個章節了,而且這個也不是領域驅動設計範疇之內的,下一個系列 Ids 的講解中,可能會穿插著講一講,然後到時候正好一起完善了。
雖然是完結了,不過心裡還是不是很開森呀,通過小夥伴的反饋,然後我也諮詢了官方的建議,好像這個DDD領域驅動設計系列,並沒有得到很多的支援,影響力完全比不過第一個系列《從壹開始前後端分離》,原因可能是,我也沒有在專案中真正的使用過DDD的原因吧,也或許是寫的比較生硬,主要我也一直在研究,不過我這裡一定要說一下,還是要多看看的,不一定要看我講的,可以看看書也行,或者看看別人的部落格,DDD領域驅動設計思想真的很不錯,然後還夾帶著CQRS命令查詢職責分離、Bus匯流排思想、EDA事件驅動思想、ES事件溯源思想(今天要說到的)、訊息佇列等等這些以前沒有接觸到的思想設計,也為微服務打下了一定的基礎,如果沒有這些基礎,你是很難理解為什麼要使用微服務的,這裡我們就先來回顧一下這些天我們都說了什麼內容吧:
- ofollow,noindex" target="_blank">我們第一次開始討論DDD領域驅動設的概念已經我的個人計劃書
- DDD入門 & 專案整體的第一次搭建
- 領域、子域、限界上下文
- 又一次討論了DDD設計思想的重大意義 以及使用EFCore
- 實體 與 值物件
- 聚合 與 聚合根
- 第一次把專案跑通,然後也簡單說了下CQRS
- 剪不斷理還亂的 值物件和Dto
-
明白領域驗證
FluentValidator
- 命令匯流排Bus 分發(一)
- 事件驅動EDA 詳解
算上今天的內容,正好是十二篇,也是我的比較喜歡的一個數字(之前在文章中說到過這個原因,這裡就不多說了),也是很辛苦寫了這麼多,希望有時間有精力的時候,還是要多看看的,多品品思想,這樣我們就不會一直問一些虛無縹緲的問題了,雖然我現在是越學的多,越不會的多:joy:。
可能你也發現了今天的題目有些不一樣,因為我之前說過,要在聖誕節簡單搞個小活動,既然說了,就不能食言,不過目前我寫了十六萬字了,就一個小夥伴給了我一塊錢 紅包:joy:,好失敗,所以我就簡單來個小福利吧,因為這個系列的名字就是Christ3D,當時就是想著在聖誕節前能說完,還可以,緊趕慢趕的說完了,我就想著一個給粉絲一個小小小福利:
具體的參與形式看文章末尾:傳送門
1、免費給送三本書,可能是《實現領域驅動設計 》這本書,或者《領域驅動設計 軟體核心複雜性應對之道 》;
2、本來想抽十位粉絲,送精裝的聖誕節蘋果,但是考慮食品安全問題就算了,直接到時候發紅包吧(時間地點保密,提示:為了老粉絲);
緣起
言歸正傳,今天的重點還是要好好的說說新知識——事件溯(su)源,Event Source,也有人翻譯事件採購,或者是事件回溯,或者直接就是ES,其實都是一個意思,要是下次你發現這幾個詞語的時候,都是指的事件溯源,其實事件溯源已經有一隻腳邁進了微服務的大家族了,甚至可以說已經在微服務的一員了,他配合著事件匯流排EventBus、訊息佇列等,在微服務的工作中起著一定的作用。當然今天只是簡單的入門講解,要是想開啟真正的微服務的大門,就需要大家自己去探索,當然,我也會繼續跟進這個講解,下一個系列 Ids ,其實也是微服務的一個分支,慢慢來,希望大家多捧場啦!
馬上開始今天的講解,還是一天一問吧,希望大家帶著這個問題通讀本文,自己能想到合適的答案:
1、你認為事件儲存 EventStore 和 日誌記錄的區別是什麼?
這裡要給大家再強調兩點 :
1、CQRS、EDA和ES這些其實已經不在DDD設計的範圍之內,只不過這些技術都是一起使用的,多個技術的相互結合使用,才能發揮很大的作用,所以說本系列教程是
DDD+CQRS+EDA+ES的結合體,以後被別人問到的時候,可別說時間溯源就是領域驅動設計的一部分喲。
2、事件溯源不是一兩句能說清的,這篇文章只是一個啟蒙的作用,等大家從事微服務工作的時候,就知道它深層次的意義了,切不可和平時的 CURD 專案生搬硬套做比較。
零、今天要實現右下角 綠色 的部分
(我寫的十二篇文章中的知識點,這裡基本都有了,也算是一個圓滿了,集齊七顆啦:grinning:)
一、什麼是事件溯源 —— Event Source
時間溯源其實很好理解,首先從字面上的理解:
事件就是 Event,溯是一個動詞,可以理解為追溯,回溯的意思,源代表原始、源頭的意思,合起來表示一個事件追蹤源的過程。
1、理解事件溯源的概念
我們知道,一個物件從建立開始到消亡會經歷很多階段,同樣也會經歷很多個事件,現在我們在設計的時候,都是在每次物件參與完一個業務動作後,把它最新的狀態持久化儲存到資料庫中,也就是說我們的資料庫中的資料是反映了物件的當前最新的狀態。當然我們也是一直這麼使用的(面向物件程式設計)。然而事件溯源則相反,它不是儲存物件的最新狀態,而是儲存這個物件所經歷的每個事件,所有的由物件產生的事件會按照時間先後順序有序的存放在資料庫中——這個就是事件儲存Event Store ,然後我們一一取出來做處理就是事件溯源Event Source 。可以看出,事件溯源的這種做法是更符合事實觀的,因為它完整的描述了物件的整個生命週期過程中所經歷的所有事件。
你一定會問,為什麼記錄事件,就是更符合客觀事實呢,為什麼會這麼說呢?別慌,聽我繼續往下說。
2、事件溯源的執行過程
這個時候你仔細想一想,我們在和領域專家 (預設他們不懂技術)討論使用者下單 流程的時候,專家一定會說:客戶首先選擇一個商品,然後新增到購物車,確認無誤下單,接著使用者支付,支付成功後,就給使用者發貨。而我們呢,我們作為一個開發人員,和領域專家討論的時候,自然而然的也是這麼思考的,對不對!(你肯定在討論需求的時候用的不是資料庫的思維!),只不過我們後期開發的時候,拘泥於技術和資料優先 的思維,不得不轉向CURD 的道路了,當然這個沒有什麼錯誤,我只是說明一點,事件儲存真的離我們不遠。
那我們平時是怎麼做的呢,這裡說一個特別簡單的:
從這個特別簡單的流程中我們可以看到,平時我們都是直接操作的 Order 這個領域聚合根,一直在修改模型狀態,這個看似正常的操作下,有一些問題,是我們建立在每一步都正常執行的情況下,不過一般總會出現一些問題,特別是分散式的環境中。
然而,事件溯源與上述的情況恰好相反,它並不關心當前狀態,而是關注持續不斷的變化事件。
舉個例子,假設我們有一個“購物車”,我們可以建立購物車,往裡面新增商品或移除商品,然後結賬。
購物車的生命週期可以包含如下一系列事件:
建立購物車
往購物車裡新增商品
再次往購物車裡新增商品
從購物車裡移除商品
結賬
這些就是一個購物車的生命週期,包含了一系列事件。這就是事件溯源,非常簡單吧?
幾乎所有的流程都可以被看成一系列事件。在與領域專家交談時,他們不會提及“表”和“連線”,他們會將流程描述成一系列事件以及可以應用在這些事件上的規則。
3、事件溯源是如何更新實時狀態的
那麼,事件到底如何影響一個領域物件的狀態的呢?很簡單,當我們在觸發某個領域物件的某個行為時,該領域物件會先產生一個事件,然後該物件自己響應該事件並更新其自己的狀態,同時我們還會持久化在該物件上所發生的每一個事件;這樣當我們要重新得到該物件的最新狀態時,只要先建立一個空的物件,然後將和該物件相關的所有事件按照事件發生先後順序從先到後再全部應用一遍即可還原得到該物件的最新狀態,這個過程就是所謂的事件溯源。
二、事件溯源的存在意義與問題
事件溯源不是萬能的,不過它可以在某一些領域發揮很大的作用,這個在以後的微服務設計中,會更能體現出來,那我們就簡單說兩點:
1、傳統應用中出現的某些問題
從業務的角度
傳統的應用中,資料庫裡存的是Domain Model的例項的當前狀態,比如某個儲戶銀行賬戶的存款數 ,通常是一個數字.如果考慮到如下的三個情形,我們可能付出的代價比較大:
1) 老規則:問題跟蹤
如果某個儲戶的賬戶出現問題,那麼我們只有從大到PB的日誌中去分析使用者的賬戶資料是如何出錯的,而且我們在做日誌的時候,不可能所有的都考慮到,就算是把全部資料都儲存,時間都記下來,操作者都備份,那ATM機資訊呢?(可能不恰當,只是說明我們總有想不到的地方),但如果一旦日誌不夠詳細,找出問題根源基本只能靠猜了。
2) 新需求:趨勢分析
歷史資料的作用在於分析未來的趨勢,如果僅僅從浩如煙海的日誌中尋找規律,我們還得單獨寫邏輯,對日誌進行建模,清洗,其實我們已經能接受,日誌就是用來記錄異常資訊的,這個時候我們就很崩潰了。
3) 更奇葩:事務回滾
在介紹事務修正模式中,我們講到某個步驟發生錯誤,之前的各個節點可以自己獨立地完成回滾,回滾的依據就是記錄的操作步驟及相關引數,根據這些有用資訊就可以每個節點自行回滾到原始狀態,並且在失敗的時候可以retry
可見儲存對於Domain Model 的各個事件還是非常有用的,尤其是對於複雜的系統,這也就是我們今天要討論的事件溯源模式.
從技術角度
大多數的應用都和資料打交道,最常見的打交道方式就是將使用者在使用過程中的資料最終狀態同步到資料庫中。例如,在傳統的增刪改查(CURD)模式中,一個典型的資料過程就是從資料庫中讀出資料,修改完後再把修改後的資料更新到資料庫中——通常來說,在這個更新過程這張資料表是被鎖住的。
這種傳統的增刪改查(CURD)方式存在一些侷限性:
- 事實上執行這種直接依賴資料庫的增刪改查(CRUD)開銷會影響系統的效能和響應性,不利於系統的可伸縮性。
- 在一個存在多個使用者併發操作的領域中,因為多個使用者也許會同時操作同一張表,所以資料更新造成的衝突更加可能發生。
- 除非系統額外有一個可以記錄所有業務細節的日誌系統以實現審查機制,否則所有的歷史都會丟失。
這是一個大問題。在以表作為驅動的系統裡,你只儲存了系統的當前狀態,你根本就無法知道系統是如何達到當前狀態的。如果我問你“這個使用者修改了幾次郵件地址”,你有辦法回答嗎?或者我再問“有多少人把一件商品新增到購物車裡,然後又移除掉,直到一個月之後才買了那件商品”,你就更沒法回答了。你儲存資料的方式丟掉了很多有用的業務資訊!
2、我們有哪些理由使用Event Sourcing(優點)
儘管它是一個簡單的模式,但使用它有很多優點:
事件日誌具有很高的商業價值;
它在DDD和事件驅動架構下執行得非常好。
除錯用應用程式狀態中所有變更的來源;
它允許您重放失敗的事件;
易於除錯,您可以將目標實體的所有事件複製到您的機器並除錯每個事件,以瞭解應用程式如何達到特定狀態(忽略從生產環境複製資料的安全隱患);
允許您使用追溯事件模式重建/修復您的狀態。
許多作者還將優先順序作為時間查詢的能力,但我認為查詢多個後續事件不是一項簡單的任務。因此,我通常認為時間查詢是快照模式的一個優點。
有許多理由使用Event Sourcing,當你瀏覽Greg Young的系列文章和談話你會發現下面要點:
1. 它不是一個新概念,真實世界中許多領域都很像它,看看你的銀行賬戶狀態,比如儲蓄卡,它打印出一筆筆進出明細和當前餘額,這一筆筆代表了領域事件。
2.通過重播事件,我們能夠得到物件的任何時刻狀態(這裡應該用正確術語:聚合aggregate),比如儲蓄卡每筆記錄的當前餘額代表你這個賬戶聚合物件的某刻時刻的狀態,這可能會極大地幫助我們理解領域知識,當前狀態是怎麼來,因為什麼改變?方便除錯關鍵問題的錯誤
3.領域中當前狀態和儲存資料庫中的資料沒有任何耦合,而傳統上我們都是將應用狀態儲存到資料庫中,比如儲蓄卡當前餘額100元儲存到資料庫中,現在我們儲存導致餘額的進出事件了,存款了多少錢,取
款了多少錢,這一筆筆領域事件都會記錄在資料庫中。
4.Append-only追加模型儲存這些事件,易於擴充套件,這樣我們無論讀寫都有很好地效能,讀取能夠轉為查詢優化,也可以轉為寫優化(因為沒有讀,寫得很快),讀寫分離。
5除了可以儲存使用者意圖資料,也就是操作事件,事件儲存順序能夠用來分析使用者正在做什麼,通往大資料。
6.我們能避免了物件與關係資料庫的不匹配。
7.審計日誌是免費的,一次審計日誌所有變化,因為沒有狀態改變,只有事件。
這樣不會浪費時間嗎?
一點也不。一般來說,要執行約束,只需要獲得事件的一個很小子集。通過簡單的資料庫查詢就可以獲得有用的歷史事件,在載入完這些事件後重放它們,把它們“投射”出來,以此構建你的資料集。這樣的操作其實是很快的,因為你使用的是本地的處理器,而不是執行一系列SQL查詢(跨域網路的呼叫要比本地操作慢得多,至少會相差兩個數量等級)。
你可以在後臺構建資料集,然後把中間結果儲存在資料庫裡。這樣,使用者就可以在很短的時間內查詢到這些資料。
3、事件溯源存在的一些問題(缺點)
下面是一些困難:
1.定義事件是一件藝術,需要熟悉的領域建模,DDD領域驅動設計是關鍵。
2.需要軟體和硬體支援事件採購,在以後幾年,你會看到這個領域的很多解決方案。
3.這方面是新生事物,可指導的經驗太少。
4.限制與真正成熟的DDD/ES技能。
其他帶來的問題還有:
1.需要超級大的儲存消耗。雲端儲存解決。
2.比較慢也不是問題,因為我們優化優化IO來實現快照和持久。並利用基於事件的天然“推”性質,我們可以得到立即失效快取。簡而言之,能夠過後有多個插入,需要這種多個的技術解決方案。
3.脆弱(丟失失過去的一個事件將導致整個流腐敗)不是一個問題,因為你可以決定自己的SLA水平去(通過複製和冗餘)。使用Git的方法,可以可靠地檢測在任何一個副本的腐敗事件包括SHA1簽名針對它的內容和以前的事件簽名計算。
-
在同步呼叫中不太直觀,因為需要首先將請求轉換為事件。
-
無論何時部署重大更新,如果您想要向後相容(也稱為“事件升級”),你將被迫遷移事件歷史記錄。
-
某些實現可能需要額外的工作來檢查最新事件的狀態,以確保所有事件都已被處理。
-
事件可能包含私有資料,所以不要忘記確保事件日誌得到適當保護。
4、什麼時候使用這種模式
這種模式在以下幾種場景中是最理想的解決方案:
- 當你想獲得資料的“意圖”,“目的”或者“原因”的時候。 例如,一個客戶的實體改變可能用一系列的類似於”搬家“,”登出賬戶“或者”死亡“等事件型別。
- 併發更新資料時候非常需要減少或者完全避免衝突的時候。
- 當你需要儲存已經發生的事件,並且能夠重播他們來還原到某個狀態、使用這些事件去回滾系統的某些變化或者僅僅是歷史或者審查記錄的時候。例如 ,當一個任務包括幾個步驟,你可能需要執行一個撤銷更新的操作然後重播過去的每個步驟來回到穩定的狀態。
- 當使用事件是一些應用程式的某些操作的天然屬性,並且需要很少的額外擴充套件或者實施的時候。
- 當你需要把插入,更新資料和需要執行這些操作的應用程式解耦開的時候。用這種模式可以提高UI的效能,或者把這些事件分發給其他的監聽者,比如有些應用程式或系統,它們在一些事件發生的時候必須做出一些反應。例如,將一個工資系統和一個報銷系統結合起來,這樣的話當報銷系統更新一個事件給事件資料庫,資料庫對此做出的相應事件就可以被報銷系統和工資系統共享。
- 當要求變更或者——當和CQRS配合使用的時候——你需要適配一個讀的模型或者檢視來顯示資料,而你想要更靈活地改變物化檢視的格式和實體資料的時候。
- 當和CQRS配合使用的時候,並且當一個讀模型被更新時能接受資料的最終一致性問題,或者說從一系列的事件序列中生成實體對效能的影響可以被接受。
這種模式在以下幾種場景中可能並不適用:
- 小而簡單的,業務邏輯簡單或者根本沒有業務邏輯,或者領域概念的,一般傳統的增刪改查(CURD)就能實現功能的業務領域,或者系統。
- 需要實時一致和實時更新資料的系統。
- 不需要審查,歷史和回滾的系統。
- 併發更新資料可能性非常小的系統。例如,只增加資料不更新資料的系統。
5、ES 和 CQRS 的關係
CQRS與事件溯源有著相輔相成的關係。CQRS允許事件溯源作為領域的資料儲存機制。然而,使用事件溯源的一個最大的缺點是,你無法向你的系統提出類似“請告訴我所有名字為Greg的使用者”這樣的問題,這是由於事件溯源無法提供物件的當前狀態而引起的。CQRS唯一支援的查詢就是:GetById - 通過ID來獲得某個聚合。下圖為基於CQRS/ES的應用系統結構:
CQRS經常和事件溯源模式結合使用
基於CQRS的系統使用分離的讀和寫模型,每一個都對應相應的任務並且一般儲存在不同的資料庫中。當和事件溯源模式一起使用的時候,一系列的事件儲存相當於“寫”模型,是所有資訊的可信賴來源(authoritative source )。基於CQRS的系統的讀模型提供了資料的物化檢視,經常是一種高度格式化的檢視形式。這些檢視對應相應的介面並且展示了應用程式的需求,幫助最大化展示和查詢效率。
使用一系列的事件當作“寫”而不是某一個時間點的資料,避免了更新的衝突並且最大化效能和系統的伸縮性,這些事件可以被非同步地產生被用來展示資料的物化檢視。
因為事件資料庫是所有資訊的可信賴來源,當系統改進的時候,有可能刪除物化檢視並且展示所有過去的時間來產生一個新的資料,或者當讀模型必須改變的時候。物化檢視是一個長久的資料快取。
當將CQRS和事件溯源模式結合起來的時候,考慮以下幾點:
- 對於任何的讀寫分離儲存的系統,這些系統基於事件溯源模式都是“最終一致”的。因此在事件產生和資料儲存之間會有一些延遲。
- 這種模式會造成一些額外的複雜度,因為程式碼必須要能夠初始化和處理事件,然後組合或者更新相應的讀寫模型需要檢視或者物件。這種複雜度會對讓系統的實現變得有些困難,需要重新學習一些概念和一個不同的設計系統的方式。然而事件溯源可以讓為領域建模,讓重建檢視或者物件更加容易。
- 生成物化檢視
CQRS最核心的概念是Command、Event,“將資料(Data)看做是事實(Fact)。每個事實都是過去的痕跡,雖然這種過去可以遺忘,但卻無法改變。” 這一思想直接發展了Event Source,即將這些事件的發生過程記錄下來,使得我們可以追溯業務流程。CQRS對設計者的影響,是將領域邏輯,尤其是業務流程,皆看做是一種領域物件狀態遷移的過程。這一點與REST將HTTP應用協議看做是應用狀態遷移的引擎,有著異曲同工之妙。
1、必須自己實現事務的統一commit和rollback:這個是無論哪一種方式,都必須面對的問題。完全逃不掉。在DDD中有一個叫Saga
的概念,專門用於統理這種複雜互動業務的,CQRS/ES架構下,由於本身就是最終一致性,所以都實現了Saga
,可以使用該機制來做微服務下的transaction治理。
2、請求冪等:請求傳送後,由於各種原因,未能收到正確響應,而被請求端已經正確執行了操作。如果這時重發請求,則會造成重複操作。
CQRS/ES架構下通過AggregateRootId、Version、CommandId三種標識來識別相同command,目前的開源框架都實現了冪等支援。
3、併發:單點上,CQRS/ES中按事件的先來後到嚴格執行,記憶體中Aggregate
的狀態由單一執行緒原子操作進行改變。
多節點上,通過EventStore的broker機制,毫秒級將事件複製到其他節點,保證同步性,同時支援版本回退。(Eventuate)
三、在專案中使用ES
大家請注意,下邊的這一個流程,就和我們平時開發的順序是一樣的,比如先建立模型,然後倉儲層,然後應用服務層,最後是呼叫的過程,東西雖然很多,但是很簡單,慢慢看都能看懂。
同時也複習下我們DDD領域驅動設計是如何搭建環境的,正好在最後一篇和第一篇遙相呼應。
1、建立事件儲存模型 StoredEvent : Event
那既然說到了事件溯源,我們就需要首先把事件儲存下來,那存下來之前,首先要進行建模:
在核心應用層 Christ3D.Domain.Core 的 Events資料夾下,新建 Message.cs 用來獲取我們事件請求的型別:
namespace Christ3D.Domain.Core.Events { /// <summary> /// 抽象類Message,用來獲取我們事件執行過程中的類名 /// 然後並且新增聚合根 /// </summary> public abstract class Message : IRequest { public string MessageType { get; protected set; } public Guid AggregateId { get; protected set; } protected Message() { MessageType = GetType().Name; } } }
同時在該資料夾下,新建 儲存事件 模型StoredEvent.cs
public class StoredEvent : Event { /// <summary> /// 構造方式例項化 /// </summary> /// <param name="theEvent"></param> /// <param name="data"></param> /// <param name="user"></param> public StoredEvent(Event theEvent, string data, string user) { Id = Guid.NewGuid(); AggregateId = theEvent.AggregateId; MessageType = theEvent.MessageType; Data = data; User = user; } // 為了EFCore能正確CodeFirst protected StoredEvent() { } // 事件儲存Id public Guid Id { get; private set; } // 儲存的資料 public string Data { get; private set; } // 使用者資訊 public string User { get; private set; } }
2、定義事件儲存上下文 EventStoreSQLContext
定義好了模型,那我們接下來就是要建立資料庫上下文了:
1、首先在基礎設施資料層 Christ3D.Infrastruct.Data 下的 Mappings資料夾下,建立事件儲存Map模型 StoredEventMap.cs
namespace Christ3D.Infra.Data.Mappings { /// <summary> /// 事件儲存模型Map /// </summary> public class StoredEventMap : IEntityTypeConfiguration<StoredEvent> { public void Configure(EntityTypeBuilder<StoredEvent> builder) { builder.Property(c => c.Timestamp) .HasColumnName("CreationDate"); builder.Property(c => c.MessageType) .HasColumnName("Action") .HasColumnType("varchar(100)"); } } }
2、然後再上下文資料夾 Context 下,新建事件儲存Sql上下文 EventStoreSQLContext.cs
namespace Christ3D.Infra.Data.Context { /// <summary> /// 事件儲存資料庫上下文,繼承 DbContext /// /// </summary> public class EventStoreSQLContext : DbContext { // 事件儲存模型 public DbSet<StoredEvent> StoredEvent { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.ApplyConfiguration(new StoredEventMap()); base.OnModelCreating(modelBuilder); } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { // 獲取連結字串 var config = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json") .Build(); // 使用預設的sql資料庫連線 optionsBuilder.UseSqlServer(config.GetConnectionString("DefaultConnection")); } } }
3、持久化事件倉儲 EventStoreSQLRepository : IEventStoreRepository
上邊咱們定義了用於持久化事件模型的上下文,那麼現在我們就需要設計倉儲操作類了
1、在 基礎設施資料層中的 Repository 資料夾下,定義事件儲存倉儲介面 IEventStoreRepository.cs
namespace Christ3D.Infra.Data.Repository.EventSourcing { /// <summary> /// 事件儲存倉儲介面 /// 繼承IDisposable ,可手動回收 /// </summary> public interface IEventStoreRepository : IDisposable { void Store(StoredEvent theEvent); IList<StoredEvent> All(Guid aggregateId); } }
2、然後對上邊的介面進行實現
namespace Christ3D.Infra.Data.Repository.EventSourcing { /// <summary> /// 事件倉儲資料庫倉儲實現類 /// </summary> public class EventStoreSQLRepository : IEventStoreRepository { // 注入事件儲存資料庫上下文 private readonly EventStoreSQLContext _context; public EventStoreSQLRepository(EventStoreSQLContext context) { _context = context; } /// <summary> /// 根據聚合id 獲取全部的事件 /// 這個聚合是指領域模型的聚合根模型 /// </summary> /// <param name="aggregateId"> 聚合根id 比如:訂單模型id</param> /// <returns></returns> public IList<StoredEvent> All(Guid aggregateId) { return (from e in _context.StoredEvent where e.AggregateId == aggregateId select e).ToList(); } /// <summary> /// 將命令事件持久化 /// </summary> /// <param name="theEvent"></param> public void Store(StoredEvent theEvent) { _context.StoredEvent.Add(theEvent); _context.SaveChanges(); } /// <summary> /// 手動回收 /// </summary> public void Dispose() { _context.Dispose(); } } }

這個時候,我們的事件儲存模型、上下文和倉儲層已經建立好了,也就是說我們可以對我們的事件模型進行持久化了,接下來就是在建立服務了,用來呼叫倉儲的服務,就好像我們的應用服務層的概念。
4、建立事件儲存服務 SqlEventStoreService: IEventStoreService
建完了基礎設施層,那我們接下來就需要建立服務層了,並對其進行呼叫:
1、還是在核心領域層中的Events資料夾下,建立介面
namespace Christ3D.Domain.Core.Events { /// <summary> /// 領域儲存服務介面 /// </summary> public interface IEventStoreService { /// <summary> /// 將命令模型進行儲存 /// </summary> /// <typeparam name="T"> 泛型:Event命令模型</typeparam> /// <param name="theEvent"></param> void Save<T>(T theEvent) where T : Event; } }
2、然後再來實現該介面
在應用層 Christ3D.Application 中,新建 EventSourcing 資料夾,用來對我們的事件儲存進行溯源,然後新建 事件儲存服務類 SqlEventStoreService.cs
namespace Christ3D.Infra.Data.EventSourcing { /// <summary> /// 事件儲存服務類 /// </summary> public class SqlEventStoreService : IEventStoreService { // 注入我們的倉儲介面 private readonly IEventStoreRepository _eventStoreRepository; public SqlEventStoreService(IEventStoreRepository eventStoreRepository) { _eventStoreRepository = eventStoreRepository; } /// <summary> /// 儲存事件模型統一方法 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="theEvent"></param> public void Save<T>(T theEvent) where T : Event { // 對事件模型序列化 var serializedData = JsonConvert.SerializeObject(theEvent); var storedEvent = new StoredEvent( theEvent, serializedData, "Laozhang"); _eventStoreRepository.Store(storedEvent); } } }
這個時候你會問了,那我們現在都寫好了,在哪裡使用呢,欸?!聰明,既然是事件儲存,那就是在事件儲存的時候,進行儲存,請往下看。
5、在匯流排中釋出事件的同時,對事件儲存 Task RaiseEvent<T>
在我們的匯流排實現類 InMemoryBus.cs 下的引發事件方法中,將我們的事件都儲存下來(除了領域通知,這個錯誤通知不需要儲存):
/// <summary> /// 引發事件的實現方法 /// </summary> /// <typeparam name="T">泛型 繼承 Event:INotification</typeparam> /// <param name="event">事件模型,比如StudentRegisteredEvent</param> /// <returns></returns> public Task RaiseEvent<T>(T @event) where T : Event { // 除了領域通知以外的事件都儲存下來 if ([email protected]("DomainNotification")) _eventStoreService?.Save(@event); // MediatR中介者模式中的第二種方法,釋出/訂閱模式 return _mediator.Publish(@event); }
四、未完待續...
DDD領域驅動設計就到這裡到一段落了,江湖很遠,話不多說,咱們下一系列再見!
五、粉絲活動
關於活動的原因文章開頭已經說的很清楚了,這裡就直接說規則吧:
這裡是三個問題,大家仔細思考
//1、聚合根是什麼?或者說是什麼資料結構?(言之成理即可) //2、我的專案中,有幾條匯流排,分別是? //3、我的專案中,在使用領域通知處理器之前,我是用什麼不當的臨時方法來處理驗證錯誤資訊的?(提示:在自定義檢視元件中)
1、請仔細思考上邊這三個問題,並在評論區留言,格式是:
2、評論時間最早 的兩個 人(也就是樓層最靠前),並且答案正確 ,可以每人一本書(或者等價紅包),《實現領域驅動設計 》,或者《領域驅動設計 軟體核心複雜性應對之道 》。
為了公平,每個人只有一次評論的機會,且不能先佔樓再修改內容,我這裡會有修改記錄,所以請一次性寫好正確答案(違規直接作廢)。
3、從釋出文章開始,24小時內,評論被點贊最多 的那一個人,可以獲獎,獎品同上。
如果沒有一個人被點贊,就根據時間順序的第三個正確答案。