这是关于如何解决你在使用Spring Data JDBC时可能遇到的各种挑战的系列文章的第一篇。
如果你是Spring Data JDBC的新手,你应该先阅读它的介绍和这篇文章,这篇文章解释了在Spring Data JDBC的背景下聚合体的相关性。相信我,这很重要。
这篇文章是基于我在2021年Spring One大会上的部分演讲。
现在我们可以开始讨论ID了--特别是当你想控制一个实体的ID而不想把它留给数据库时,你有哪些选择。 但让我们首先重申Spring Data JDBC在这方面的默认策略。
默认情况下,Spring Data JDBC假设ID是由某种SERIAL 、IDENTITY 、或AUTOINCREMENT 列生成的。它基本上会检查一个聚合根的ID是否是null 或0 ,对于原始数字类型,如果是,则假设该聚合是新的,对聚合根执行插入操作。 数据库会生成一个ID,该ID由Spring Data JDBC在聚合根中设置。如果ID不是null ,则假定聚合是现有的,并对聚合根执行更新。
考虑一个由单个简单类组成的简单聚合。
class Minion {
@Id
Long id;
String name;
Minion(String name) {
this.name = name;
}
}
进一步考虑一个默认的CrudRepository 。
interface MinionRepository extends CrudRepository<Minion, Long> {
}
存储库被自动连接到你的代码中,就像下面这一行。
@Autowired
MinionRepository minions;
下面的工作就很好。
Minion before = new Minion("Bob");
assertThat(before.id).isNull();
Minion after = minions.save(before);
assertThat(after.id).isNotNull();
但是接下来的这段话就不灵了。
Minion before = new Minion("Stuart");
before.id = 42L;
minions.save(before);
如前所述,Spring Data JDBC试图执行更新,因为ID已经被设置了。 然而,由于聚合体实际上是新的,更新语句影响了零行,Spring Data JDBC抛出一个异常。
有几种方法可以解决这个问题。 我找到了四种不同的方法,我把我认为最简单的方法先列出来,所以你可以在找到适合你的解决方案后停止阅读。 你可以以后再来阅读其他选项,提高你的Spring Data技能。
版本
给你的聚合属性添加一个版本属性。 我所说的 "版本属性 "是指用@Version 注释的属性。这种属性的主要目的是为了实现乐观锁定。然而,作为一个副作用,版本属性也会被Spring Data JDBC用来确定聚合根是否是新的。只要版本是null 或0 ,对于原始类型,聚合被认为是新的,即使设置了id 。
使用这种方法,你必须改变实体和(当然)模式,但没有其他东西。
另外,对于许多应用来说,乐观的锁定首先是一个很好的东西。
我们把原来的Minion ,变成一个VersionedMinion 。
class VersionedMinion {
@Id Long id;
String name;
@Version Integer version;
VersionedMinion(long id, String name) {
this.id = id;
this.name = name;
}
}
存储库和自动布线看起来与原来的例子基本相同。 有了这个改变,下面的结构就可以工作了。
VersionedMinion before = new VersionedMinion(23L, "Bob");
assertThat(before.id).isNotNull();
versionedMinions.save(before);
VersionedMinion reloaded = versionedMinions.findById(before.id).get();
assertThat(reloaded.name).isEqualTo("Bob");
模板
另一个让你对ID有意愿的方法是自己进行插入。你可以通过注入一个JdbcAggregateTemplate ,并调用JdbcAggregateTemplate.insert(T) 。JdbcAggregateTemplate 是存储库下面的一个抽象层,所以你使用存储库用于插入的相同代码,但你决定何时使用插入。
Minion before = new Minion("Stuart");
before.id = 42L;
template.insert(before);
Minion reloaded = minions.findById(42L).get();
assertThat(reloaded.name).isEqualTo("Stuart");
注意,我们使用的不是存储库,而是一个模板,它被注入了以下内容。
@Autowired
JdbcAggregateTemplate template;
EventListener
模板的方法对于你已经知道ID的情况非常好--例如,当你从另一个系统导入数据时,你想重复使用该系统的ID。
如果你不知道ID,也不想在你的业务代码中出现任何与ID相关的东西,使用回调可能是更好的选择。
回调是一个在某些生命周期事件中被调用的bean。对于我们的目的来说,正确的回调是BeforeSaveCallback 。它返回可能被修改的聚合根,所以它也适用于不可变的实体类。
在回调中,我们确定相关的聚合根是否需要一个新的ID。如果是的话,我们使用我们选择的算法来生成它。
我们使用另一种变体Minion
class StringIdMinion {
@Id
String id;
String name;
StringIdMinion(String name) {
this.name = name;
}
}
存储库和注入点仍然与原来的例子类似。 然而,我们在配置中注册了回调。
@Bean
BeforeSaveCallback<StringIdMinion> beforeSaveCallback() {
return (minion, mutableAggregateChange) -> {
if (minion.id == null) {
minion.id = UUID.randomUUID().toString();
}
return minion;
};
}
保存实体的代码现在看起来就像id ,是由数据库生成的。
StringIdMinion before = new StringIdMinion("Kevin");
stringions.save(before);
assertThat(before.id).isNotNull();
StringIdMinion reloaded = stringions.findById(before.id).get();
assertThat(reloaded.name).isEqualTo("Kevin");
可持久化
最后一个选择是让聚合根控制是否应该进行更新或插入。 你可以通过实现Persistable 接口(特别是isNew 方法)来做到这一点。最简单的方法是一直返回true ,从而迫使一直进行插入。 当然,当你想使用聚合根进行更新时,这并不可行。 在这种情况下,你需要想出一个更灵活的策略。
我们需要再次调整我们的Minion 。
class PersistableMinion implements Persistable<Long> {
@Id Long id;
String name;
PersistableMinion(Long id, String name) {
this.id = id;
this.name = name;
}
@Override
public Long getId() {
return id;
}
@Override
public boolean isNew() {
// this implementation is most certainly not suitable for production use
return true;
}
}
保存一个PersistableMinion 的代码看起来是一样的。
PersistableMinion before = new PersistableMinion(23L, "Dave");
persistableMinions.save(before);
PersistableMinion reloaded = persistableMinions.findById(before.id).get();
assertThat(reloaded.name).isEqualTo("Dave");
结论
Spring Data JDBC为你如何控制聚合体的ID提供了大量的选择。 虽然我在例子中使用了微不足道的逻辑,但没有什么能阻止你实现你想到的任何逻辑,因为它们都可以归结为相当基本的Java代码。
以后还会有更多类似的文章,如果你想让我介绍特定的主题,请告诉我。