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 社区所有即将举行的活动。

查看所有