保持领先
VMware 提供培训和认证,以加速您的进步。
了解更多这是关于如何解决使用 Spring Data JDBC 时可能遇到的各种挑战系列的第四篇文章。该系列包括:
Spring Data JDBC - 如何对聚合根进行局部更新?(本文)
如果您是 Spring Data JDBC 的新手,应该先阅读介绍以及这篇文章,它解释了聚合在 Spring Data JDBC 上下文中的相关性。相信我,这很重要。
Spring Data JDBC 围绕聚合(Aggregate)和仓库(Repository)的概念构建。仓库是类似集合的对象,用于查找、加载、保存和删除聚合。聚合是对象集群,它们之间关系紧密,并且在程序控制位于其方法之外时内部保持一致。因此,聚合也作为一个原子操作一起加载和持久化。
然而,Spring Data JDBC 不跟踪您的聚合如何变化。因此,Spring Data JDBC 持久化聚合的算法最小化了对数据库状态的假设。如果您的聚合包含实体集合,这会很耗费资源。
为了举例说明会发生什么,我们再次以小黄人(Minions)为例。这个小黄人有一套玩具(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> {}
如果我们保存一个数据库中已经存在的小黄人,会发生以下情况:
该小黄人在数据库中的所有玩具都会被删除。
小黄人本身会被更新。
目前属于该小黄人的所有玩具都会被插入到数据库中。
当玩具很多且没有变化、没有删除或添加时,这样做会很浪费。然而,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 中的任何逻辑。您必须确保这不会对您的应用程序造成问题。这种逻辑的一个例子是乐观锁。上面的语句处理了乐观锁,这样其他正在对 Minion 进行操作的进程就不会意外撤销颜色更改。类似地,如果您的实体有审计列,您需要确保它们得到相应的更新。如果您使用生命周期事件或实体回调,您需要考虑如何模拟它们的操作。
许多 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 示例仓库中找到。
还会有更多类似的文章。如果您希望我涵盖特定主题,请告诉我。