JPA中多对多的关系,可以使用注解@ManyToMany
, @OneToMany
, 和 @ManyToOne
:
主要是分三大类,即:
- 关联表有自己的主键(即单个主键)
- 关联表是组合主键
- 不创建关联表
【具体来讲】
-
关联表有自己的主键(即单个主键):
关联表有自己的主键(即单个主键)
-
关联表是组合主键:
关联表是组合主键
-
不创建关联表:
不创建关联表
1. 关联表有自己的主键(即单个主键)
【数据模型】
医生(doctor
)和病人(patient
),一个病人可以有多个医生,医生可以为不同的病人看病。
单个主键的意思是关联表doctor_patient_rel
有自己单独的主键id,doctor_id
和patient_id
仅仅作为外键:

1. @ManyToOne:单向关联
doctor_id
和patient_id
是这张表的外键。分别关联到Doctor
和Patient实体
,用@ManyToOne
表示。
@ManyToOne
默认的fetch类型为EAGER
,即一起取出数据。为了避免效率问题,这里用了fetch类型为LAZY
,即只有当调用到这个属性的get方法,才会去加载这个属性的数据。
@JoinColumn
表示外键的列,如果是不声明,那默认的名字是属性名+"_"+列名。
@Entity
@Table(name = "doctor_patient_rel")
public class DoctorPatientRel {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "doctor_id")
private Doctor doctor;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "patient_id")
private Patient patient;
@Column(name = "create_date")
private Date createDate;
}
因为是单向,所以一方没有任何关联:
@Entity
@Table(name = "doctor")
public class Doctor {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String name;
}
@Entity
@Table(name = "patient")
public class Patient {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String name;
}
【测试a】在多方尝试查询数据,可以返回三张表的数据,因为是LAZY,所以需要在查询的时候加上@Transactional确保session处于open状态:
@Transactional
@Test
public void queryTest() {
DoctorPatientRel relationship = doctorPatientRelRepository.findById(1).get();
System.out.println(relationship.getDoctor());
System.out.println(relationship.getPatient());
}
sql,在doctorPatientRelRepository.findById(int)时,只会返回本表数据:
select
doctorpati0_.id as id1_4_0_,
doctorpati0_.create_date as create_d2_4_0_,
doctorpati0_.doctor_id as doctor_i3_4_0_,
doctorpati0_.patient_id as patient_4_4_0_
from
doctor_patient_rel doctorpati0_
where
doctorpati0_.id=?
当调用relationship.getDoctor()
,会查询doctor表:
select
doctor0_.id as id1_3_0_,
doctor0_.name as name2_3_0_
from
doctor doctor0_
where
doctor0_.id=?
当调用relationship.getPatient()
,会查询patient表:
select
patient0_.id as id1_7_0_,
patient0_.name as name2_7_0_
from
patient patient0_
where
patient0_.id=?
【测试b】由于是单向关系,doctor, patient那边并没有配置,所以查询也只能查到自己的数据。
【测试c】插入,在单向关系中,需要各自插入自己的数据:
@Test
public void saveTest() {
Doctor doctor = Doctor.builder().name("bill").build();
doctorRepository.save(doctor);
Patient patient = Patient.builder().name("p1").build();
patientRepository.save(patient);
DoctorPatientRel relationship = DoctorPatientRel.builder()
.doctor(doctor).patient(patient).createDate(new Date()).build();
doctorPatientRelRepository.save(relationship);
}
【测试d】删除,在单向关系中,需要各自删除自己的数据。
比如在多方删除数据,也只能删除doctor_patient_rel
表的数据,并不会删除doctor或patient的数据:
@Test
public void deleteTest() {
doctorPatientRelRepository.deleteById(1);
}
1.2 使用@ManyToOne和@OneToMany进行双向关联
DoctorPatientRel类,同#1.1。
双向关联,所以doctor和patient会有一对多的声明:
public class Doctor {
...
@OneToMany(mappedBy = "doctor", cascade = CascadeType.ALL)
private Set<DoctorPatientRel> doctorPatientRels = new HashSet<>();
}
public class Patient {
...
@OneToMany(mappedBy = "patient", cascade = CascadeType.ALL)
private Set<DoctorPatientRel> doctorPatientRels = new HashSet<>();
}
【测试a】在多方查询数据,和#1.1一样,sql会分成三个(select后的列省略):
select ...
from doctor_patient_rel doctorpati0_
where doctorpati0_.id=?
select ...
from doctor doctor0_
where doctor0_.id=?
select ...
from patient patient0_
where patient0_.id=?
【测试b】在一方doctor中查询,因为是双向关联,所以也会查到doctor_patient_rel的数据:
@Transactional
@Test
public void queryTest() {
Doctor doctor = doctorRepository.findById(1).get();
doctor.getDoctorPatientRels()
}
select
doctor0_.id as id1_3_0_,
doctor0_.name as name2_3_0_
from
doctor doctor0_
where
doctor0_.id=?
select
doctorpati0_.doctor_id as doctor_i3_4_0_,
doctorpati0_.id as id1_4_0_,
doctorpati0_.id as id1_4_1_,
doctorpati0_.create_date as create_d2_4_1_,
doctorpati0_.doctor_id as doctor_i3_4_1_,
doctorpati0_.patient_id as patient_4_4_1_
from
doctor_patient_rel doctorpati0_
where
doctorpati0_.doctor_id=?
【测试c】在多方插入数据,和#1.1一样,可以先分别调用doctorRepository和patientRepository插入doctor和patient的数据,再插入doctor_patient_rel的数据。
【测试d】在一方插入数据,可以自动插入多方的数据,可以看到并没有通过doctorPatientRelRepository插入,而是通过doctorRepository插入doctor_patient_rel数据:
@Test
public void saveTest() {
Patient patient = Patient.builder().name("p1").build();
patientRepository.save(patient);
Doctor doctor = Doctor.builder().name("bill").build();
DoctorPatientRel relationship = DoctorPatientRel.builder()
.doctor(doctor).patient(patient).createDate(new Date()).build();
Set<DoctorPatientRel> rels = new HashSet<>();
rels.add(relationship);
doctor.setDoctorPatientRels(rels);
doctorRepository.save(doctor);
}
【测试e】在多方试图删除数据,和#1.1一样,并不会删除一方的数据。
【测试f】在一方试图删除数据,可以删除doctor以及doctor_patient_rel的数据,但patient的数据还是在的:
@Test
public void deleteTest() {
doctorRepository.deleteById(1);
}
2. 关联表是组合主键
【数据模型】
用户(user)和角色(role),一个用户可以有多个角色,一个角色可以属于多个用户。

2.1 使用@ManyToOne进行单向关联
联合主键,需要单独一个类,用@Embeddable
标记,并且需要实现序列化,否则会报错Composite-id class must implement Serializable
:
@Embeddable
public class UserRoleId implements Serializable {
@Column(name = "user_id")
private Integer userId;
@Column(name = "role_id")
private Integer roleId;
}
在关联表实体类中,联合主键需要用@EmbeddedId
注解表示。
User
和Role
用多对一@ManyToOne
表示,这里需要注明@MapsId
,指向的是联合主键的@Embeddable类里的相应的列。
@JoinColumn
和上述一样,表明这是一个外键的列。
@Entity
@Table(name = "user_role_rel")
public class UserRoleRel {
@EmbeddedId
private UserRoleId id;
@ManyToOne
@MapsId("userId")
@JoinColumn(name = "user_id")
private User user;
@ManyToOne
@MapsId("roleId")
@JoinColumn(name = "role_id")
private Role role;
@Column(name = "create_date")
private Date createDate;
}
@Entity
@Table(name = "user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String name;
}
@Entity
@Table(name = "role")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String name;
}
【测试a】在多方尝试查询数据,注意,我们的例子中@ManyToOne
没有特别注明fetch类型,那使用的即是默认的EAGER,即用一个sql查询出所有的数据:
@Test
public void querytest() {
UserRoleRel userRoleRel = userRoleRelRepository.findById(new UserRoleId(1, 1)).get();
}
sql:
select
userrolere0_.role_id as role_id1_12_0_,
userrolere0_.user_id as user_id2_12_0_,
userrolere0_.create_date as create_d3_12_0_,
role1_.id as id1_8_1_,
role1_.name as name2_8_1_,
user2_.id as id1_11_2_,
user2_.name as name2_11_2_
from
user_role_rel userrolere0_
inner join
role role1_ on userrolere0_.role_id=role1_.id
inner join
user user2_ on userrolere0_.user_id=user2_.id
where
userrolere0_.role_id=? and userrolere0_.user_id=?
【测试b】由于是单向关联,user方只能查询自己的数据。
【测试c】插入数据,单向的关联关系无法联级插入,另外,需要手动set联合主键的id,即new UserRoleId(user.getId(), role.getId())
,否则会报错:
org.springframework.orm.jpa.JpaSystemException: Could not set field value [2] value by reflection
@Test
public void saveTest() {
User user = User.builder().name("user a").build();
userRepository.save(user);
Role role = Role.builder().name("role a").build();
roleRepository.save(role);
UserRoleRel userRoleRel = UserRoleRel.builder()
.user(user).role(role).createDate(new Date()).build();
userRoleRel.setId(new UserRoleId(user.getId(), role.getId()));
userRoleRelRepository.save(userRoleRel);
}
【测试d】单向关联,user方只能删除自己的数据。
【测试e】尝试在多方删除数据,由于多对一的关系,也只能删除自己的数据,因为它关联的user可能被其它的数据关联,所以一般情况下删除mapping表并不会删除一方数据。
【测试f】单向关联,user方只能删除自己的数据,由于删除了user表的数据,但并没有删除相应的user_role_rel表中的外键,所以在查询UserRoleRel的时候,会查询不到数据:
userRoleRelRepository.findById(new UserRoleId(1, 1)).isPresent()
即使user_role_rel的id=1是有数据的,但因为user_id不存在,可以看【测试a】,在查询的时候用的sql是inner join,因为关联不到user,导致找不到userRoleRel:

2.2 使用@ManyToOne和@OneToMany进行双向关联
双向关联,在多方一切不变,一方只需要加入@OneToMany
即可,因为是被关联,所以用mappedBy
。
public class User {
...
@OneToMany(mappedBy = "user")
private Set<UserRoleRel> userRoleRels = new HashSet<>();
public class Role {
...
@OneToMany(mappedBy = "role")
private Set<UserRoleRel> userRoleRels = new HashSet<>();
测试略,双向关联基本上是可以在一方可以操作多方。
3. 使用@ManyToMany,不创建关联表
【数据模型和#2一致】
3.1 单向关联
不创建UserRoleRel
类,在user
或role
的一方使用@ManyToMany
来关联数据。
@ManyToMany
定义的是两个实体类的多对多关系。我们的user方是主动的一方,因为是单向关联,所以在Role方不需要声明。
@JoinTable
定义在主动的一方(user),表示了关联表的信息(即上述UserRoleRel类),如果没有声明@JoinTable
,那么默认的名字是两张表的名字,中间加下划线,在我们这里,即user_role。
当然这里有个问题,即这种形式的关联,只能在关联表(user_role_rel)只有两列的情况下(即user_id和role_id),因为假如有其它列(如create_date),那么这个字段将难以被维护。
@Entity
@Table(name = "user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String name;
@ManyToMany(cascade = CascadeType.ALL)
@JoinTable(name = "user_role_rel",
joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"),
inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"))
private Set<Role> roles = new HashSet<>();
}
@Entity
@Table(name = "role")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String name;
}
【测试a】在主动方(user)查询数据,可以查询到两张表的数据:
@ManyToMany
没有声明fetch类型,默认为LAZY,所以分两次查询:
@Transactional
@Test
public void test() {
User user = userRepository.findById(1).get();
user.getRoles()
}
select
user0_.id as id1_11_0_,
user0_.name as name2_11_0_
from
user user0_
where
user0_.id=?
select
roles0_.user_id as user_id1_12_0_,
roles0_.role_id as role_id2_12_0_,
role1_.id as id1_8_1_,
role1_.name as name2_8_1_
from
user_role_rel roles0_
inner join
role role1_
on roles0_.role_id=role1_.id
where
roles0_.user_id=?
【测试b】由于是单向关联,role只能查询自己表的数据。
【测试c】在主动方(user)新增数据,可以插入关联方数据:
@Test
public void saveTest() {
Set<Role> roles = new HashSet<>();
roles.add(Role.builder().name("role a").build());
User user = User.builder().name("user a").roles(roles).build();
userRepository.save(user);
}
如同上述写的,这里关联表只会维护user_id, role_id,其它字段无法维护:

【测试d】由于是单向关联,role只能新增自己的数据。
【测试e】在user中删除数据,可以同时删除3张表的数据:
@Test
public void deleteTest() {
userRepository.deleteById(1);
}
【测试f】在role中删除数据,只能删除自己的数据。这样会在user_role_rel中还留着其实已经被删除的role_id。
由于user中getRoles()用的是关联表+inner join+role表,所以就算有脏数据也并不会报错,只会查不到相应的role了。
3.2 双向关联
双向关联只需要在被关联方Role中加上@ManyToMany
,并且需要声明mappedBy,即被关联的一方。
public class Role {
...
@ManyToMany(mappedBy = "roles")
private Set<User> users = new HashSet<>();
}
【测试a】在user方查询,和上述#3.1一样。
【测试b】在role方查询,也可以查询user的数据,同时由于@ManyToMany的默认fetch类型为LAZY,则也会分两个sql查询:
select
role0_.id as id1_8_0_,
role0_.name as name2_8_0_
from
role role0_
where
role0_.id=?
select
users0_.role_id as role_id2_12_0_,
users0_.user_id as user_id1_12_0_,
user1_.id as id1_11_1_,
user1_.name as name2_11_1_
from
user_role_rel users0_
inner join
user user1_ on users0_.user_id=user1_.id
where
users0_.role_id=?
【测试c】user端可以同时插入三张表的数据。同#3.1
【测试d】role端只能插入自己的数据。
【测试e】user端可以同时删除三张表的数据。
【测试f】role端只能删除自己的数据。
总结双向关联的优点是在被动方(role)中可以查询主动方(user)的数据,别的和单向的一样。
网友评论