SpringBoot从入门到精通(二十八)JPA 的实体映射关系,一对一,一对多,多对多关系映射!

简介: 前面讲了Spring Boot 使用 JPA,实现JPA 的增、删、改、查的功能,同时也介绍了JPA的一些查询,自定义SQL查询等使用。JPA使用非常简单,功能非常强大的ORM框架,无需任何数据访问层和sql语句即可实现完整的数据操作方法。但是,之前都是介绍的单表的增删改查等操作,多表多实体的数据操作怎么实现呢?接下来聊一聊 JPA 的一对一,一对多,多对一,多对多等实体映射关系。

前面讲了Spring Boot 使用 JPA,实现JPA 的增、删、改、查的功能,同时也介绍了JPA的一些查询,自定义SQL查询等使用。JPA使用非常简单,功能非常强大的ORM框架,无需任何数据访问层和sql语句即可实现完整的数据操作方法。

但是,之前都是介绍的单表的增删改查等操作,多表多实体的数据操作怎么实现呢?接下来聊一聊 JPA 的一对一,一对多,多对一,多对多等实体映射关系。

 

一、常用注解详解

1、实体定义注解

(1) @JoinColumn指定该实体类对应的表中引用的表的外键,name属性指定外键名称,referencedColumnName指定应用表中的字段名称

(2) @JoinColumn(name=”role_id”): 标注在连接的属性上(一般多对一中的‘’方),指定了本类的外键名叫什么。

(3) @JoinTable(name="permission_role") :标注在连接的属性上(一般多对多),指定了多对多的中间表叫什么。

备注:Join的标注,和下面几个标注的mappedBy属性互斥!


2、关系映射注解

(1) @OneToOne 配置一对一关联,属性targetEntity指定关联的对象的类型 。

(2) @OneToMany注解“一对多”关系中‘一’方的实体类属性(该属性是一个集合对象),targetEntity注解关联的实体类类型,mappedBy注解另一方实体类中本实体类的属性名称

(3)@ManyToOne注解“一对多”关系中‘多’方的实体类属性(该属性是单个对象),targetEntity注解关联的实体类类型

  • 属性1: mappedBy="permissions" 表示,当前类不维护状态,属性值其实是本类在被标注的链接属性上的链接属性,此案例的本类时Permission,连接属性是roles,连接属性的类的连接属性是permissions
  • 属性2: fetch = FetchType.LAZY 表示是不是懒加载,默认是,可以设置成FetchType.EAGER
  • 属性3:cascade=CascadeType.ALL 表示当前类操作时,被标注的连接属性如何级联,比如班级和学生是一对多关系,cascade标注在班级类中,那么执行班级的save操作的时候(班级.学生s.add(学生)),能级联保存学生,否则报错,需要先save学生,变成持久化对象,在班级.学生s.add(学生)

注意:只有OneToOne,OneToMany,ManyToMany上才有mappedBy属性,ManyToOne不存在该属性;

 

二、一对一

首先,一对一的实体关系最常用的场景就是主表与从表,即主表存关键经常使用的字段,从表保存非关键字段,类似 User与UserDetail 的关系。主表和详细表通过外键一一映射。

一对一的映射关系通过@OneToOne 注解实现。通过 @JoinColumn 配置一对一关系。

其实,一对一有好几种,这里举例的是常用的一对一双向外键关联(改造成单向很简单,在对应的实体类去掉要关联其它实体的属性即可),并且配置了级联删除和添加,相关类如下:

1、User 实体类定义:

package com.weiz.pojo;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
@Getter
@Setter
@Entity
@Table(name = "Users")
public class Users {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
    private String account;
    private String pwd;
    @OneToOne(cascade = {CascadeType.PERSIST,CascadeType.REMOVE})
    @JoinColumn(name="detailId",referencedColumnName = "id")
    private UsersDetail userDetail;
    @Override
    public String toString() {
        return String.format("Book [id=%s, name=%s, user detail=%s]", id, userDetail.getId());
    }
}

上面的示例中,@OneToOne注解关联实体映射,关联的实体的主键一般是用来做外键的。但如果此时不想主键作为外键,则需要设置referencedColumnName属性。当然这里关联实体(Address)的主键 id 是用来做主键,所以这里第20行的 referencedColumnName = "id" 实际可以省略。

 

2、从表 UserDetail 实体类定义

package com.weiz.pojo;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
@Getter
@Setter
@Entity
@Table(name = "UsersDetail")
public class UsersDetail {
    @Id
    @GeneratedValue
    private Long id;
    @Column(name = "address")
    private String address;
    @Column(name = "age")
    private Integer age;
    @Override
    public String toString() {
        return String.format("UsersDetail [id=%s, address=%s, age=%s]", id,address,age);
    }
}

代码说明:子类无需任何定义,关系均在主类中维护。

 

3、验证测试

创建单元测试方法,验证一对一关系的保存和查询功能。

@Test
public void testOneToOne(){
    // 用户
    User user = new User();
    user.setName("one2one");
    user.setPassword("123456");
    user.setAge(20);
    // 详情
    UserDetail userDetail = new UserDetail();
    userDetail.setAddress("beijing,haidian,");
    // 保存用户和详情
    user.setUserDetail(userDetail);
    userRepository.save(user);
    User result = userRepository.findById(7L).get();
    System.out.println("name:"+result.getName()+",age:"+result.getAge()+", address:"+result.getUserDetail().getAddress());
}

单击Run Test或在方法上右击,选择Run 'testOneToOne',运行单元测试方法,结果如下图所示。结果表明创建的单元测试运行成功,用户信息(User)和用户详细信息(UserDetail)保存成功,实现了一对一实体的级联保存和关联查询。

image.png


二、一对多和对多对一

一对多和多对一的关系映射,最常见的场景就是:人员角色关系。实体Users:人员。 实体 Roles:角色。 人员 和角色是一对多关系(双向)。那么在JPA中,如何表示一对多的双向关联呢?

JPA使用@OneToMany和@ManyToOne来标识一对多的双向关联。一端(Roles)使用@OneToMany,多端(Users)使用@ManyToOne。在JPA规范中,一对多的双向关系由多端(Users)来维护。也就是说多端(Users)为关系维护端,负责关系的增删改查。

一端(Roles)则为关系被维护端,不能维护关系。 一端(Roles)使用@OneToMany注释的mappedBy="role"属性表明Author是关系被维护端。

多端(Users)使用@ManyToOne和@JoinColumn来注释属性 role,@ManyToOne表明Article是多端,@JoinColumn设置在Users表中的关联字段(外键)。

1、原先的User 实体类修改如下:

package com.weiz.pojo;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
@Getter
@Setter
@Entity
@Table(name = "Users")
public class Users {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
    private String account;
    private String pwd;
    @OneToOne(cascade = {CascadeType.PERSIST,CascadeType.REMOVE})
    @JoinColumn(name="detailId",referencedColumnName = "id")
    private UsersDetail userDetail;
    /**一对多,多的一方必须维护关系,即不能指定mapped=""**/
    @ManyToOne(fetch = FetchType.LAZY,cascade=CascadeType.MERGE)
    @JoinColumn(name="role_id")
    private Roles role;
    @Override
    public String toString() {
        return String.format("Book [id=%s, name=%s, user detail=%s]", id, userDetail.getId());
    }
}


2、角色实体类

package com.weiz.pojo;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
import java.util.HashSet;
import java.util.Set;
@Getter
@Setter
@Entity
@Table(name = "Roles")
public class Roles {
    @Id
    @GeneratedValue()
    private Long id;
    private String name;
    @OneToMany(mappedBy="role",fetch=FetchType.LAZY,cascade=CascadeType.ALL)
    private Set<Users> users = new HashSet<Users>();
}

其中 @OneToMany 和 @ManyToOne 用得最多,这里再补充一下 关于级联,一定要注意,要在关系的维护端,即 One 端。

比如 人员和角色,角色是One,人员是Many;cascade = CascadeType.ALL 只能写在 One 端,只有One端改变Many端,不准Many端改变One端。 特别是删除,因为 ALL 里包括更新,删除。

如果删除一条评论,就把文章删了,那算谁的。所以,在使用的时候要小心。一定要在 One 端使用。


最终生成的表结构 Users 表中会增加role_id 字段。

 

3、验证测试

@Test
public void testOneToMany() {
    // 保存角色
    Role role = new Role();
    role.setId(3L);
    role.setName("管理员");
    roleRepository.save(role);
    // 修改人员角色
    User user = userRepository.findById(7L).orElse(null);
    Role admin = roleRepository.findById(3L).orElse(null);
    if (user!=null){
        user.setRole(admin);
    }
    userRepository.save(user);
    User result = userRepository.findById(7L).get();
    System.out.println("name:"+result.getName()+",age:"+result.getAge()+", role:"+result.getRole().getName());
} 

特别注意的是更新和删除的级联操作。


单击Run Test或在方法上右击,选择Run 'testOneToMany',运行单元测试方法,结果下图所示。

image.png

 

三、多对多

多对多的映射关系最常见的场景就是:权限和角色关系。角色和权限是多对多的关系。一个角色可以有多个权限,一个权限也可以被很多角色拥有。

JPA中使用@ManyToMany来注解多对多的关系,由一个关联表来维护。这个关联表的表名默认是:主表名+下划线+从表名。(主表是指关系维护端对应的表,从表指关系被维护端对应的表)。这个关联表只有两个外键字段,分别指向主表ID和从表ID。字段的名称默认为:主表名+下划线+主表中的主键列名,从表名+下划线+从表中的主键列名。


需要注意的:

1、多对多关系中一般不设置级联保存、级联删除、级联更新等操作。

2、可以随意指定一方为关系维护端,在这个例子中,我指定 User 为关系维护端,所以生成的关联表名称为: role_permission,关联表的字段为:role_id 和 permission_id。

3、多对多关系的绑定由关系维护端来完成,即由 role1.setPermissions(ps);来绑定多对多的关系。关系被维护端不能绑定关系,即permission不能绑定关系。

4、多对多关系的解除由关系维护端来完成,即由 role1.getPermissions().remove(permission);来解除多对多的关系。关系被维护端不能解除关系,即permission不能解除关系。

5、如果Role和Permission已经绑定了多对多的关系,那么不能直接删除Permission,需要由Role解除关系后,才能删除Permission。但是可以直接删除Role,因为Role是关系维护端,删除Role时,会先解除Role和Permission的关系,再删除Role。


下面,看看角色Roles 和 权限 Permissions 的多对多的映射关系实现,具体代码如下:

1、角色Roles 实体类定义:

package com.weiz.pojo;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
import java.util.HashSet;
import java.util.Set;
@Getter
@Setter
@Entity
@Table(name = "Roles")
public class Roles {
    @Id
    @GeneratedValue()
    private Long id;
    private String name;
    @ManyToMany(cascade = CascadeType.MERGE,fetch = FetchType.LAZY)
    @JoinTable(name="permission_role")
    private Set<Permissions> permissions = new HashSet<Permissions>();
    @OneToMany(mappedBy="role",fetch=FetchType.LAZY,cascade=CascadeType.ALL)
    private Set<Users> users = new HashSet<Users>();
}

代码说明:

cascade表示级联操作,all是全部,一般用MERGE 更新,persist表示持久化即新增

此类是维护关系的类,删除它,可以删除对应的外键,但是如果需要删除对应的权限就需要CascadeType.all

cascade:作用在本放,对于删除或其他操作本方时,对标注连接方的影响!和数据库一样!!

 

2、权限Permissions 实体类定义:

package com.weiz.pojo;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
import java.util.Set;
/**
 * 权限表
*/
@Getter
@Setter
@Entity
@Table(name="Permissions")
public class Permissions {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
    private String type;
    private String url;
    @Column(name="perm_code")
    private String permCode;
    @ManyToMany(mappedBy="permissions",fetch = FetchType.LAZY)
    private Set<Roles> roles;
}

注意:不能两边用mappedBy:这个属性就是维护关系的意思!谁主类有此属性谁不维护关系。

  1. 比如两个多对多的关系是由role中的permissions维护的,那么,只有操作role实体对象时,指定permissions,才可建立外键的关系。
  2. 只有OneToOne,OneToMany,ManyToMany上才有mappedBy属性,ManyToOne不存在该属性; 并且mappedBy一直和joinXX互斥。

注解中属性的汉语解释:权限不维护关系,关系表是permission_role,全部懒加载,角色的级联是更新 (多对多关系不适合用all,不然删除一个角色,那么所有此角色对应的权限都被删了,级联删除一般用于部分一对多时业务需求上是可删的,比如品牌类型就不适合删除一个类型就级联删除所有的品牌,一般是把此品牌的类型设置为null(解除关系),然后执行删除,就不会报错了!)


3、验证测试

@Test
public void testManyToMany(){
    // 角色
    Roles role1 = new Roles();
    role1.setName("admin role");
    // 角色赋权限
    Set<Permissions> ps = new HashSet<Permissions>();
    for (int i = 0; i < 3; i++) {
        Permission pm = new Permission();
        pm.setName("permission"+i);
        permissionRespository.save(pm);  /**由于Role类没有设置级联持久化,因此这里需要先持久化pm,否则报错!*/
        ps.add(pm);
    }
    role1.setPermissions(ps);
    // 保存
    roleRespository.save(role1);
}

配置说明:由于多对一不能用mapped,那么它必然必须维护关系,维护关系是多的一方由User维护的,User的级联是更新,Role的级联是All,User的外键是role_id指向Role。


最后

维护关系是由mapped属性决定,标注在那,那个就不维护关系。级联操作是作用于当前类的操作发生时,对关系类进行级联操作。和hibernate使用没多大区别啊!



推荐阅读:

SpringBoot从入门到精通(二十七)JPA实现自定义查询,完全不需要写SQL!

SpringBoot从入门到精通(二十六)超级简单的数据持久化框架!Spring Data JPA 的使用!

SpringBoot从入门到精通(二十五)搞懂自定义系统配置

SpringBoot从入门到精通(二十四)3分钟搞定Spring Boot 多环境配置!

SpringBoot从入门到精通(二十三)Mybatis系列之——实现Mybatis多数据源配置

SpringBoot从入门到精通(二十二)使用Swagger2优雅构建 RESTful API文档

相关文章
|
10天前
|
Java 应用服务中间件 数据库连接
SpringBoot入门(2) - SpringBoot HelloWorld
SpringBoot入门(2) - SpringBoot HelloWorld
26 2
SpringBoot入门(2) - SpringBoot HelloWorld
|
21天前
|
Java 数据库连接 测试技术
SpringBoot入门(4) - 添加内存数据库H2
SpringBoot入门(4) - 添加内存数据库H2
41 4
SpringBoot入门(4) - 添加内存数据库H2
|
10天前
|
Java 中间件
SpringBoot入门(6)- 添加Logback日志
SpringBoot入门(6)- 添加Logback日志
47 5
|
10天前
|
前端开发 Java 数据库
SpringBoot入门(3) - 对Hello world进行MVC分层
SpringBoot入门(3) - 对Hello world进行MVC分层
26 4
|
10天前
|
缓存 IDE Java
SpringBoot入门(7)- 配置热部署devtools工具
SpringBoot入门(7)- 配置热部署devtools工具
22 2
 SpringBoot入门(7)- 配置热部署devtools工具
|
21天前
|
Java 应用服务中间件 数据库连接
SpringBoot入门(2) - SpringBoot HelloWorld
SpringBoot入门(2) - SpringBoot HelloWorld
17 2
SpringBoot入门(2) - SpringBoot HelloWorld
|
23天前
|
Java 数据库连接 测试技术
SpringBoot入门(4) - 添加内存数据库H2
SpringBoot入门(4) - 添加内存数据库H2
29 2
SpringBoot入门(4) - 添加内存数据库H2
|
23天前
|
前端开发 Java 数据库
SpringBoot入门(3) - 对Hello world进行MVC分层
SpringBoot入门(3) - 对Hello world进行MVC分层
33 1
SpringBoot入门(3) - 对Hello world进行MVC分层
|
23天前
|
Java 应用服务中间件 数据库连接
SpringBoot入门(2) - SpringBoot HelloWorld
SpringBoot入门(2) - SpringBoot HelloWorld
19 1
 SpringBoot入门(2) - SpringBoot HelloWorld
|
15天前
|
Java 数据库连接 测试技术
SpringBoot入门(4) - 添加内存数据库H2
SpringBoot入门(4) - 添加内存数据库H2
55 13