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

一、常用注解详解

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

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

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

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

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

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

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

  属性1: mappedBy="permissions" 表示,当前类不维护状态,属性值其实是本类在被标注的链接属性上的链接属性,此案例的本类时Permission,连接属性是roles,连接属性的类的连接属性是permissions

属性2: fetch = FetchType.LAZY 表示是不是懒加载,默认是,可以设置成FetchType.EAGER

属性3:cascade=CascadeType.ALL 表示当前类操作时,被标注的连接属性如何级联,比如班级和学生是1对多关系,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());
}
}
@OneToMany(targetEntity=UsersDetail.class,fetch=FetchType.LAZY,mappedBy="source")
关联的实体的主键一般是用来做外键的。但如果此时不想主键作为外键,则需要设置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、测试

   @RequestMapping("/save")
public JSONResult save(){
//用户
Users user = new Users();
user.setName("springbootjpa");
user.setAccount("admin");
user.setPwd("123456");
//详情
UsersDetail usersDetail = new UsersDetail();
usersDetail.setAge(19);
usersDetail.setAddress("beijing,haidian,");
//保存用户和详情
user.setUserDetail(usersDetail);
userRespository.save(user);
return JSONResult.ok("保存成功");
}

二、一对多和对多对一

一对多和多对一的关系映射,最常见的场景就是:人员角色关系。实体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>();
}

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

3、测试

    @RequestMapping("/updateRole/{id}")
public JSONResult updateRole(@PathVariable Long id) {
Users user = userRespository.findById(id).orElse(null);
Long roleId = Long.valueOf(25);
Roles role = roleRespository.findById(roleId).orElse(null);
if (user!=null){
user.setRole(role);
}
userRespository.save(user);
return JSONResult.ok("修改成功");
}

主要特别注意的是更新和删除的级联操作:

其中 @OneToMany 和 @ManyToOne 用得最多,这里再补充一下 关于级联,一定要注意,要在关系的维护端,即 One 端。
比如 人员和角色,角色是One,人员是Many;cascade = CascadeType.ALL 只能写在 One 端,只有One端改变Many端,不准Many端改变One端。 特别是删除,因为 ALL 里包括更新,删除。
如果删除一条评论,就把文章删了,那算谁的。所以,在使用的时候要小心。一定要在 One 端使用。

三、多对多

多对多的映射关系最常见的场景就是:权限和角色关系。角色和权限是多对多的关系。一个角色可以有多个权限,一个权限也可以被很多角色拥有。 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:这个属性就是维护关系的意思!谁主类有此属性谁不维护关系。
* 比如两个多对多的关系是由role中的permissions维护的,那么,只有操作role实体对象时,指定permissions,才可建立外键的关系。
* 只有OneToOne,OneToMany,ManyToMany上才有mappedBy属性,ManyToOne不存在该属性; 并且mappedBy一直和joinXX互斥。

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

3、测试

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

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

说明:test1我们可以看到,由于role方是维护关系的,所以建立Roles.set(Permissions)就能把关系表建立,但是注意一点,由于我没有设置级联=all,而Permissions是个临时对象,而临时对象保存时会持久化,如果不是我级联保存的话,那么会报错,解决办法如测试范例,先通过save(pm),再操作。

test2我们可以观察到,当执行完后,中间表的删除是由维护关系的role删除了(自己都删除了,关系肯定也需要维护的),但是,permission表还存在数据。

test3我们可以观察到,我把role.setPermission(null),就可以解除关系,中间表的对应的记录也没有了。


四、最后

维护关系是由mapped属性决定,标注在那,那个就不维护关系。级联操作是作用于当前类的操作发生时,对关系类进行级联操作。

和hibernate使用没多大区别啊!



Spring Boot 入门系列(二十八) JPA 的实体映射关系,一对一,一对多,多对多关系映射!的更多相关文章

  1. Spring Boot入门系列(十八)整合mybatis,使用注解的方式实现增删改查

    之前介绍了Spring Boot 整合mybatis 使用xml配置的方式实现增删改查,还介绍了自定义mapper 实现复杂多表关联查询.虽然目前 mybatis 使用xml 配置的方式 已经极大减轻 ...

  2. Spring Boot入门系列(十六)使用pagehelper实现分页功能

    之前讲了Springboot整合Mybatis,然后介绍了如何自动生成pojo实体类.mapper类和对应的mapper.xml 文件,并实现最基本的增删改查功能.接下来要说一说Mybatis 的分页 ...

  3. Spring Boot入门系列(十四)使用JdbcTemplate操作数据库,配置多数据源!

    前面介绍了Spring Boot 中的整合Mybatis并实现增删改查.如何实现事物控制.不清楚的朋友可以看看之前的文章:https://www.cnblogs.com/zhangweizhong/c ...

  4. Spring Boot入门系列(十)如何使用拦截器,一学就会!

    前面介绍了Spring Boot 如何整合定时任务已经Spring Boot 如何创建异步任务,不清楚的朋友可以看看之前的文章:https://www.cnblogs.com/zhangweizhon ...

  5. Spring Boot入门系列(十五)Spring Boot 开发环境热部署

    在实际的项目开发过中,当我们修改了某个java类文件时,需要手动重新编译.然后重新启动程序的,整个过程比较麻烦,特别是项目启动慢的时候,更是影响开发效率.其实Spring Boot的项目碰到这种情况, ...

  6. Spring Boot入门系列(十九)整合mybatis,使用注解实现动态Sql、参数传递等常用操作!

    前面介绍了Spring Boot 整合mybatis 使用注解的方式实现数据库操作,介绍了如何自动生成注解版的mapper 和pojo类. 接下来介绍使用mybatis 常用注解以及如何传参数等数据库 ...

  7. Spring Boot教程(二十八)通过JdbcTemplate编写数据访问

    数据源配置 在我们访问数据库的时候,需要先配置一个数据源,下面分别介绍一下几种不同的数据库配置方式. 首先,为了连接数据库需要引入jdbc支持,在pom.xml中引入如下配置: <depende ...

  8. 学习Spring Boot:(二十八)Spring Security 权限认证

    前言 主要实现 Spring Security 的安全认证,结合 RESTful API 的风格,使用无状态的环境. 主要实现是通过请求的 URL ,通过过滤器来做不同的授权策略操作,为该请求提供某个 ...

  9. Spring Boot入门系列(二十)快速打造Restful API 接口

    spring boot入门系列文章已经写到第二十篇,前面我们讲了spring boot的基础入门的内容,也介绍了spring boot 整合mybatis,整合redis.整合Thymeleaf 模板 ...

  10. Spring Boot入门系列(十七)整合Mybatis,创建自定义mapper 实现多表关联查询!

    之前讲了Springboot整合Mybatis,介绍了如何自动生成pojo实体类.mapper类和对应的mapper.xml 文件,并实现最基本的增删改查功能.mybatis 插件自动生成的mappe ...

随机推荐

  1. 腾讯技术团队整理,为什么 Flutter 能最好地改变移动开发

    导语 | Flutter 框架是当下非常热门的跨端解决方案,能够帮助开发者通过一套代码库高效构建多平台精美应用,支持移动.Web.桌面等多端开发.但仍然有很多产品.设计.甚至开发同学并不了解 Flut ...

  2. IP地址,InetAddress类的使用

    IP地址 IP地址:InetAddress(没有构造器,通过静态方法返回) java.net包下 唯一定位一台网络上的计算机 127.0.0.1:本机localhost ip地址的分类 IPV4/IP ...

  3. Use Emacs as Personal Knowledge Base

    http://stackoverflow.com/questions/2014636/how-to-maintain-an-emacs-based-knowledge-base

  4. 通过Mysql提权的几种姿势

    本文记录利用mysql数据库,在拿到shell之后进行提权的两种方法. 一.UDF提权 原理:UDF是mysql的一个拓展接口,UDF(Userdefined function)让用户通过该接口可以自 ...

  5. noip13

    T1 一开始直接丢了个暴力走人50pts,然后开始打表找规律,啥也没找着,最后二十分钟突然看出来了什么,把 \(f_{n,m}\)式子列了一下,发现常数项没啥规律,最后五分钟,突然闪过一丝灵感,但是是 ...

  6. 题解—P2898 [USACO08JAN]Haybale Guessing G

    pre 首先注意一下翻译里面并没有提到的一点,也是让我没看懂样例的一点,就是这个长度为 \(n\) 的数组里面的数各不相同. 有很多人用并查集写的这道题,题解里面也有一些用线段树写的,不过我认为我的做 ...

  7. Docker创建Nexus

    docker-compose.yml 注意为/usr/local/docker/nexus/data授权读写权限! version: '3.1' services: nexus: restart: a ...

  8. OAuth2 与OpenID的区别

    OAuth2 OpenId OpenId是在OAuth2基础之上实现的 比OAuth2更简便 OAuth2需要在认证后 额外的去再调用用户信息的接口 才能获取用户信息 而OpenId直接伴随token ...

  9. malloc 和new , free 和delete的区别

    #include <iostream>using namespace std;class user{ public: int age; int number; void test() { ...

  10. 【C#】GC和析构函数(Finalize 方法)

    析构函数: (来自百度百科)析构函数(destructor) 与构造函数相反,当对象脱离其作用域时(例如对象所在的函数已调用完毕),系统自动执行析构函数.析构函数往往用来做"清理善后&quo ...