Spring Data JDBC如何對DDD聚合根進行部分更新? - spring.io

banq發表於2022-01-27

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

  1. Spring Data JDBC - 如何使用自定義 ID 生成。
  2. Spring Data JDBC - 我如何建立雙向關係?
  3. Spring Data JDBC - 如何實現快取?
  4. 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 ,會發生以下情況。

  1. 資料庫中該Minion 的所有玩具被刪除。
  2. Minion 自身被更新。
  3. 目前屬於該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中提供了完整的示例程式碼。

相關文章