在这一课中,我们将介绍一对多的关系,并学习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()
}
@OneToMany
Tournament
类与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
属性,可以用来删除已经成为孤儿的记录。