飞道的博客

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

366人阅读  评论(0)

这是关于如何解决使用 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 JDBC 算法最大限度地减少了对数据库状态的假设。 如果聚合包含实体集合,则成本高昂。

为了举例说明发生了什么,我们再次转向小黄人。 这个小黄人有一套玩具。


    
  1. class Minion {
  2. @Id Long id;
  3. String name;
  4. Color color = Color.YELLOW;
  5. Set<Toy> toys = new HashSet<>();
  6. @Version int version;
  7. Minion(String name) {
  8. this.name = name;
  9. }
  10. @PersistenceConstructor
  11. private Minion(Long id, String name, Collection<Toy> toys, int version) {
  12. this.id = id;
  13. this.name = name;
  14. this.toys.addAll(toys);
  15. this.version = version;
  16. }
  17. Minion addToy(Toy toy) {
  18. toys.add(toy);
  19. return this;
  20. }
  21. }

这些类的架构如下所示:


    
  1. CREATE TABLE MINION
  2. (
  3. ID IDENTITY PRIMARY KEY,
  4. NAME VARCHAR(255),
  5. COLOR VARCHAR(10),
  6. VERSION INT
  7. );
  8. CREATE TABLE TOY
  9. (
  10. MINION BIGINT NOT NULL,
  11. NAME VARCHAR(255)
  12. );

存储库界面目前微不足道:

interface MinionRepository extends CrudRepository<Minion, Long> {}

如果我们保存数据库中已存在的工作节点,则会发生以下情况:

  1. 该小黄人数据库中的所有玩具都将被删除。

  2. 工作节点本身会更新。

  3. 当前属于该小黄人的所有玩具都将插入到数据库中。

当有很多玩具并且没有一个更改、删除或添加时,这是浪费。 但是,Spring Data JDBC没有任何关于这方面的信息,也不应该,为了简单起见。 此外,你可能知道的代码比Spring Data或任何其他工具或库知道的更多,你也许能够利用这些知识。 接下来的部分将介绍执行此操作的各种方法。

使用聚合根的简化视图

玩具是任何适当的小黄人不可或缺的一部分,但也许有些领域并不关心玩具。 如果是这样,映射到同一个表并没有错:PlainMinion


      
  1. @Table("MINION")
  2. class PlainMinion {
  3. @Id Long id;
  4. String name;
  5. @Version int version;
  6. }

由于它不了解玩具,因此可以单独使用它们,您可以通过测试进行验证:


      
  1. @SpringBootTest
  2. class SelectiveUpdateApplicationTests {
  3. @Autowired MinionRepository minions;
  4. @Autowired PlainMinionRepository plainMinions;
  5. @Test
  6. void renameWithReducedView() {
  7. Minion bob = new Minion("Bob")
  8. .addToy(new Toy("Tiger Duck"))
  9. .addToy(new Toy("Security blanket"));
  10. minions.save(bob);
  11. PlainMinion plainBob = plainMinions.findById(bob.id).orElseThrow();
  12. plainBob.name = "Bob II.";
  13. plainMinions.save(plainBob);
  14. Minion bob2 = minions.findById(bob.id).orElseThrow();
  15. assertThat(bob2.toys).containsExactly(bob.toys.toArray(new Toy[]{}));
  16. }
  17. }

只要确保在玩具和小黄人之间有一个外键,这样你就不会意外删除小黄人而不删除它的玩具。 此外,这仅适用于聚合根。聚合中的实体将被删除并重新创建,因此此类实体的简化视图中不存在的任何列都将重置为其默认值。

使用直接数据库更新

或者,您可以使用新的存储库方法编写更新:


      
  1. interface MinionRepository extends CrudRepository<Minion, Long> {
  2. @Modifying
  3. @Query("UPDATE MINION SET COLOR ='PURPLE', VERSION = VERSION +1 WHERE ID = :id")
  4. void turnPurple(Long id);
  5. }

您需要注意它绕过了Spring Data JDBC中的任何逻辑。 必须确保这不会给应用程序带来问题。 这种逻辑的一个例子是乐观锁定。 上面的语句负责乐观锁定,因此对工作节点执行其他操作的其他进程不会意外撤消颜色更改。 同样,如果您的实体具有审核列,则需要确保它们相应地更新。 如果您使用生命周期事件实体回调,您需要考虑是否以及如何模仿他们的行动。

使用自定义方法

许多Spring Data用户经常忽略的一种选择是实现自定义方法的选项,您可以在其中编写任何您想要或需要的内容。

为此,您可以让存储库扩展接口以包含要实现的方法:

interface MinionRepository extends CrudRepository<Minion, Long>, PartyHatRepository {}

      
  1. interface PartyHatRepository {
  2. void addPartyHat(Minion minion);
  3. }

然后为其提供一个具有相同名称但添加Impl的实现:


      
  1. class PartyHatRepositoryImpl implements PartyHatRepository {
  2. private final NamedParameterJdbcOperations template;
  3. public PartyHatRepositoryImpl(NamedParameterJdbcOperations template) {
  4. this.template = template;
  5. }
  6. @Override
  7. public void addPartyHat(Minion minion) {
  8. Map<String, Object> insertParams = new HashMap<>();
  9. insertParams.put("id", minion.id);
  10. insertParams.put("name", "Party Hat");
  11. template.update("INSERT INTO TOY (MINION, NAME) VALUES (:id, :name)", insertParams);
  12. Map<String, Object> updateParams = new HashMap<>();
  13. updateParams.put("id", minion.id);
  14. updateParams.put("version", minion.version);
  15. final int updateCount = template.update("UPDATE MINION SET VERSION = :version + 1 WHERE ID = :id AND VERSION = :version", updateParams);
  16. if (updateCount != 1) {
  17. throw new OptimisticLockingFailureException("Minion was changed before a Party Hat was given");
  18. }
  19. }
  20. }

在我们的示例中,我们执行多个 SQL 语句来添加玩具,并确保使用乐观锁定:


      
  1. @Test
  2. void grantPartyHat() {
  3. Minion bob = new Minion("Bob")
  4. .addToy(new Toy("Tiger Duck"))
  5. .addToy(new Toy("Security blanket"));
  6. minions.save(bob);
  7. minions.addPartyHat(bob);
  8. Minion bob2 = minions.findById(bob.id).orElseThrow();
  9. assertThat(bob2.toys).extracting("name").containsExactlyInAnyOrder("Tiger Duck", "Security blanket", "Party Hat");
  10. assertThat(bob2.name).isEqualTo("Bob");
  11. assertThat(bob2.color).isEqualTo(Color.YELLOW);
  12. assertThat(bob2.version).isEqualTo(bob.version+1);
  13. assertThatExceptionOfType(OptimisticLockingFailureException.class).isThrownBy(() -> minions.addPartyHat(bob));
  14. }

结论

Spring Data JDBC在这里让您在标准情况下的生活更轻松。 同时,如果您希望某些东西表现不同,它会尽量不妨碍您。 您可以选择在许多级别上实现所需的行为。

完整的示例代码可在Spring 数据示例存储库.


转载:https://blog.csdn.net/allway2/article/details/127671694
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场