在这一课中,我们将介绍一对多的关系,并学习orphan removal属性。
为了显示一对多的关系,我们将模拟许多球员可以注册参加一个锦标赛的情况。我们将创建一个锦标赛(tournament)表和一个注册(registration)表来模拟这种关系。
单向的一对多关系意味着只有一方维护关系细节。因此,给定一个Tournament实体,我们可以找到Registration人数,但我们不能从Registration实体中找到Tournament的细节。

一对多的单向关系
为了模拟一对多的关系,创建一个新的包onetomany.uni,并定义一个有三个字段的Tournament类:id、name和location。id字段是主键。我们还可以保存其他细节,如比赛发生的日期,比赛的场地类型,以及比赛的轮数等。
package io.datajek.databaserelationships.onetomany.uni;
@Entity
public class Tournament {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String name;
private String location;
//getters and setters
//constructor
//toString method
}
接下来,定义Registration类,目前只有一个字段,id。id字段是该表的主键。我们以后会添加更多的字段。
Registration类可以存储关于注册日期的信息,球员注册的比赛类型(单打/双打),以及分配给球员的等级(种子)等。
package io.datajek.databaserelationships.onetomany.uni;
@Entity
public class Registration {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private int id;
//getters and setters
//constructor
//toString method
}
<aside> 📢 由于球员注册参加比赛,注册对象应该与球员对象相关联。我们将在下一课中讨论这种关联。
</aside>
现在,我们将更新Tournament类以显示注册情况。由于一个锦标赛可以有多个注册者,我们将添加一个Registration的List作为一个新的字段。
public class Tournament {
//...
private List<Registration> registrations = new ArrayList<>();
//generate getter and setter methods
//update constructor & toString()
}
@OneToManyTournament类与Registration类有一对多的关系,因为一个锦标赛可以有多个注册者。这可以通过@OneToMany注解来模拟。在一对多的关系中,一方的主键被置于多方的外键中。
@JoinColumn注解表明这是关系的所有方。 tournament_id将被添加为registration表中的外键列。
@OneToMany
@JoinColumn(name="tournament_id")
private List<Registration> registrations = new ArrayList<>();

@JoinColumn注解
<aside>
📢 在没有@JoinColumn注解的情况下,Hibernate为一对多的关系创建一个包含两个表的主键的连接表。
</aside>
如果应用程序被运行,它会创建如下所示的数据库结构。这里tournament_id是外键列。我们可以使用H2控制台(在http://localhost:8080/h2-console,连接URL为jdbc:h2:mem:testdb)来验证这一点。

表结构
接下来,我们将为这种关系选择级联类型。当一个锦标赛被删除时,我们将同时删除相关的注册。这可以通过选择CascadeType.ALL来实现。
@OneToMany(cascade=CascadeType.ALL)
@JoinColumn(name="tournament_id")
private List<Registration> registrations = new ArrayList<>();
为了建立锦标赛和注册之间的关联,我们将在Tournament类中添加一个方法,将Registration对象分配给Tournament对象。
public void addRegistration(Registration reg) {
registrations.add(reg);
}
现在我们将在适当的包中为Registration和Tournament创建数据接入层、服务层和控制器类。数据接入层被命名为TournamentRepository和RegistrationRepository,并扩展了JpaRepository接口。
REST控制器TournamentController和RegistrationController的@RequestMapping分别为/tournaments和/registrations。控制器类调用服务层类TournamentService和RegistrationService中的方法。
所有上述的接口和类都显示在下面的代码压缩包中。
我们需要在TournamentController类中设置一个PUT映射,将一个注册信息分配给一个锦标赛。带有/{id}/registrations/{registration_id}映射的addRegistration方法将一个带有registration_id的注册加入到以id为键的锦标赛中。
@PutMapping("/{id}/registrations/{registration_id}")
public Tournament addRegistration(@PathVariable int id, @PathVariable int registration_id) {
Registration registration = registrationService.getRegistration(registration_id);
System.out.println(registration);
return service.addRegistration(id, registration);
}
TournamentService类中相应的服务层方法显示。
public Tournament addRegistration(int id, Registration registration) {
Tournament tournament = repo.findById(id).get();
tournament.addRegistration(registration);
return repo.save(tournament);
}
此时的所有代码:
为了测试应用程序,首先使用以下POST请求向/tournaments添加两个锦标赛。
{
"name": "Canadian Open",
"location": "Toronto"
}
{
"name": "US Open",
"location": "New York City"
}
接下来,我们将通过向/registrations发送带有空主体的POST请求来添加四个注册项目。
{}
在这四个注册中,我们将把一个与第一场比赛相关联,三个与第二场比赛相关联。这可以通过发送以下PUT请求来实现。
http://localhost:8080/tournaments/1/registrations/3
http://localhost:8080/tournaments/2/registrations/1
http://localhost:8080/tournaments/2/registrations/2
http://localhost:8080/tournaments/2/registrations/4
对/tournaments的GET请求显示了锦标赛和他们的注册情况。这一点也可以通过H2控制台进行验证。

锦标赛与注册信息
如果我们通过向/tournaments/2发送DELETE请求来删除id为2的锦标赛,那么这个锦标赛和它的三个注册信息就会被删除。注册表只剩下一个注册信息。

在删除ID为2的锦标赛
孤儿记录是指有外键值的记录,它指向一个不再存在的主键值。孤儿记录索引缺乏引用完整性,这意味着表中的数据处于不一致状态。
在我们的例子中,注册记录有一个外键值tournament_id。我们可以通过破坏两者之间的关联,将注册从锦标赛中删除。在这种情况下,registration表中的记录将成为一个孤儿,因为它不再与锦标赛表中的任何条目有联系。下图显示了一个孤儿记录。

为了演示这个概念,我们在业务层中创建一个方法removeRegistration,这个方法将打破Tournament和Registration对象之间的关联。
public void removeRegistration(Registration reg) {
if (registrations != null)
registrations.remove(reg);
}
我们将在TournamentController类中创建一个新的PUT映射/tournaments/{id}/remove_registrations/{registration_id}。removeRegistration方法将以registraion_id为键的注册实体从使用id指定的Tournament实体中移除。
@PutMapping("/{id}/remove_registrations/{registration_id}")
public Tournament removeRegistration(@PathVariable int id, @PathVariable int registration_id) {
Registration registration = registrationService.getRegistration(registration_id);
return service.removeRegistration(id, registration);
}
注意,控制器调用服务层方法,removeRegistration,它只是间接调用Tournament类的removeRegistration方法。
级联类型 REMOVE 只会将删除操作级联到与父记录相关的子记录上。为了展示它是如何工作的,我们将创建与之前相同的场景(有2个锦标赛和4个注册,通过将一个注册分配给第一个锦标赛,三个注册分配给第二个锦标赛)。
在做了上述改动后,再次运行应用程序,创建两个锦标赛和四个注册。然后如上所述将注册与两个锦标赛相关联。
我们将通过向/tournaments/2/remove_registrations/4发送一个PUT请求,从id为2的锦标赛中删除一个注册。现在这个锦标赛还剩下两个注册信息。注意,我们没有删除注册,只是将其从锦标赛中移除。该注册记录没有与任何锦标赛相关联,是一个孤儿记录。
数据库的当前状态反映在对/tournaments和/registrations的GET请求的响应中,如下所示。

从锦标赛2中删除一个注册后
接下来,通过向/tournaments/2发送DELETE请求来删除该锦标赛。删除操作被级联到注册表,与该锦标赛相关的两条记录被删除。如果我们对/registrations进行GET,我们可以看到id为4的无主记录仍然在表中。

注册表包含一条无主记录
orphanRemoval属性@OneToMany注解有一个orphanRemoval属性,可以用来删除已经成为孤儿的记录。