美文网首页
【Spring JPA总结】JPA Many-To-Many介绍

【Spring JPA总结】JPA Many-To-Many介绍

作者: 伊丽莎白2015 | 来源:发表于2023-01-07 22:20 被阅读0次

参考:
https://hellokoding.com/jpa-many-to-many-relationship-mapping-example-with-spring-boot-maven-and-mysql/


JPA中多对多的关系,可以使用注解@ManyToMany, @OneToMany, 和 @ManyToOne

主要是分三大类,即:

  1. 关联表有自己的主键(即单个主键)
  2. 关联表是组合主键
  3. 不创建关联表

【具体来讲】

  1. 关联表有自己的主键(即单个主键): 关联表有自己的主键(即单个主键)
  2. 关联表是组合主键: 关联表是组合主键
  3. 不创建关联表: 不创建关联表

1. 关联表有自己的主键(即单个主键)

【数据模型】
医生(doctor)和病人(patient),一个病人可以有多个医生,医生可以为不同的病人看病。

单个主键的意思是关联表doctor_patient_rel有自己单独的主键id,doctor_idpatient_id仅仅作为外键:

image.png

1. @ManyToOne:单向关联

doctor_idpatient_id是这张表的外键。分别关联到DoctorPatient实体,用@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),一个用户可以有多个角色,一个角色可以属于多个用户。

组合主键的意思是关联表user_role_rel不再有自己的自增主键id,而是使用user_id和role_id组成联合主键。 image.png

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注解表示。

UserRole用多对一@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: image.png

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类,在userrole的一方使用@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,其它字段无法维护: image.png

【测试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)的数据,别的和单向的一样。

相关文章

网友评论

      本文标题:【Spring JPA总结】JPA Many-To-Many介绍

      本文链接:https://www.haomeiwen.com/subject/agedcdtx.html