Spring Data JDBC - 如何建立双向关系?

工程 | Jens Schauder | 2021年9月22日 | ...

这是关于如何解决使用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上下文中重要性的文章。相信我,这很重要。

本文基于我在2021年Spring One大会上的演讲的一部分。

Spring Data JDBC对双向关系没有特殊支持。为了理解为什么你实际上不需要任何特殊支持,我们必须查看两种不同类型的关系:我们区分聚合内部的引用和跨聚合的引用。

内部引用

我们首先来看聚合内部的引用。这些引用在Spring Data JDBC中通过实际的Java引用建模。这些引用总是从聚合根指向聚合内部的实体。实际上,引用是从更接近聚合根的实体指向更深层次的实体。但相同的论点适用,所以我们只考虑聚合根和一个内部实体。

如果你遵循DDD的理念和规则,你永远不会直接访问内部实体。相反,当你想要操作内部实体时,你会在聚合根上调用一个方法,然后聚合根会在内部实体上调用适当的方法。如果方法需要一个对聚合根的引用,你只需在调用内部实体上的方法时将其传递过去。中间实体也是如此。

但是也许你有许多这样的方法,并且不想在所有地方都传递this。在这种情况下,你只需在聚合的构建过程中而不是在方法调用期间传递引用。这只是普通的Java代码,没有什么特别之处。

举个例子,考虑一个Minion和它的ToyToy应该有一个对Minion的反向引用,这样它就可以告诉主人姓名。Minion将自己设置为所有玩具的主人。

class Minion {
	@Id
	Long id;
	String name;
	final Set<Toy> toys = new HashSet<>();

	Minion(String name) {
		this.name = name;
	}

	@PersistenceConstructor
	private Minion(Long id, String name, Collection<Toy> toys) {

		this.id = id;
		this.name = name;
		toys.forEach(this::addToy);
	}

	public void addToy(Toy toy) {
		toys.add(toy);
		toy.minion = this;
	}

	public void showYourToys() {
		toys.forEach(Toy::sayHello);
	}
}

class Toy {
	String name;

	@Transient // org.SPRINGframework.DATA...
	Minion minion;

	Toy(String name) {
		this.name = name;
	}

	public void sayHello() {
		System.out.println("I'm " + name + " and I'm a toy of " + minion.name);
	}
}

请注意,你需要使用Spring Data注解(而不是JPA注解)将这些反向引用标记为@Transient。否则,Spring Data JDBC会尝试持久化它们,这将导致无限循环。

外部引用

对于聚合之间的引用,情况甚至更简单。这些引用不是通过Java引用实现的,而是通过使用被引用聚合的id实现的,可以选择包装在AggregateReference中。

导航这样的引用转换为使用目标聚合的仓库及其findById方法。例如,一个Minion可能引用它的邪恶主人,一个Person

class Minion {
	@Id
	Long id;
	String name;
	AggregateReference<Person, Long> evilMaster;

	Minion(String name, AggregateReference<Person, Long> evilMaster) {
		this.name = name;
		this.evilMaster = evilMaster;
	}
}

class Person {
	@Id
	Long id;
	String name;

	Person(String name) {
		this.name = name;
	}
}

给定一个Minion,你现在可以加载它的邪恶主人。

@Autowired
PersonRepository persons;

//...

Minion minion = //...

Optional<Person> evilMaster = persons.findById(minion.evilMaster.getId());

为了在相反方向导航关系,你在MinionRepository中声明一个方法,该方法为给定的邪恶主人查找适当的minions。

interface MinionRepository extends CrudRepository<Minion, Long> {

	@Query("SELECT * FROM MINION WHERE EVIL_MASTER = :id")
	Collection<Minion> findByEvilMaster(Long id);
}

@Autowired
MinionRepository minions;

//...

Person evilMaster = // ...

Collection<Minion>findByEvilMaster(evilMaster.id);

使用Spring Data JDBC 2.3,你不再需要使用@Query注解,因为查询派生支持AggregateReference作为参数类型。

结论

虽然Spring Data JDBC没有对双向关系的显式支持,但事实证明你不需要特殊支持。你所需要的只是现有功能和标准Java代码。完整的示例代码可在Spring Data示例仓库中找到。这里有一个内部引用的例子还有一个外部引用的例子

还会有更多类似的文章。如果您希望我涵盖特定主题,请告诉我。

获取 Spring 新闻通讯

通过 Spring 新闻通讯保持联系

订阅

领先一步

VMware 提供培训和认证,助您加速进步。

了解更多

获得支持

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件,只需一份简单的订阅。

了解更多

即将举行的活动

查看 Spring 社区所有即将举行的活动。

查看所有