Command Model
在一個基于CQRS的應用程序中,一個領域模型(如Eric Evans和Martin Fowler所定義的)可以是一個非常強大的機制炬转,這種機制可以處理聚合狀態(tài)發(fā)生改變時的校驗和執(zhí)行時所帶來的復雜性验游。通常一個典型的領域模型會有大量的構建塊浓瞪,但是在CQRS中的Command 處理過程中有一個重要的角色就是:聚合宏邮。
Command是應用程序狀態(tài)發(fā)生改變的源頭根盒。命令是表達意圖(它描述你想要做什么)和你所須要的信息的組合褐健。命令模型用于處理傳入的命令付鹿,以驗證它并定義他的輸出結果。在這個模型中蚜迅,一個Command Handler負責處理某種類型的命令舵匾,并根據(jù)其中包含的信息做出相應的邏輯處理。
Aggregate聚合
聚合是始終保持一致狀態(tài)的一個實體或一組實體谁不。聚合根是負責維護該聚合內(nèi)一致狀態(tài)的聚合樹頂部的對象坐梯。這使得Aggregate成為在任何基于CQRS的應用程序中實現(xiàn)命令模型的主要構建塊。
注意:
術語“聚合”是指Evans在領域驅(qū)動設計中定義的聚合:
因為數(shù)據(jù)狀態(tài)發(fā)生改變所引起的相關聯(lián)的對象被視為一個單元(聚合)刹帕,在外部只能引用聚合的聚合根對象吵血。在聚合內(nèi)部使用一套一致性規(guī)則。
例如偷溺,“Contact”聚合包含了兩個實體:Contact和Address蹋辅。為了使整個聚合保持一致的狀態(tài),應該通過Contact實體來添加地址給聯(lián)系人亡蓉。在這種情況下晕翠,Contact實體是聚合根。
在Axon中,聚合由一個聚合標識來標識淋肾。他可以是任何對象硫麻,但是標識符的最好的做法是這樣的:
1.實現(xiàn)equals和hashCode方法來保證與其他實例進行相等比較
2.實現(xiàn)一個提供一致結果的的toString()方法(標識符相等的話,那么toString()方法的結果也應該相等.
3.標識符是可序列化的
當聚合使用不兼容的識符時樊卓,測試程序(見測試)將驗證這些條件并使測試失敗拿愧。String,UUID和數(shù)字類型的標識符是最好的碌尔。不要使用原始類型(比如int這些)作為標識符浇辜,因為它們不允許延遲初始化。在某些情況下唾戚,Axon可能會錯誤地假設原始類型的默認值是標識符的值柳洋。
注意:一個好習慣就是使用隨機生成的標識符,而不是按順序生成標識符叹坦。使用順序生成的序列會大大降低應用程序的可伸縮性熊镣,因為機器需要保持彼此最后一次使用的最新的序列號。與UUID沖突的機會非常渺茫(如果生成8.2×10 11個UUID募书,則機會為10的-15次方)绪囱。
聚合的實現(xiàn)
聚合的操作總是交給一個稱為聚集根的實體來控制。通常莹捡,這個實體的名稱和聚合名完全一樣鬼吵。例如,Order聚合可能由Order實體引用幾個Orderline實體組合而成.Order和Orderline一起形成聚合篮赢。雖然根據(jù)CQRS原則來講這并不完全正確齿椅,但他也可以通過方法來暴露出聚合的狀態(tài)。
聚合根必須聲明包含聚合標識符的字段荷逞。這個標識符必須在第一個事件發(fā)布的時候最初被初始化媒咳。該標識符字段必須由@AggregateIdentifier注解注釋粹排。如果您使用JPA并在聚合上使用JPA注解种远,則Axon也可以使用JPA提供的@Id注解。
聚合可以使用AggregateLifecycle.apply()方法來注冊要發(fā)布的事件顽耳。與EventBus不同的是坠敷,消息需要被包裝在EventMessage中,apply()允許你直接傳遞payload對象射富。
import static org.axonframework.commandhandling.model.AggregateLifecycle.apply;
@Entity // Mark this aggregate as a JPA Entity
public class MyAggregate {
@Id // When annotating with JPA @Id, the @AggregateIdentifier annotation is not necessary
private String id;
// fields containing state...
@CommandHandler
public MyAggregate(CreateMyAggregateCommand command) {
// ... update state
apply(new MyAggregateCreatedEvent(...));
}
// constructor needed by JPA
protected MyAggregate() {
}
}
通過定義一個帶@EventHandler注解的方法膝迎,聚合內(nèi)的實體能監(jiān)聽聚合發(fā)布的事件。當一個EventMessage發(fā)布時這些方法將被調(diào)用(在任何外部處理器發(fā)布之前)胰耗。
聚合的事件溯源
除了存儲Aggregate的當前狀態(tài)之外限次,還可以根據(jù)它過去發(fā)布的Events來重建Aggregate的狀態(tài)。為了要達到這樣的效果,所有的狀態(tài)改變必須由一個事件來表示卖漫。
事件源聚合類似于一種被約定成俗的聚合:它們必須聲明一個標識符费尽,并可以使用apply方法來發(fā)布事件。然而羊始,事件溯源聚合類的狀態(tài)更改(即字段值的任何更改)必須在被@EventSourcingHandler注解的方法上執(zhí)行旱幼。這包括了設置聚合根的標識符。
我們須要注意的是突委,聚合標識符必須在聚合發(fā)布的第一個事件中就添加進去柏卤,以便在@EventSourcingHandler注解的方法將聚合的標識符進行賦值(注:也就是@EventSourcingHandler中處理的event的聚合標識符不能為空)。他們通常是在創(chuàng)建事件的時候就把聚合標識符添加到事件中匀油。
事件源聚合類的聚合根必須包含一個無參數(shù)構造函數(shù)缘缚。Axon框架會用這個構造方法創(chuàng)建一個空的Aggregate實例,他會在事件溯源的時候調(diào)用敌蚜。加載聚合時忙灼,如果找不到這個無參構造函數(shù)將導致異常。
publicclass MyAggregateRoot {
@AggregateIdentifier
private String aggregateIdentifier;
// fields containing state...
@CommandHandler
public MyAggregateRoot(CreateMyAggregate cmd) {
apply(new MyAggregateCreatedEvent(cmd.getId()));
}
// constructor needed for reconstruction
protected MyAggregateRoot() {
}
@EventSourcingHandler
private void handleMyAggregateCreatedEvent(MyAggregateCreatedEvent event) {
// make sure identifier is always initialized properly
this.aggregateIdentifier = event.getMyAggregateIdentifier();
// ... update state
}
}
帶@EventSourcingHandler注解的方法會使用特定的規(guī)則來解析钝侠。這些規(guī)則同樣對帶有@EventHandler注解的方法也一樣適合该园,他們在Defining Event Handlers這一章節(jié)中有詳細的解釋。
注意:Event handler方法可以是私有的帅韧,只要JVM的安全設置允許Axon框架更改方法的可訪問性即可里初。這樣可以清楚地將聚合的公共API(公開生成事件的方法)從處理事件的內(nèi)部邏輯中分離出來。大多數(shù)的IDE有一個選項用來忽略“未使用的私有方法”的警告方法忽舟∷粒或者,您可以向該方法添加@SuppressWarnings(“UnusedDeclaration”)注解叮阅,以確保不會出現(xiàn)一不小心就將Event handler方法刪除刁品。
在有些情況下,特別是當聚合有很多實體的時候浩姥,對同一聚合的其他實體中事件發(fā)布的影響更明顯挑随。然而,由于在聚合進行事件溯源的時候也會調(diào)用 Event Handler方法勒叠,因此必須采取特別的預防措施兜挨。
我們可以在Event Sourcing Handler方法內(nèi)通apply()發(fā)布一個新事件。這使得實體 B可以發(fā)布一個事件來響應實體A眯分。當進行事件重演的時候拌汇,Axon會忽略這個apply()方法而不會去調(diào)用他。請注意弊决,在這種情況下噪舀,在所有實體接收到第一個事件后,內(nèi)部apply()調(diào)用的事件只會發(fā)布給實體。如果有更多的事件須要發(fā)布与倡,可以使用apply(...).andThenApply(...)這種方法來實現(xiàn)先改。
您也可以使用靜態(tài)的AggregateLifecycle.isLive()方法來檢查聚合是否是“l(fā)ive”。如果一個聚合能夠完成對歷史事件的重演蒸走,那么他就是live的仇奶。在重演這些事件時,isLive()將返回false比驻。
復雜的聚合結構
一個只包含聚合根的聚合來處理很復雜的業(yè)務邏輯往往是不現(xiàn)實的该溯。在這種情況下,我們須要將這種復雜性分散到聚合的各個實體上别惦。當使用事件溯源時狈茉,不僅聚合根需要使用事件來觸發(fā)狀態(tài)轉(zhuǎn)換,而且聚合內(nèi)其他實體也如此掸掸。
注意:對聚合不應該暴露狀態(tài)的規(guī)則的普片誤解是所有實體都不應該包含任何屬性訪問方法氯庆。 這并非如此。事實上扰付,在同一聚合內(nèi)的實體向其他的實體暴露狀態(tài)堤撵,可能會使一個聚合受益很多。然而羽莺,建議不要向外部暴露聚合的狀態(tài)实昨。
Axon為復雜的聚合結構提供了對事件溯源的支持。實體就像聚合根盐固,簡單的對象荒给。聲明子實體的字段必須用@aggregateMember注解。這個注解告訴Axon被注解的字段刁卜,包含一個應該對Command 和 Event Handlers進行檢查的類志电。
當一個實體(包括聚合根)發(fā)布一個事件,這個事件首先應該被聚合根處理蛔趴。然后通過@AggregateMember注解的作用傳到其子實體挑辆。
包含子實體的字段必須用@AggregateMember注解,此注釋可用于多種字段類型:
l 實體類型夺脾,在字段中直接引用;
l 內(nèi)部包含一個Iterable字段(包括所有集合之拨,如Set,List等)
l 內(nèi)部包含java.util.Map字段的值
聚合中的命令處理
推薦直接在包含處理狀態(tài)命令的Aggregate中定義Command Handlers咧叭,因為命令處理程序可能需要該Aggregate的狀態(tài)來完成其工作。
在Aggregate中定義Command Handlers只須要在你要用的方法上面添加一個@CommandHandler注解就行了烁竭。帶@CommandHandler注解方法的處理規(guī)則和其他處理方法都是一樣的菲茬。但是,Command不僅僅是通過它們的payload進行路由。Command Messages(命令消息)會攜帶一個名稱婉弹,該名稱默認為Command對象的完整的類名稱睬魂。
默認情況下,帶@CommandHandler注解的方法允許以下參數(shù)類型:
l 第一個參數(shù)是Command Message中的payload.如果@CommandHandler顯示的定義了他要處理的Command的名字,那么他的類型可以是Message镀赌,或者CommandMessage.默認情況下一個Command的名字是這個Command的payload的完整的類名稱氯哮。
l 用@MetaDataValue注解的參數(shù),將用注解上的鍵對元數(shù)據(jù)值進行解析商佛。如果這個值是false(默認)喉钢,則當元數(shù)據(jù)值不存在時會傳遞null。如果值是true良姆,而元數(shù)據(jù)值不存時肠虽,這時解析器會發(fā)現(xiàn)錯誤,并阻止該方法的調(diào)用玛追。
l 參數(shù)為MetaData的話税课,那么將注入一個CommandMessage的整個MetaData。
l 如果參數(shù)是UnitOfWork的話痊剖,那么將獲取當前的UnitOfWork并注入進來韩玩。
l 如果參數(shù)Message, or CommandMessage的話,他們會獲取整個的數(shù)據(jù)陆馁,它們包括Meta Data元數(shù)據(jù)和payload.如果一個方法需要多個元數(shù)據(jù)字段啸如,或者包裝消息的其他屬性,這很有用處氮惯。