Java的CQRS和事件溯源ES入門:如何從CRUD切換到CQRS/ES - Baeldung
在本教程中,我們將探索命令查詢責任隔離(CQRS)和事件源設計模式的基本概念。
雖然通常被稱為互補模式,但我們將嘗試分別理解它們,並最終了解它們如何相互補充。這些模式通常在企業應用程式中一起使用。在這方面,他們還受益於其他幾種企業架構模式。我們將討論其中的一些內容。有多種工具和框架可幫助採用這些模式,但是我們將使用Java建立一個簡單的應用程式以瞭解基礎知識。
事件溯源ES
ES為我們提供了一種新的方式來將應用程式狀態保持為事件的有序序列。我們可以有選擇地查詢這些事件並在任何時間點重建應用程式的狀態。當然,要使其工作,我們需要將對應用程式狀態的所有更改重新對映為事件:
這些事件是已經發生並且不能更改的事實,換句話說,它們必須是不變的。重新建立應用程式狀態只是重播所有事件。
請注意,這還提供了選擇性重播事件,反向重播某些事件等的可能性。因此,我們可以將應用程式狀態本身視為次要公民,而事件日誌則是我們的主要事實來源。
CQRS
CQRS是關於將應用程式體系結構的命令和查詢分開。CQRS基於Bertrand Meyer提出的命令查詢分離(CQS)原理。CQS建議將對域物件的操作分為兩個不同的類別:查詢和命令:
查詢返回結果,並且不更改系統的可觀察狀態。命令會更改系統的狀態,但不一定會返回值。
我們通過完全分離域模型的Command和Query端來實現這一點。當然,我們可以採取進一步的措施,通過引入一種使資料儲存保持同步的機制來拆分資料儲存區的寫和讀側。
一個簡單的應用
我們將從描述一個簡單的Java應用程式開始,該應用程式可以構建域模型。
該應用程式將在域模型上提供CRUD操作,並且還將具有域物件的永續性。CRUD代表建立,讀取,更新和刪除,這是我們可以對域物件執行的基本操作。
在後面的部分中,我們將使用相同的應用程式來介紹事件源和CQRS。
在此過程中,我們將在示例中利用域驅動設計(DDD)中的一些概念。
DDD解決了依賴複雜領域特定知識的軟體的分析和設計。它基於這樣的思想,即軟體系統必須基於完善的域模型。EDD Evans首先將DDD規定為模式目錄。我們將使用其中一些模式來構建示例。
建立使用者配置檔案並對其進行管理是許多應用程式中的典型要求。我們將定義一個簡單的域模型來捕獲使用者配置檔案以及永續性:
如我們所見,我們的域模型已規範化並公開了幾個CRUD操作。這些操作僅用於演示,根據需要可以簡單或複雜。而且,這裡的永續性儲存庫可以在記憶體中,也可以使用資料庫。
簡單CRUD應用
首先,我們必須建立代表域模型的Java類。這是一個非常簡單的領域模型,甚至可能不需要複雜的事件源和CQRS等設計模式。但是,我們將保持簡單,著重於瞭解基礎知識:
public class User { private String userid; private String firstName; private String lastName; private Set<Contact> contacts; private Set<Address> addresses; // getters and setters } public class Contact { private String type; private String detail; // getters and setters } public class Address { private String city; private String state; private String postcode; // getters and setters } |
同樣,我們將為應用程式狀態的永續性定義一個簡單的記憶體儲存庫。當然,這並沒有增加任何價值,但足以滿足我們稍後的演示要求:
public class UserRepository { private Map<String, User> store = new HashMap<>(); } |
現在,我們將定義一個服務以在我們的域模型上公開典型的CRUD操作:
public class UserService { private UserRepository repository; public UserService(UserRepository repository) { this.repository = repository; } public void createUser(String userId, String firstName, String lastName) { User user = new User(userId, firstName, lastName); repository.addUser(userId, user); } public void updateUser(String userId, Set<Contact> contacts, Set<Address> addresses) { User user = repository.getUser(userId); user.setContacts(contacts); user.setAddresses(addresses); repository.addUser(userId, user); } public Set<Contact> getContactByType(String userId, String contactType) { User user = repository.getUser(userId); Set<Contact> contacts = user.getContacts(); return contacts.stream() .filter(c -> c.getType().equals(contactType)) .collect(Collectors.toSet()); } public Set<Address> getAddressByRegion(String userId, String state) { User user = repository.getUser(userId); Set<Address> addresses = user.getAddresses(); return addresses.stream() .filter(a -> a.getState().equals(state)) .collect(Collectors.toSet()); } } |
這幾乎是我們設定簡單CRUD應用程式所要做的。這遠非生產就緒型程式碼,但它揭示了一些我們在本教程後面將要討論的重要點。
CRUD應用程式中的問題
在繼續進行與事件源和CQRS的討論之前,值得討論當前解決方案中的問題。畢竟,我們將通過應用這些模式來解決相同的問題!
在我們可能在這裡注意到的許多問題中,我們只想關注其中兩個:
- 域模型:讀寫操作是在同一域模型上進行的。儘管對於像這樣的簡單域模型來說這不是問題,但隨著域模型變得複雜,它可能會惡化。我們可能需要優化我們的域模型和基礎儲存,以適合讀寫操作的各個需求。
- 永續性:我們對域物件的永續性僅儲存域模型的最新狀態。儘管這對於大多數情況已經足夠了,但它使某些任務具有挑戰性。例如,如果我們必須對域物件如何更改狀態進行歷史稽核,則這裡是不可能的。我們必須用一些稽核日誌來補充我們的解決方案,以實現此目的。
CQRS重構
我們將通過在應用程式中引入CQRS模式來解決上一節中討論的第一個問題。作為其中的一部分,我們將域模型及其永續性分開以處理寫入和讀取操作。讓我們看看CQRS模式如何重組我們的應用程式:
此圖說明了我們打算如何徹底分離應用程式體系結構以進行寫入和讀取。但是,我們在這裡引入了很多新元件,我們必須更好地理解它們。請注意,這些與CQRS並不嚴格相關,但是CQRS可以從中受益匪淺。
Aggregate聚合/Aggregator聚合器
聚合是域驅動設計(DDD)中描述的一種模式,該模式通過將實體繫結到聚合根邏輯上對不同的實體進行分組。聚合模式提供了實體之間的事務一致性。
CQRS自然受益於聚合模式,該模式將寫域模型組合在一起,提供了交易事務保證。聚合通常保持快取記憶體狀態以提高效能,但是如果沒有它,則可以完美地工作。
Projection投影/Projector投射器
投影是另一個大大有利於CQRS的重要模式。投影本質上是指以不同的形狀和結構表示領域物件。
這些原始資料投影是隻讀的,並且經過高度優化,以提供增強的讀取體驗。我們可能會再次決定快取預測以提高效能,但這不是必須的。
CQRS寫入端
讓我們首先實現應用程式的寫入端。
我們將從定義所需的命令開始。甲命令是一個意圖突變域模型的狀態。它是否成功取決於我們配置的業務規則。
讓我們看看我們的命令:
public class CreateUserCommand { private String userId; private String firstName; private String lastName; } public class UpdateUserCommand { private String userId; private Set<Address> addresses; private Set<Contact> contacts; } |
這些是非常簡單的類,用於儲存我們打算改變的資料。
接下來,我們定義一個聚合器,負責接收和處理命令。聚合可以接受或拒絕命令:
public class UserAggregate { private UserWriteRepository writeRepository; public UserAggregate(UserWriteRepository repository) { this.writeRepository = repository; } public User handleCreateUserCommand(CreateUserCommand command) { User user = new User(command.getUserId(), command.getFirstName(), command.getLastName()); writeRepository.addUser(user.getUserid(), user); return user; } public User handleUpdateUserCommand(UpdateUserCommand command) { User user = writeRepository.getUser(command.getUserId()); user.setAddresses(command.getAddresses()); user.setContacts(command.getContacts()); writeRepository.addUser(user.getUserid(), user); return user; } } |
聚合使用儲存庫來檢索當前狀態並保留對它的任何更改。而且,它可以在本地儲存當前狀態,以避免在處理每個命令時往返儲存庫的開銷。
最後,我們需要一個儲存庫來儲存域模型的狀態。這通常是資料庫或其他永續性儲存,但是在這裡我們將簡單地將它們替換為記憶體中的資料結構:
public class UserWriteRepository { private Map<String, User> store = new HashMap<>(); // accessors and mutators } |
至此,我們的應用程式的寫入方面結束了。
CQRS讀取端
現在讓我們切換到應用程式的讀取端。我們將從定義域模型的讀取端開始:
public class UserAddress { private Map<String, Set<Address>> addressByRegion = new HashMap<>(); } public class UserContact { private Map<String, Set<Contact>> contactByType = new HashMap<>(); } |
接下來,我們將定義讀取儲存庫。同樣,我們將只使用記憶體中的資料結構,即使這將在實際應用程式中提供更持久的資料儲存:
public class UserReadRepository { private Map<String, UserAddress> userAddress = new HashMap<>(); private Map<String, UserContact> userContact = new HashMap<>(); // accessors and mutators } |
現在,我們將定義我們必須支援的必需查詢。查詢是為了獲取資料的意圖-它不一定會導致資料生成:
public class ContactByTypeQuery { private String userId; private String contactType; } public class AddressByRegionQuery { private String userId; private String state; } |
同樣,這些是儲存資料以定義查詢的簡單Java類。
我們現在需要的是可以處理以下查詢的投影:
public class UserProjection { private UserReadRepository readRepository; public UserProjection(UserReadRepository readRepository) { this.readRepository = readRepository; } public Set<Contact> handle(ContactByTypeQuery query) { UserContact userContact = readRepository.getUserContact(query.getUserId()); return userContact.getContactByType() .get(query.getContactType()); } public Set<Address> handle(AddressByRegionQuery query) { UserAddress userAddress = readRepository.getUserAddress(query.getUserId()); return userAddress.getAddressByRegion() .get(query.getState()); } } |
這裡的投影使用我們之前定義的讀取儲存庫來解決我們所擁有的查詢。這幾乎也總結了我們應用程式的讀取方面。
CQRS同步讀寫資料
這個難題的一個難題仍然沒有解決:沒有什麼可以使我們的讀寫儲存庫同步。這是我們需要投影器的地方。一個投影機投射寫域模型到讀取域模型的邏輯。有很多更復雜的方法可以解決此問題,但我們將使其保持相對簡單:
public class UserProjector { UserReadRepository readRepository = new UserReadRepository(); public UserProjector(UserReadRepository readRepository) { this.readRepository = readRepository; } public void project(User user) { UserContact userContact = Optional.ofNullable( readRepository.getUserContact(user.getUserid())) .orElse(new UserContact()); Map<String, Set<Contact>> contactByType = new HashMap<>(); for (Contact contact : user.getContacts()) { Set<Contact> contacts = Optional.ofNullable( contactByType.get(contact.getType())) .orElse(new HashSet<>()); contacts.add(contact); contactByType.put(contact.getType(), contacts); } userContact.setContactByType(contactByType); readRepository.addUserContact(user.getUserid(), userContact); UserAddress userAddress = Optional.ofNullable( readRepository.getUserAddress(user.getUserid())) .orElse(new UserAddress()); Map<String, Set<Address>> addressByRegion = new HashMap<>(); for (Address address : user.getAddresses()) { Set<Address> addresses = Optional.ofNullable( addressByRegion.get(address.getState())) .orElse(new HashSet<>()); addresses.add(address); addressByRegion.put(address.getState(), addresses); } userAddress.setAddressByRegion(addressByRegion); readRepository.addUserAddress(user.getUserid(), userAddress); } } |
這是執行此操作的一種非常粗略的方法,但可以使我們對CQRS正常執行所需的功能有足夠的瞭解。而且,沒有必要將讀寫儲存庫放在不同的物理儲存中。分散式系統有其自身的問題!
請注意,將寫入域的當前狀態投影到不同的讀取域模型並不方便。我們在這裡採取的示例非常簡單,因此我們看不到問題所在。
但是,隨著讀寫模型變得越來越複雜,投影將變得越來越困難。我們可以通過基於事件的投影來解決此問題,而不是通過事件搜尋來解決基於狀態的投影。我們將在本教程的後面部分中介紹如何實現此目的。
我們討論了CQRS模式,並學習瞭如何在典型應用程式中引入它。我們一直試圖解決與域模型在處理讀取和寫入時的剛性相關的問題。
現在讓我們討論CQRS帶給應用程式體系結構的其他一些好處:
- CQRS為我們提供了一種方便的方式來選擇適用於寫入和讀取操作的單獨域模型;我們不必建立支援兩者的複雜域模型
- 它可以幫助我們選擇適合於處理讀寫操作複雜性的儲存庫,例如寫入的高吞吐量和讀取的低延遲
- 它通過提供關注點分離和更簡單的域模型,自然地補充了分散式體系結構中基於事件的程式設計模型
但是,這不是免費的。從這個簡單的示例可以明顯看出,CQRS為體系結構增加了相當大的複雜性。在許多情況下,它可能不合適或不值得付出痛苦:
- 只有複雜的領域模型才能從此模式增加的複雜性中受益;一個簡單的域模型可以在沒有所有這些的情況下進行管理
- 自然地在某種程度上導致程式碼重複,這與它帶來的收益相比是可以接受的。但是,建議個人判斷
- 分開的儲存庫會導致一致性問題,並且很難始終保持寫入和讀取儲存庫的完美同步。我們經常必須為最終的一致性做好準備
事件溯源
接下來,我們將解決在簡單應用程式中討論的第二個問題。回想一下,它與我們的永續性儲存庫有關。
我們將介紹事件源來解決此問題。事件源極大地改變了我們對應用程式狀態儲存的看法。
在這裡,我們已經構建了儲存庫,以儲存域事件的有序列表。域物件的每次更改都被視為事件。事件的粗略程度應該是域設計的問題。這裡要考慮的重要事項是事件具有時間順序並且是不可變的。
事件驅動的應用程式中的基本物件是事件,事件源無異。正如我們之前所看到的,事件表示在特定時間點域模型狀態的特定變化。因此,我們將從為簡單應用程式定義基本事件開始:
public abstract class Event { public final UUID id = UUID.randomUUID(); public final Date created = new Date(); } |
這只是確保我們在應用程式中生成的每個事件都具有唯一的標識和建立的時間戳。這些是進一步處理它們所必需的。
當然,可能還有其他一些我們可能感興趣的屬性,例如用於建立事件來源的屬性。
接下來,讓我們建立一些從該基本事件繼承的特定於域的事件:
public class UserCreatedEvent extends Event { private String userId; private String firstName; private String lastName; } public class UserContactAddedEvent extends Event { private String contactType; private String contactDetails; } public class UserContactRemovedEvent extends Event { private String contactType; private String contactDetails; } public class UserAddressAddedEvent extends Event { private String city; private String state; private String postCode; } public class UserAddressRemovedEvent extends Event { private String city; private String state; private String postCode; } |
這些是Java中包含域事件詳細資訊的簡單POJO。但是,這裡要注意的重要事項是事件的粒度。
我們可以為使用者更新建立一個事件,但是相反,我們決定建立單獨的事件來新增和刪除地址及聯絡方式。選擇被對映到使域模型更有效的方式。
現在,自然地,我們需要一個儲存庫來儲存我們的域事件:
public class EventStore { private Map<String, List<Event>> store = new HashMap<>(); } |
這是一個簡單的記憶體資料結構,用於儲存我們的域事件。實際上,有幾種專門建立的用於處理事件資料的解決方案,例如Apache Druid。有許多能夠處理事件源的通用分散式資料儲存,包括Kafka和Cassandra。
生產和消費事件
因此,現在我們處理所有CRUD操作的服務將發生變化。現在,它將更新域事件,而不是更新移動域的狀態。它還將使用相同的域事件來響應查詢。
public class UserService { private EventStore repository; public UserService(EventStore repository) { this.repository = repository; } public void createUser(String userId, String firstName, String lastName) { repository.addEvent(userId, new UserCreatedEvent(userId, firstName, lastName)); } public void updateUser(String userId, Set<Contact> contacts, Set<Address> addresses) { User user = UserUtility.recreateUserState(repository, userId); user.getContacts().stream() .filter(c -> !contacts.contains(c)) .forEach(c -> repository.addEvent( userId, new UserContactRemovedEvent(c.getType(), c.getDetail()))); contacts.stream() .filter(c -> !user.getContacts().contains(c)) .forEach(c -> repository.addEvent( userId, new UserContactAddedEvent(c.getType(), c.getDetail()))); user.getAddresses().stream() .filter(a -> !addresses.contains(a)) .forEach(a -> repository.addEvent( userId, new UserAddressRemovedEvent(a.getCity(), a.getState(), a.getPostcode()))); addresses.stream() .filter(a -> !user.getAddresses().contains(a)) .forEach(a -> repository.addEvent( userId, new UserAddressAddedEvent(a.getCity(), a.getState(), a.getPostcode()))); } public Set<Contact> getContactByType(String userId, String contactType) { User user = UserUtility.recreateUserState(repository, userId); return user.getContacts().stream() .filter(c -> c.getType().equals(contactType)) .collect(Collectors.toSet()); } public Set<Address> getAddressByRegion(String userId, String state) throws Exception { User user = UserUtility.recreateUserState(repository, userId); return user.getAddresses().stream() .filter(a -> a.getState().equals(state)) .collect(Collectors.toSet()); } } |
請注意,作為此處處理更新使用者操作的一部分,我們將生成多個事件。另外,有趣的是要注意我們如何通過重播到目前為止生成的所有域事件來生成域模型的當前狀態。
當然,在實際的應用程式中,這不是可行的策略,我們必須維護本地快取以避免每次生成狀態。事件儲存庫中還有其他策略(例如快照和聚合)可以加快此過程。
到此結束了我們在簡單應用程式中引入事件溯源的工作。
事件溯源好處和缺點
現在,我們已經成功採用了使用事件源儲存域物件的另一種方法。事件源是一種強大的模式,如果使用得當,它將為應用程式體系結構帶來很多好處:
- 由於不需要讀取,更新和寫入,因此使寫入操作快得多;寫只是將事件附加到日誌
- 消除了物件關係阻抗,從而消除了對複雜對映工具的需求;當然,我們仍然需要重新建立物件
- 恰好提供作為副產品的稽核日誌,這是完全可靠的;我們可以準確除錯域模型的狀態如何變化
- 它使支援時態查詢和實現時間旅行成為可能(過去某個時間點的域狀態)!
- 很自然地適合設計微服務架構中的鬆耦合元件,這些元件通過交換訊息進行非同步通訊
但是,像往常一樣,即使事件源也不是萬靈丹。它確實迫使我們採用截然不同的方式來儲存資料。在某些情況下,這可能沒有用:
- 有一個相關的學習曲線,並且採用事件源需要思維方式的轉變。首先是不直觀的
- 除非我們將狀態保留在本地快取中,否則它使處理典型查詢變得相當困難,因為我們需要重新建立狀態
- 儘管它可以應用於任何領域模型,但它更適合事件驅動的體系結構中基於事件的模型
事件溯源的CQRS
既然我們已經瞭解瞭如何將事件源和CQRS分別引入到我們的簡單應用程式中,是時候將它們組合在一起了。現在應該很直觀,因為這些模式可以相互受益。但是,在本節中我們將使其更加明確。
首先讓我們看看應用程式體系結構如何將它們組合在一起:
到目前為止,這應該不足為奇。我們已將儲存庫的寫端替換為事件儲存,而儲存庫的讀端仍然相同。
請注意,這不是在應用程式體系結構中使用事件源和CQRS的唯一方法。我們可以非常有創意,可以將這些模式與其他模式一起使用,並提供幾種架構選擇。
這裡重要的是確保我們使用它們來管理複雜性,而不是簡單地進一步增加複雜性!
我們將從引入CQRS的應用程式開始,然後進行相關更改以使事件源變得更加重要。我們還將利用在引入事件源的應用程式中定義的相同事件和事件儲存。
只有幾處更改。我們將首先更改聚合以生成事件,而不是更新state:
public class UserAggregate { private EventStore writeRepository; public UserAggregate(EventStore repository) { this.writeRepository = repository; } public List<Event> handleCreateUserCommand(CreateUserCommand command) { UserCreatedEvent event = new UserCreatedEvent(command.getUserId(), command.getFirstName(), command.getLastName()); writeRepository.addEvent(command.getUserId(), event); return Arrays.asList(event); } public List<Event> handleUpdateUserCommand(UpdateUserCommand command) { User user = UserUtility.recreateUserState(writeRepository, command.getUserId()); List<Event> events = new ArrayList<>(); List<Contact> contactsToRemove = user.getContacts().stream() .filter(c -> !command.getContacts().contains(c)) .collect(Collectors.toList()); for (Contact contact : contactsToRemove) { UserContactRemovedEvent contactRemovedEvent = new UserContactRemovedEvent(contact.getType(), contact.getDetail()); events.add(contactRemovedEvent); writeRepository.addEvent(command.getUserId(), contactRemovedEvent); } List<Contact> contactsToAdd = command.getContacts().stream() .filter(c -> !user.getContacts().contains(c)) .collect(Collectors.toList()); for (Contact contact : contactsToAdd) { UserContactAddedEvent contactAddedEvent = new UserContactAddedEvent(contact.getType(), contact.getDetail()); events.add(contactAddedEvent); writeRepository.addEvent(command.getUserId(), contactAddedEvent); } // similarly process addressesToRemove // similarly process addressesToAdd return events; } } |
唯一需要進行的其他更改是在投影儀中,它現在需要處理事件而不是域物件狀態:
public class UserProjector { UserReadRepository readRepository = new UserReadRepository(); public UserProjector(UserReadRepository readRepository) { this.readRepository = readRepository; } public void project(String userId, List<Event> events) { for (Event event : events) { if (event instanceof UserAddressAddedEvent) apply(userId, (UserAddressAddedEvent) event); if (event instanceof UserAddressRemovedEvent) apply(userId, (UserAddressRemovedEvent) event); if (event instanceof UserContactAddedEvent) apply(userId, (UserContactAddedEvent) event); if (event instanceof UserContactRemovedEvent) apply(userId, (UserContactRemovedEvent) event); } } public void apply(String userId, UserAddressAddedEvent event) { Address address = new Address( event.getCity(), event.getState(), event.getPostCode()); UserAddress userAddress = Optional.ofNullable( readRepository.getUserAddress(userId)) .orElse(new UserAddress()); Set<Address> addresses = Optional.ofNullable(userAddress.getAddressByRegion() .get(address.getState())) .orElse(new HashSet<>()); addresses.add(address); userAddress.getAddressByRegion() .put(address.getState(), addresses); readRepository.addUserAddress(userId, userAddress); } public void apply(String userId, UserAddressRemovedEvent event) { Address address = new Address( event.getCity(), event.getState(), event.getPostCode()); UserAddress userAddress = readRepository.getUserAddress(userId); if (userAddress != null) { Set<Address> addresses = userAddress.getAddressByRegion() .get(address.getState()); if (addresses != null) addresses.remove(address); readRepository.addUserAddress(userId, userAddress); } } public void apply(String userId, UserContactAddedEvent event) { // Similarly handle UserContactAddedEvent event } public void apply(String userId, UserContactRemovedEvent event) { // Similarly handle UserContactRemovedEvent event } } |
如果我們回想起在處理基於狀態的投影時所討論的問題,那麼這可能是一種解決方案。
基於事件的投影相當方便且易於實現。我們要做的就是處理所有發生的領域事件,並將它們應用於所有讀取的領域模型。通常,在基於事件的應用程式中,投影儀將收聽其感興趣的領域事件,而不依賴於直接呼叫它的人。
這是我們在簡單的應用程式中將事件源和CQRS整合在一起的全部工作。
詳細點選標題,可以在GitHub上找到本文的原始碼。
banq評:該文基於貧血領域模型,將原屬於充血領域模型的邏輯放到UserProjector中,這是其最大問題,不遵循DDD情況下的CQRS/ES無疑複雜化傳統的微服務架構,不值得推薦。但是作為了解入門可以學習。
相關文章
- CQRS+ES專案解析01-Diary.CQRS
- 從CRUD程式設計切換到事件溯源和區塊鏈程式設計程式設計事件區塊鏈
- 最全面的CQRS和事件溯源介紹 - Software House ASC事件
- .NET遵循CQRS-ES架構的EventFlow的DDD + CQRS + Event-sourcing原始碼架構原始碼
- 從入門到放棄 - 事件溯源事件
- GitHub - soooban/AxonDemo: 使用Axon/Spring Cloud實現事件溯源和CQRS案例GithubSpringCloud事件
- 使用AsyncAPI規範簡潔實現CQRS事件溯源案例API事件
- 無伺服器事件源和CQRS指南伺服器事件
- 使用Redis/RabbitMQ/EventStore實現事件溯源CQRS微服務應用 - Aram KoukiaRedisMQ事件微服務
- 從 CRUD 遷移到事件溯源的祕訣 - eventstore事件
- booking-microservices:基於.Net Core的CQRS、DDD、垂直切片架構、事件溯源案例ROS架構事件
- 經驗分享:從CRUD重構到事件源ES的有狀態系統 -Stitcher.io事件
- 在 Node.js 中設計一種 flexible 的模式(CQRS/ES/Onion) (譯)Node.jsFlex模式
- kakafka - 為CQRS而生fka - 為CQRS而生Kafka
- CQRS架構和Axon框架入門實踐架構框架
- 領域驅動設計:CQRS 和事件源的強大功能事件
- OpenGL ES 入門
- WIX是如何從CRUD轉換到Event Sourcing?
- ES6封裝MongoDB的CRUD封裝MongoDB
- 如何從 Docker Desktop 切換到 ColimaDocker
- 從單體架構轉向CQRS - Wu架構
- CQRS模式學習模式
- DDD 中的那些模式 — CQRS模式
- k8s 事件收集到esK8S事件
- lakeFS:實現類似於Git或事件溯源ES的物件儲存功能Git事件物件
- 微服務、CQRS和eventsourcing開源資源微服務
- 使用Spring Boot和Kafka Streams實現CQRSSpring BootKafka
- webpack4.0入門指南(一)安裝和轉換es6語法Web
- Java,Spring,SpringBoot和Axon實現CQRS深度示例 -jofisaes@gmail.comJavaSpring BootAI
- es6 入門筆記筆記
- ES6極速入門
- ES6快速入門(三)
- ES6快速入門(二)
- ES6 知識整理一(es6快速入門)
- 實戰:如何優雅的從 Skywalking 切換到 OpenTelemetry
- 新手學習Java,如何快速從入門到精通!Java
- 一款不錯的 Go Server/API boilerplate,使用 K8S+DDD+CQRS+ES+gRPC 最佳實踐構建GoServerAPIK8SRPC
- OpenGL/OpenGL ES 入門:基礎變換 - 初識向量/矩陣矩陣