Spring Boot 入门系列(二十八) JPA 的实体映射关系,一对一,一对多,多对多关系映射!
前面讲了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 的实体映射关系,一对一,一对多,多对多关系映射!的更多相关文章
- Spring Boot入门系列(十八)整合mybatis,使用注解的方式实现增删改查
之前介绍了Spring Boot 整合mybatis 使用xml配置的方式实现增删改查,还介绍了自定义mapper 实现复杂多表关联查询.虽然目前 mybatis 使用xml 配置的方式 已经极大减轻 ...
- Spring Boot入门系列(十六)使用pagehelper实现分页功能
之前讲了Springboot整合Mybatis,然后介绍了如何自动生成pojo实体类.mapper类和对应的mapper.xml 文件,并实现最基本的增删改查功能.接下来要说一说Mybatis 的分页 ...
- Spring Boot入门系列(十四)使用JdbcTemplate操作数据库,配置多数据源!
前面介绍了Spring Boot 中的整合Mybatis并实现增删改查.如何实现事物控制.不清楚的朋友可以看看之前的文章:https://www.cnblogs.com/zhangweizhong/c ...
- Spring Boot入门系列(十)如何使用拦截器,一学就会!
前面介绍了Spring Boot 如何整合定时任务已经Spring Boot 如何创建异步任务,不清楚的朋友可以看看之前的文章:https://www.cnblogs.com/zhangweizhon ...
- Spring Boot入门系列(十五)Spring Boot 开发环境热部署
在实际的项目开发过中,当我们修改了某个java类文件时,需要手动重新编译.然后重新启动程序的,整个过程比较麻烦,特别是项目启动慢的时候,更是影响开发效率.其实Spring Boot的项目碰到这种情况, ...
- Spring Boot入门系列(十九)整合mybatis,使用注解实现动态Sql、参数传递等常用操作!
前面介绍了Spring Boot 整合mybatis 使用注解的方式实现数据库操作,介绍了如何自动生成注解版的mapper 和pojo类. 接下来介绍使用mybatis 常用注解以及如何传参数等数据库 ...
- Spring Boot教程(二十八)通过JdbcTemplate编写数据访问
数据源配置 在我们访问数据库的时候,需要先配置一个数据源,下面分别介绍一下几种不同的数据库配置方式. 首先,为了连接数据库需要引入jdbc支持,在pom.xml中引入如下配置: <depende ...
- 学习Spring Boot:(二十八)Spring Security 权限认证
前言 主要实现 Spring Security 的安全认证,结合 RESTful API 的风格,使用无状态的环境. 主要实现是通过请求的 URL ,通过过滤器来做不同的授权策略操作,为该请求提供某个 ...
- Spring Boot入门系列(二十)快速打造Restful API 接口
spring boot入门系列文章已经写到第二十篇,前面我们讲了spring boot的基础入门的内容,也介绍了spring boot 整合mybatis,整合redis.整合Thymeleaf 模板 ...
- Spring Boot入门系列(十七)整合Mybatis,创建自定义mapper 实现多表关联查询!
之前讲了Springboot整合Mybatis,介绍了如何自动生成pojo实体类.mapper类和对应的mapper.xml 文件,并实现最基本的增删改查功能.mybatis 插件自动生成的mappe ...
随机推荐
- 腾讯技术团队整理,为什么 Flutter 能最好地改变移动开发
导语 | Flutter 框架是当下非常热门的跨端解决方案,能够帮助开发者通过一套代码库高效构建多平台精美应用,支持移动.Web.桌面等多端开发.但仍然有很多产品.设计.甚至开发同学并不了解 Flut ...
- IP地址,InetAddress类的使用
IP地址 IP地址:InetAddress(没有构造器,通过静态方法返回) java.net包下 唯一定位一台网络上的计算机 127.0.0.1:本机localhost ip地址的分类 IPV4/IP ...
- Use Emacs as Personal Knowledge Base
http://stackoverflow.com/questions/2014636/how-to-maintain-an-emacs-based-knowledge-base
- 通过Mysql提权的几种姿势
本文记录利用mysql数据库,在拿到shell之后进行提权的两种方法. 一.UDF提权 原理:UDF是mysql的一个拓展接口,UDF(Userdefined function)让用户通过该接口可以自 ...
- noip13
T1 一开始直接丢了个暴力走人50pts,然后开始打表找规律,啥也没找着,最后二十分钟突然看出来了什么,把 \(f_{n,m}\)式子列了一下,发现常数项没啥规律,最后五分钟,突然闪过一丝灵感,但是是 ...
- 题解—P2898 [USACO08JAN]Haybale Guessing G
pre 首先注意一下翻译里面并没有提到的一点,也是让我没看懂样例的一点,就是这个长度为 \(n\) 的数组里面的数各不相同. 有很多人用并查集写的这道题,题解里面也有一些用线段树写的,不过我认为我的做 ...
- Docker创建Nexus
docker-compose.yml 注意为/usr/local/docker/nexus/data授权读写权限! version: '3.1' services: nexus: restart: a ...
- OAuth2 与OpenID的区别
OAuth2 OpenId OpenId是在OAuth2基础之上实现的 比OAuth2更简便 OAuth2需要在认证后 额外的去再调用用户信息的接口 才能获取用户信息 而OpenId直接伴随token ...
- malloc 和new , free 和delete的区别
#include <iostream>using namespace std;class user{ public: int age; int number; void test() { ...
- 【C#】GC和析构函数(Finalize 方法)
析构函数: (来自百度百科)析构函数(destructor) 与构造函数相反,当对象脱离其作用域时(例如对象所在的函数已调用完毕),系统自动执行析构函数.析构函数往往用来做"清理善后&quo ...