Spring Data JDBC - 如何對聚合根進行區域性更新?

工程 | Jens Schauder | 2022年1月20日 | ...

這是關於如何應對使用Spring Data JDBC時可能遇到的各種挑戰系列文章的第四篇。該系列文章包括:

  1. Spring Data JDBC - 如何使用自定義ID生成。

  2. Spring Data JDBC - 如何建立雙向關係?

  3. Spring Data JDBC - 如何實現快取?

  4. Spring Data JDBC - 如何對聚合根進行區域性更新? (本文)

  5. Spring Data JDBC - 如何為我的領域模型生成Schema?

如果您是Spring Data JDBC的新手,您應該從閱讀簡介這篇解釋聚合在Spring Data JDBC背景下重要性的文章開始。相信我,這很重要。

Spring Data JDBC圍繞聚合和倉庫的思想構建。倉庫是類似集合的物件,用於查詢、載入、儲存和刪除聚合。聚合是緊密相關並在程式控制在其方法之外時保持內部一致的物件叢集。因此,聚合也以一個原子操作被一起載入和持久化。

然而,Spring Data JDBC並不跟蹤您的聚合如何變化。因此,Spring Data JDBC持久化聚合的演算法最大限度地減少了對資料庫狀態的假設。如果您的聚合包含實體集合,這會很耗費資源。

為了展示會發生什麼,我們再次以小黃人(Minions)為例。這個小黃人有一套玩具(Set of 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)
);

目前,倉庫介面很簡單:

interface MinionRepository extends CrudRepository<Minion, Long> {}

如果我們儲存一個數據庫中已經存在的小黃人,會發生以下情況:

  1. 資料庫中該小黃人的所有玩具都會被刪除。

  2. 小黃人本身會被更新。

  3. 目前屬於該小黃人的所有玩具都會被插入到資料庫中。

當玩具很多,但它們都沒有改變、刪除或新增時,這種做法是浪費的。然而,Spring Data JDBC沒有關於這些的資訊,為了保持其簡單性,它也不應該有。此外,在您的程式碼中,您可能比Spring Data或任何其他工具或庫瞭解得更多,您可能能夠利用這些知識。接下來的部分將描述實現這一點的各種方法。

使用聚合根的精簡檢視

玩具是任何合格小黃人不可或缺的一部分,但也許有些領域不關心玩具。如果是這樣,使用對映到同一張表的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[]{}));
	}
}

請確保在玩具和小黃人之間有一個外部索引鍵,這樣您就不會在不刪除其玩具的情況下意外刪除小黃人。此外,這僅適用於聚合根。聚合內部的實體會被刪除並重新建立,因此任何未出現在此類實體的精簡檢視中的列都將重置為其預設值。

使用直接資料庫更新

或者,您可以在新的倉庫方法中編寫更新:

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中的任何邏輯。您必須確保這不會給您的應用程式帶來問題。這種邏輯的一個例子是樂觀鎖定。上面的語句處理了樂觀鎖定,因此執行其他操作的程序不會意外撤銷顏色更改。同樣,如果您的實體有審計列,您需要確保它們得到相應更新。如果您使用生命週期事件實體回撥,您需要考慮是否以及如何模擬其操作。

使用自定義方法

許多Spring Data使用者常常忽略的一種替代方法是實現一個自定義方法,您可以在其中編寫您想要或需要的任何程式碼以滿足您的目的。

為此,您讓您的倉庫擴充套件一個介面,以包含您想要實現的方法:

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示例倉庫中找到。

還會有更多類似的文章。如果您希望我涵蓋特定主題,請告訴我。

獲取 Spring 新聞通訊

透過 Spring 新聞通訊保持聯絡

訂閱

領先一步

VMware 提供培訓和認證,助您加速進步。

瞭解更多

獲得支援

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位制檔案,只需一份簡單的訂閱。

瞭解更多

即將舉行的活動

檢視 Spring 社群所有即將舉行的活動。

檢視所有