Spring Data JDBC - 如何使用自定义ID生成

436 阅读5分钟

这是关于如何解决你在使用Spring Data JDBC时可能遇到的各种挑战的系列文章的第一篇。

如果你是Spring Data JDBC的新手,你应该先阅读它的介绍这篇文章,这篇文章解释了在Spring Data JDBC的背景下聚合体的相关性。相信我,这很重要。

这篇文章是基于我在2021年Spring One大会上的部分演讲

现在我们可以开始讨论ID了--特别是当你想控制一个实体的ID而不想把它留给数据库时,你有哪些选择。 但让我们首先重申Spring Data JDBC在这方面的默认策略。

默认情况下,Spring Data JDBC假设ID是由某种SERIALIDENTITY 、或AUTOINCREMENT 列生成的。它基本上会检查一个聚合根的ID是否是null0 ,对于原始数字类型,如果是,则假设该聚合是新的,对聚合根执行插入操作。 数据库会生成一个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用来确定聚合根是否是新的。只要版本是null0 ,对于原始类型,聚合被认为是新的,即使设置了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代码。

完整的示例代码可以在Spring Data示例库中找到

以后还会有更多类似的文章,如果你想让我介绍特定的主题,请告诉我。