Spring Data JDBC如何對DDD聚合根進行部分更新? - spring.io
這是有關如何應對使用 Spring Data JDBC 時可能遇到的各種挑戰的系列文章的第四篇。該系列包括:
- Spring Data JDBC - 如何使用自定義 ID 生成。
- Spring Data JDBC - 我如何建立雙向關係?
- Spring Data JDBC - 如何實現快取?
- Spring Data JDBC - 如何對聚合根進行部分更新?(本文)
如果您是 Spring Data JDBC 新手,您應該先閱讀介紹和這篇文章,其中解釋了 Spring Data JDBC 上下文中聚合的相關性。相信我。這很重要。
Spring Data JDBC 是圍繞聚合和儲存庫的思想構建的。儲存庫是查詢、載入、儲存和刪除聚合的類似集合的物件。聚合是具有緊密關係的物件叢集,並且只要程式控制超出其方法,它們就會在內部保持一致。因此,聚合也可以在一個原子操作中載入和持久化。
但是,Spring Data JDBC 不會跟蹤您的聚合是如何變化的。因此,用於持久化聚合的 Spring Data JDBCs 演算法最大限度地減少了對資料庫狀態的假設。如果您的聚合包含實體集合,則成本會很高。
舉個例子來說明會發生什麼,我們再次求助於 Minions。這個Minion 有一套玩具。
class Minion { @Id Long id; String name; Color color = Color.YELLOW; Set<Toy> toys = new HashSet<>(); @Version int version; Minion(String name) { this.name = name; } @PersistenceConstructor private Minion(Long id, String name, Collection<Toy> toys, int version) { this.id = id; this.name = name; this.toys.addAll(toys); this.version = version; } Minion addToy(Toy toy) { toys.add(toy); return this; } } |
這些類的架構如下所示:
CREATE TABLE MINION ( ID IDENTITY PRIMARY KEY, NAME VARCHAR(255), COLOR VARCHAR(10), VERSION INT ); CREATE TABLE TOY ( MINION BIGINT NOT NULL, NAME VARCHAR(255) ); |
儲存庫介面現在很簡單:
interface MinionRepository extends CrudRepository<Minion, Long> {} |
如果我們儲存一個已經存在於資料庫中的Minion ,會發生以下情況。
- 資料庫中該Minion 的所有玩具被刪除。
- Minion 自身被更新。
- 目前屬於該Minion 的所有玩具都被插入資料庫中。
當Minion 有許多玩具,而它們都沒有改變、被刪除或新增時,這就很浪費了。然而,Spring Data JDBC並沒有這方面的資訊,為了保持簡單,它也不應該有。另外,在你的程式碼中,你可能比Spring Data或任何其他工具或庫知道的更多,你可能會利用這些知識。接下來的章節描述了各種方法。
使用聚合根的縮小檢視
Minion 是任何適當的minion中不可缺少的部分,但也許有些領域並不關心Minion 。如果是這樣的話,讓PlainMinion對映到同一張表上並沒有什麼問題。
@Table("MINION") class PlainMinion { @Id Long id; String name; @Version int version; } |
由於它不知道玩具,所以它不去管它們,這一點你可以通過測試來驗證。
@SpringBootTest class SelectiveUpdateApplicationTests { @Autowired MinionRepository minions; @Autowired PlainMinionRepository plainMinions; @Test void renameWithReducedView() { Minion bob = new Minion("Bob") .addToy(new Toy("Tiger Duck")) .addToy(new Toy("Security blanket")); minions.save(bob); PlainMinion plainBob = plainMinions.findById(bob.id).orElseThrow(); plainBob.name = "Bob II."; plainMinions.save(plainBob); Minion bob2 = minions.findById(bob.id).orElseThrow(); assertThat(bob2.toys).containsExactly(bob.toys.toArray(new Toy[]{})); } } |
只要確保你在玩具和Minion 之間有一個外來鍵,這樣你就不會意外地刪除Minion 而不同時刪除其玩具。另外,這隻對聚合根起作用。聚合體內的實體會被刪除和重新建立,所以任何不存在於這種實體的簡化檢視中的列都會被重置為其預設值。
使用直接資料庫更新
另外,你可以直接在一個新的儲存庫方法中編寫你的更新。
interface MinionRepository extends CrudRepository<Minion, Long> { @Modifying @Query("UPDATE MINION SET COLOR ='PURPLE', VERSION = VERSION +1 WHERE ID = :id") void turnPurple(Long id); } |
你需要注意的是,它繞過了Spring Data JDBC中的任何邏輯。你必須確保這不會給你的應用程式帶來問題。這種邏輯的一個例子是樂觀鎖。上面的語句處理了樂觀鎖定,所以其他對Minion做其他事情的程式不會意外地撤銷顏色的改變。同樣地,如果你的實體有審計列,你需要確保它們得到相應的更新。如果你使用生命週期事件或實體回撥,你需要考慮是否以及如何模仿它們的動作。
使用自定義方法
許多Spring Data使用者經常忽略的一個選擇是實現一個自定義方法,你可以為你的目的編寫任何你想要或需要的程式碼。
為此,你可以讓你的儲存庫擴充套件一個介面來包含你想實現的方法。
interface MinionRepository extends CrudRepository<Minion, Long>, PartyHatRepository {} interface PartyHatRepository { void addPartyHat(Minion minion); } class PartyHatRepositoryImpl implements PartyHatRepository { private final NamedParameterJdbcOperations template; public PartyHatRepositoryImpl(NamedParameterJdbcOperations template) { this.template = template; } @Override public void addPartyHat(Minion minion) { Map<String, Object> insertParams = new HashMap<>(); insertParams.put("id", minion.id); insertParams.put("name", "Party Hat"); template.update("INSERT INTO TOY (MINION, NAME) VALUES (:id, :name)", insertParams); Map<String, Object> updateParams = new HashMap<>(); updateParams.put("id", minion.id); updateParams.put("version", minion.version); final int updateCount = template.update("UPDATE MINION SET VERSION = :version + 1 WHERE ID = :id AND VERSION = :version", updateParams); if (updateCount != 1) { throw new OptimisticLockingFailureException("Minion was changed before a Party Hat was given"); } } } |
在我們的例子中,我們執行多條SQL語句來新增一個玩具,同時也確保使用樂觀鎖。
@Test void grantPartyHat() { Minion bob = new Minion("Bob") .addToy(new Toy("Tiger Duck")) .addToy(new Toy("Security blanket")); minions.save(bob); minions.addPartyHat(bob); Minion bob2 = minions.findById(bob.id).orElseThrow(); assertThat(bob2.toys).extracting("name").containsExactlyInAnyOrder("Tiger Duck", "Security blanket", "Party Hat"); assertThat(bob2.name).isEqualTo("Bob"); assertThat(bob2.color).isEqualTo(Color.YELLOW); assertThat(bob2.version).isEqualTo(bob.version+1); assertThatExceptionOfType(OptimisticLockingFailureException.class).isThrownBy(() -> minions.addPartyHat(bob)); } |
結論
Spring Data JDBC 在標準情況下可以讓您的生活更輕鬆。同時,如果您希望某些東西表現不同,它會盡量不妨礙您。您可以選擇在許多級別上實現所需的行為。
Spring Data Example repository中提供了完整的示例程式碼。
相關文章
- 使用Spring Data JDBC實現DDD聚合SpringJDBC
- Spring Data 2021.0增加了對DDD聚合更多自動支援!Spring
- DDD之4聚合和聚合根
- 使用Spring Data JPA實現DDD聚合的動態投影Spring
- DDD中聚合、聚合根的含義以及作用
- 如何使用Spring Data進行一個實體中一部分資料的更新? | BaeldungSpring
- Spring Data Moore有哪些新功能? - spring.ioSpring
- DDD | 04-什麼是聚合根
- Spring Data JDBC: 對映無ID列的表SpringJDBC
- Spring Data JDBC介紹SpringJDBC
- 通過 Spring 框架如何進行JDBC操作呢?Spring框架JDBC
- Spring Data JDBC參考文件SpringJDBC
- 在Spring Data MongoDB中實現關係建模 - spring.ioSpringMongoDB
- Spring04——Spring操作JdbcTemplate進行JDBC操作SpringJDBC
- Spring Data JDBC參考文件 三SpringJDBC
- 使用Project Reactor進行反應式資料流 - spring.ioProjectReactSpring
- Spring 對JDBC的支援SpringJDBC
- spring data mongodb 如何以事物的方式進行增刪改查SpringMongoDB
- Spring Boot整合Spring Data JPA進行資料庫操作Spring Boot資料庫
- 使用Spring Data JPA進行資料庫操作Spring資料庫
- 使用更新補丁對Android Studio進行更新。Android
- 關於聚合根,領域事件的那點事---深入淺出理解DDD事件
- Spring Cloud Stream事件路由 - spring.ioSpringCloud事件路由
- Spring Cloud Gateway入門 - spring.ioSpringCloudGateway
- 可落地的DDD(4)-如何利用DDD進行微服務的劃分(2)微服務
- DDD聚合設計原則
- hibernate進行JDBC批量新增JDBC
- 如何對Spring MVC中的Controller進行單元測試SpringMVCController
- 領域設計:聚合與聚合根
- DDD的實體、值物件、聚合根的基類和介面:設計與實現物件
- Spring系列之JDBC對不同資料庫異常如何抽象的?SpringJDBC資料庫抽象
- 重要版本Spring Boot 2.3.0釋出 - spring.ioSpring Boot
- 利用Data Vault對資料倉儲進行建模(二)
- 【Spring】jdbcSpringJDBC
- DDD聚合五種設計方法
- REST:使用PATCH進行部分更新 - mscharhagREST
- Spring Boot無法對Service進行注入Spring Boot
- RSocket入門:Spring Boot伺服器 -Spring.ioSpring Boot伺服器