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


这是有关如何应对使用 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中提供了完整的示例代码。