这是关于如何解决在使用Spring数据JDBC时可能遇到的各种挑战的系列文章的第一篇文章。该系列包括
-
春季数据 JDBC - 如何使用自定义 ID 生成。(本文)。
如果您不熟悉春季数据JDBC,则应首先阅读其介绍和本文解释了聚合在春季数据 JDBC 上下文中的相关性.相信我,这很重要。
本文基于我在2021年春季一号做的演讲.
现在我们可以开始使用ID - 特别是当您想要控制实体的ID而不想将其留给数据库时,您的选项是什么。但是,让我们首先重申春季数据JDBC对此的默认策略。
默认情况下,弹簧数据 JDBC 假定 ID 由某种类型的SERIAL
、IDENTITY
或AUTOINCREMENT
列生成。它基本上检查聚合根的 ID 是基元数类型null
还是基元数类型0
。如果是null
,则假定聚合是新的,并为聚合根执行插入。数据库生成一个 ID,该 ID 由弹簧数据 JDBC 在聚合根中设置。如果 ID 不是null
,则假定聚合是现有 ID,并为聚合根执行更新。
考虑一个由单个简单类组成的简单聚合:
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 数据 JDBC 会尝试执行更新,因为 ID 已设置。但是,由于聚合实际上是新的,因此更新语句会影响零行,并且 Spring 数据 JDBC 会引发异常。
有几种方法可以解决这个问题。为此,我发现了四种不同的方法,我首先列出了我认为最简单的方法,因此一旦找到适合您的解决方案,您就可以停止阅读。您可以稍后再回来阅读其他选项并提高您的Spring数据技能。
版本
将版本属性添加到聚合属性。通过“版本属性”,我的意思是用@Version
注释的属性。此类属性的主要目的是启用乐观锁定。但是,作为副作用,版本属性也会被 Spring 数据 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;
事件接收器
模板方法非常适合您已经知道 ID 的情况 - 例如,当您从另一个系统导入数据并希望重用该系统的 ID 时。
如果您不知道 ID,并且不希望在业务代码中包含任何 ID,则使用回调可能是更好的选择。
回调是在某些生命周期事件中被调用的 Bean。对于我们的目的来说,正确的回调是BeforeConvertCallback
.它返回可能修改的聚合根,因此也适用于不可变的实体类。
在回调中,我们确定有问题的聚合根是否需要新的 ID。如果是这样,我们通过使用我们选择的算法来生成它。
我们使用的另一种变体Minion
class StringIdMinion {
@Id
String id;
String name;
StringIdMinion(String name) {
this.name = name;
}
}
存储库和注入点看起来仍然类似于原始示例。但是,我们在配置中注册回调:
@Bean
BeforeConvertCallback<StringIdMinion> beforeConvertCallback() {
return (minion) -> {
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");
结论
春季数据 JDBC 为如何控制聚合的 ID 提供了大量选项。虽然我在示例中使用了琐碎的逻辑,但没有什么能阻止您实现您想到的任何逻辑,因为它们都归结为非常基本的Java代码。