領先一步
VMware 提供培訓和認證,以加速您的進展。
了解更多這是關於如何應對在使用 Spring Data JDBC 時可能遇到的各種挑戰的系列文章中的第四篇。該系列包括:
Spring Data JDBC - 如何對聚合根執行部分更新? (本文)
如果您不熟悉 Spring Data JDBC,您應該從閱讀 介紹 和 這篇文章,其中解釋了聚合在 Spring Data JDBC 上下文中的相關性。相信我,這很重要。
Spring Data JDBC 是圍繞聚合和 repositories 的概念建立的。Repositories 類似於集合的物件,可以尋找、載入、儲存和刪除聚合。聚合是一組物件,它們具有緊密的關係,並且在程式控制在其方法之外時,它們在內部是一致的。因此,聚合也會在一個原子操作中一起載入和持久化。
但是,Spring Data JDBC 不會追蹤您的聚合如何變更。因此,Spring Data JDBC 用於持久化聚合的演算法會盡可能減少對資料庫狀態的假設。如果您的聚合包含一個實體集合,則這會付出很大的代價。
為了舉例說明會發生什麼,我們再次求助於 Minions。這個 Minion 有一組玩具 (Toys)。
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;
}
}
這些類別的 Schema 如下所示:
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)
);
現在,repository 介面非常簡單:
interface MinionRepository extends CrudRepository<Minion, Long> {}
如果我們儲存一個已經存在於資料庫中的 Minion,會發生以下情況:
該 Minion 在資料庫中的所有玩具都會被刪除。
Minion 本身會被更新。
目前屬於該 Minion 的所有玩具都會被插入到資料庫中。
當有很多玩具並且沒有一個玩具被更改、刪除或新增時,這是一種浪費。但是,Spring Data JDBC 沒有任何關於此的資訊,並且為了保持簡單,它也不應該有。此外,您可能比 Spring Data 或任何其他工具或程式庫更了解您的程式碼,並且您也許可以利用這些知識。以下各節描述了各種方法來做到這一點。
玩具是任何適當的 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,同時也刪除了它的玩具。此外,這僅適用於聚合根。聚合內的實體會被刪除和重新建立,因此此類實體的簡化檢視中不存在的任何欄位都會重設為其預設值。
或者,您可以在新的 repository 方法中撰寫您的更新:
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 使用者經常忽略的一個選項是實作一個自訂方法,您可以在其中編碼您想要的或需要的任何內容以達到您的目的。
為此,讓您的 repository 擴展一個介面,以包含您想要實作的方法:
interface MinionRepository extends CrudRepository<Minion, Long>, PartyHatRepository {}
interface PartyHatRepository {
void addPartyHat(Minion minion);
}
然後為它提供一個實作,其名稱相同,但新增了 Impl
:
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 中找到。
將會有更多類似的文章。如果您希望我涵蓋特定主題,請告訴我。