SpringDataJPA
JPA 第三弹
回顾:
springDataJPA的运行过程和原理:
1. 通过 `org.springframework.aop.framework.JdkDynamicAopProxy`的`invoke`方法创建了一个动态代理对象
2. `org.springframework.data.jpa.repository.support.SimpleJpaRepository`中封装了 JPA 的操作(借助 JPA 的 API 完成数据库的 CRUD 操作)
3. 由于 JPA 是一套规范,不会单独的操作数据库,所以需要提供 JPA 规范的实现。所以需要通过 Hibernate 来实现数据库的操作
springDataJpa、jpa规范、Hibernate三者之间的关系:
我们的代码要符合 springDatajpa 规范,springDataJpa 是对 Jpa 规范的扩展,最底层实现(操作数据库的)是Hibernate
符合springDataJpa规范的dao层接口的编写规则:
1. 需要实现两个接口(`org.springframework.data.jpa.repository.JpaRepository`,`org.springframework.data.jpa.repository.JapSpecificationExecutor`)
2. 提供响应的泛型
运行过程:
动态代理的方式:动态代理对象
普通 CRUD 操作
1 Specifications 动态查询
有时我们在查询某个实体的时候,给定的条件是不固定的,这时就需要动态构建相应的查询语句,在 Spring Data JPA 中可以通过 org.springframework.data.jpa.repository.JpaSpecificationExecutor
接口查询。相比JPQL,其优势是类型安全,更加的面向对象。
package org.springframework.data.jpa.repository;
import java.util.List;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
// JpaSpecificationExecutor中定义的方法
public interface JpaSpecificationExecutor<T> {
// 根据条件查询一个对象
T findOne(Specification<T> spec);
// 根据条件查询集合
List<T> findAll(Specification<T> spec);
// 根据条件分页查询
Page<T> findAll(Specification<T> spec, Pageable pageable);
// 排序查询查询
List<T> findAll(Specification<T> spec, Sort sort);
// 统计查询
long count(Specification<T> spec);
}
对于JpaSpecificationExecutor,这个接口基本是围绕着 org.springframework.data.jpa.domain.Specification
接口来定义的。我们可以简单的理解为,Specification 构造的就是查询条件。
Specification 接口中只定义了如下一个方法:
public interface Specification<T> {
/**
* 构造查询条件
*
* @param root Root接口,代表查询的根对象,可以通过root获取实体中的属性
* @param query 代表一个顶层查询对象,用来自定义查询
* @param cb 用来构建查询,此对象里有很多条件方法
*/
Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb);
}
1.1 使用Specifications完成条件查询
package com.itlaobing.test;
import com.itlaobing.dao.CustomerDao;
import com.itlaobing.entity.Customer;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring-application.xml")
public class SpecificationTest {
@Autowired
private CustomerDao customerDao;
/**
* 精确匹配, 匹配 custName 为 云创动力 的客户信息
*/
@Test
public void testSpecification() {
//使用匿名内部类的方式,创建一个Specification的实现类,并实现toPredicate方法
Specification<Customer> spec = new Specification<Customer>() {
//cb:构建查询,添加查询方式 equal:精准匹配
//root:从实体Customer对象中按照custName属性进行查询
@Override
public Predicate toPredicate(Root<Customer> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
// 1. 获取比较属性
Path<Object> custName = root.get("custName");
// 2. 构建查询条件
Predicate predicate = cb.equal(custName, "云创动力");
return predicate;
}
};
Customer customer = customerDao.findOne(spec);
System.out.println(customer);
}
/**
* 多条件匹配, 匹配客户名称为 云创动力 且 地址为 西安市未央区的
*/
@Test
public void testSpecification1() {
Specification<Customer> specification = new Specification<Customer>() {
@Override
public Predicate toPredicate(Root<Customer> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
Path<Object> custName = root.get("custName"); //客户名称
Path<Object> address = root.get("custAddress"); //地址
// 构建客户名匹配
Predicate predicate1 = cb.equal(custName, "云创动力");
// 构建地址匹配
Predicate predicate2 = cb.equal(address, "西安市未央区");
// 将多个条件组合, 两者都要满足 与关系, 满足其一即可 或关系
// 与关系
Predicate result = cb.and(predicate1, predicate2);
// 或关系
// Predicate result = cb.or(predicate1, predicate2);
return result;
}
};
List<Customer> lists = customerDao.findAll(specification);
System.out.println(lists);
}
/**
* 模糊匹配, 匹配 custName 中包含 云创 的客户信息
*/
@Test
public void testSpecification2(){
Specification<Customer> spec = new Specification<Customer>() {
@Override
public Predicate toPredicate(Root<Customer> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
// 查询属性:客户名称
Path<Object> custName = root.get("custName");
// 查询方式,模糊查询 like
/**
* equal: 可以直接使用 Path 对象
* 其他的方法得到 Path 对象后,需要指定数据类型.
* 指定的方式是 path.as(类型的字节码对象)
*/
Predicate predicate = cb.like(custName.as(String.class), "%云创%");
return predicate;
}
};
List<Customer> lists = customerDao.findAll(spec);
for (Customer list : lists) {
System.out.println(list);
}
}
}
1.2 基于Specifications的排序
使用的方法是 JpaSpecificationExecutor
接口的中的
List<T> findAll(Specification<T> spec, Sort sort);
方法
这里面需要构造一个 org.springframework.data.domain.Sort
对象, Sort
对象的构造方法有多个
我们使用public Sort(Direction direction, String... properties)
- 第一个参数: 排序的顺序 (升序、倒序)
- Sort.Direction.ASC 升序
- Sort.Direction.DESC 降序
- 排序的属性名称
/**
* 排序
*/
@Test
public void testSpecification3(){
Specification<Customer> spec = new Specification<Customer>() {
@Override
public Predicate toPredicate(Root<Customer> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
// 查询属性:客户名称
Path<Object> custName = root.get("custName");
// 查询方式,模糊查询 like
/**
* equal: 可以直接使用 Path 对象
* 其他的方法得到 Path 对象后,需要指定数据类型.
* 指定的方式是 path.as(类型的字节码对象)
*/
Predicate predicate = cb.like(custName.as(String.class), "%云创%");
return predicate;
}
};
/**
* 排序, 需要构造 Sort 对象。
* sort对象的构造方法有多个,我们使用
* public Sort(Direction direction, String... properties)
* 第一个参数: 排序的顺序 (升序、倒序)
* Sort.Direction.ASC 升序
* Sort.Direction.DESC 降序
* 第二个参数: 排序的属性名称
*/
Sort sort = new Sort(Sort.Direction.DESC, "custId");
List<Customer> lists = customerDao.findAll(spec, sort);
for (Customer list : lists) {
System.out.println(list);
}
}
1.3 基于Specifications的分页查询
使用的方法是 JpaSpecificationExecutor
接口的中的
Page<T> findAll(Specification<T> spec, Pageable pageable)
方法
需要用到 org.springframework.data.domain.Pageable
接口,PageRequest
类实现了 Pageable
接口,调用构造方法的形式构造
public PageRequest(int page, int size)
:
- 第一个参数: 页码(从0开始)
- 第二个参数: 每页查询条数
- 返回一个
org.springframework.data.domain.Page
对象,这是SpringDataJPA为我们封装好的 PageBean 对象
@Test
public void testPage() {
//构造查询条件
Specification<Customer> spec = new Specification<Customer>() {
public Predicate toPredicate(Root<Customer> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
return cb.like(root.get("custName").as(String.class), "%云创%");
}
};
/**
* 构造分页参数
* Pageable : 接口
* PageRequest实现了Pageable接口,调用构造方法的形式构造
* 第一个参数:页码(从0开始)
* 第二个参数:每页查询条数
*/
Pageable pageable = new PageRequest(1, 5);
/**
* 分页查询,封装为Spring Data Jpa 内部的page bean
* 此重载的findAll方法为分页方法需要两个参数
* 第一个参数:查询条件Specification
* 第二个参数:分页参数
*/
Page<Customer> page = customerDao.findAll(spec, pageable);
// 获取内容
List<Customer> lists = page.getContent();
System.out.println(lists);
}
对于Spring Data JPA中的分页查询,是其内部自动实现的封装过程,返回的是一个Spring Data JPA提供的 pageBean 对象。其中的方法说明如下:
//获取总页数
int getTotalPages();
//获取总记录数
long getTotalElements();
//获取列表数据
List<T> getContent();
1.4 CriteriaQuery 方法对应关系
方法名称 | Sql对应关系 |
---|---|
equle | filed = value |
gt(greaterThan ) | filed > value |
lt(lessThan ) | filed < value |
ge(greaterThanOrEqualTo ) | filed >= value |
le( lessThanOrEqualTo) | filed <= value |
notEqule | filed != value |
like | filed like value |
notLike | filed not like value |
第2章 多表设计
2.1 表之间关系的划分
数据库中多表之间存在着三种关系,如图所示。
从图可以看出,系统设计的三种实体关系分别为:
- 多对多
- 一般采用中间表的方式实现
- 中间表一般至少由两个字段组成,这两个字段作为外键指向两张表的主键,这两个字段组成联合主键
- 一对多
- 一的一方叫做 主表
- 多的一方叫做 从表
- 外键: 在 从表 中新建一列作为外键,它的取值来源于主表的主键
- 一对一
注意:一对多关系可以看为两种: 即一对多,多对一。所以说四种更精确。
明确: 我们今天只涉及实际开发中常用的关联关系,一对多和多对多。而一对一的情况,在实际开发中几乎不用。
2.2 在JPA框架中表关系的分析步骤
在实际开发中,我们数据库的表难免会有相互的关联关系,在操作表的时候就有可能会涉及到多张表的操作。而在这种实现了ORM思想的框架中(如JPA),可以让我们通过操作实体类就实现对数据库表的操作。所以今天我们的学习重点是:掌握配置实体之间的关联关系。
实体间的关系:
- 包含关系:通过这种关系描述表关系
- 继承关系
第一步:首先确定两张表之间的关系。
如果关系确定错了,后面做的所有操作就都不可能正确。
第二步:在数据库中实现两张表的关系
通过 外键 | 中间表 描述
第三步:在实体类中描述出两个实体的关系
实体类中使用包含关系描述
第四步:配置出实体类和数据库表的关系映射(重点)
第3章 JPA中的一对多
3.1 示例分析
我们采用的示例为客户和联系人。
客户: 指买我们公司产品的公司
联系人: 指公司的员工
在不考虑兼职的情况下,公司和员工的关系即为一对多。
3.2 表关系建立
在一对多关系中,我们习惯把一的一方称之为主表,把多的一方称之为从表。在数据库中建立一对多的关系,需要使用数据库的外键约束。
什么是外键?
指的是从表中有一列,取值参照主表的主键,这一列就是外键。
一对多数据库关系的建立,如下图所示
3.3 实体类关系建立以及映射配置
在实体类中,由于客户是少的一方,它应该包含多个联系人,所以实体类要体现出客户中有多个联系人的信息,代码如下:
package com.itlaobing.entity;
import javax.persistence.*;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;
/**
* @Classname Customer
* 所有的注解都是使用JPA的规范提供的注解,
* 所以在导入注解包的时候,一定要导入javax.persistence下的
* @Date 2020/4/9 10:10
* @Author by Administrator
* @Version v1.0
*/
//@Data
@Entity //声明实体类
@Table(name="cust_customer") //建立实体类和表的映射关系
public class Customer implements Serializable {
// @Id //声明当前私有属性为主键
// @GeneratedValue(strategy= GenerationType.IDENTITY) //配置主键的生成策略
// @Column(name="cust_id") //指定和表中cust_id字段的映射关系
// private Long custId;
@Id
@GeneratedValue(strategy = GenerationType.TABLE, generator="payablemoney_gen")
@TableGenerator(name = "payablemoney_gen",
table="tb_generator",
pkColumnName="gen_name",
valueColumnName="gen_value",
pkColumnValue="CUSTOMER_PK",
allocationSize=1
)
private Long custId;
@Column(name="cust_name") //指定和表中cust_name字段的映射关系
private String custName;
@Column(name="cust_source")//指定和表中cust_source字段的映射关系
private String custSource;
@Column(name="cust_industry")//指定和表中cust_industry字段的映射关系
private String custIndustry;
@Column(name="cust_level")//指定和表中cust_level字段的映射关系
private String custLevel;
@Column(name="cust_address")//指定和表中cust_address字段的映射关系
private String custAddress;
@Column(name="cust_phone")//指定和表中cust_phone字段的映射关系
private String custPhone;
/**
* 配置 一对多 关系
* 使用注解配置多表关系
* 1. 声明关系
* @oneToMany: 配置一对多关系
* targetEntity; 对方对象的字节码
* 2. 配置外键(中间表)
* @JoinColumn: 配置外键
* name: 外键字段名称
* referencedColumnName: 参照的主表的主键名称
* 在客户实体(一的一方)上面配置了外键,所以客户也具备了维护外键的作用
*/
@OneToMany(targetEntity = Contacts.class)
@JoinColumn(name = "cust_id", referencedColumnName = "custId")
private Set<Contacts> contactsSet = new HashSet<Contacts>();
// 省略 getter/setter 方法
}
由于联系人是多的一方,在实体类中要体现出,每个联系人只能对应一个客户,代码如下:
package com.itlaobing.entity;
import javax.persistence.*;
/**
* @Classname Contacts
* @Description TODO()
* @Date 2020/4/24 16:46
* @Author by Administrator
* @Version v1.0
*/
//@Data
@Entity
@Table(name = "cust_contacts")
public class Contacts {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "contacts_id")
//联系人编号
private int contactsId;
@Column(name="contacts_name")
//联系人姓名
private String contactsName;
@Column(name="contacts_gender")
//联系人性别
private String contactsGender;
@Column(name="contacts_phone")
// 联系人电话
private String contactsPhone;
@Column(name="contacts_email")
//联系人邮箱
private String contactsEmail;
@Column(name="contacts_memo")
//联系人备注
private String contactsMemo;
/**
* 配置联系人到客户的 多对一 关系
* 使用注解配置 多对一 关系
* 1. 配置表关系
* 2. 配置外键(中间表)
* 多个一方配置外键,那么多的一方也会维护外键
*/
@ManyToOne(targetEntity = Customer.class)
@JoinColumn(name = "cust_id", referencedColumnName = "custId")
private Customer customer;
//省略 getter/setter 方法
}
现在客户实体中能找到联系人,联系人实体中能找到客户。这种关系我们叫做双向关系。如果只配置任意一方,表示单向关系
3.4 映射的注解说明
@OneToMany:
作用:建立一对多的关系映射
属性:
targetEntity:指定多的多方的类的字节码
mappedBy:指定从表实体类中引用主表对象的名称。
cascade:指定要使用的级联操作
fetch:指定是否采用延迟加载
orphanRemoval:是否使用孤儿删除
@ManyToOne
作用:建立多对一的关系
属性:
targetEntity:指定一的一方实体类字节码
cascade:指定要使用的级联操作
fetch:指定是否采用延迟加载
optional:关联是否可选。如果设置为false,则必须始终存在非空关系。
@JoinColumn
作用:用于定义主键字段和外键字段的对应关系。
属性:
name:指定外键字段的名称
referencedColumnName:指定引用主表的主键字段名称
unique:是否唯一。默认值不唯一
nullable:是否允许为空。默认值允许。
insertable:是否允许插入。默认值允许。
updatable:是否允许更新。默认值允许。
columnDefinition:列的定义信息。
3.5 一对多的操作
在测试前,可以在spring-application.xml
文件中添加一个配置,以便更好的观察
<!-- jpa hibernate 配置
hibernate.hbm2ddl.auto:
create 每次启动都重新创建表
update 有表不会创建,没表创建
-->
<property name="jpaProperties">
<props>
<prop key="hibernate.hbm2ddl.auto">create</prop>
</props>
</property>
3.5.1 添加
package com.itlaobing;
import com.itlaobing.dao.ContactsDao;
import com.itlaobing.dao.CustomerDao;
import com.itlaobing.entity.Contacts;
import com.itlaobing.entity.Customer;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.annotation.Transactional;
/**
* @Classname test
* @Description TODO()
* @Date 2020/4/24 17:18
* @Author by Administrator
* @Version v1.0
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring-application.xml")
public class OneToManyTest {
@Autowired
private CustomerDao customerDao;
@Autowired
private ContactsDao contactsDao;
/**
* 保存一个客户,保存一个联系人
* 结果: 客户和联系人独立的保存在数据库表中
* 联系人外键为空
* 原因:实体类中没有使用配置关系,所以我们需要在代码中加入
* customer.getContactsSet().add(contacts);
* 或者 contacts.setCustomer(customer);
*/
@Test
@Transactional //配置事务
@Rollback(false) // 不自动回滚
public void testAdd(){
// 创建一个客户,创建一个联系人
Customer customer = new Customer();
customer.setCustName("阿里巴巴");
Contacts contacts = new Contacts();
contacts.setContactsName("小马");
// customer.getContactsSet().add(contacts);
// contacts.setCustomer(customer);
// 保存
customerDao.save(customer);
contactsDao.save(contacts);
}
}
通过保存的案例,我们可以发现在设置了双向关系之后,使用customer.getContactsSet().add(contacts)
会发送两条insert语句,一条多余的update语句。所以我们的解决方法就是一的一方放弃维护权
修改后的内容为:
package com.itlaobing.entity;
import lombok.Data;
import javax.persistence.*;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;
/**
* @Classname Customer
* 所有的注解都是使用JPA的规范提供的注解,
* 所以在导入注解包的时候,一定要导入javax.persistence下的
* @Date 2020/4/9 10:10
* @Author by Administrator
* @Version v1.0
*/
@Data
@Entity //声明实体类
@Table(name="cust_customer") //建立实体类和表的映射关系
public class Customer implements Serializable {
// @Id //声明当前私有属性为主键
// @GeneratedValue(strategy= GenerationType.IDENTITY) //配置主键的生成策略
// @Column(name="cust_id") //指定和表中cust_id字段的映射关系
// private Long custId;
@Id
@GeneratedValue(strategy = GenerationType.TABLE, generator="payablemoney_gen")
@TableGenerator(name = "payablemoney_gen",
table="tb_generator",
pkColumnName="gen_name",
valueColumnName="gen_value",
pkColumnValue="CUSTOMER_PK",
allocationSize=1
)
private Long custId;
@Column(name="cust_name") //指定和表中cust_name字段的映射关系
private String custName;
@Column(name="cust_source")//指定和表中cust_source字段的映射关系
private String custSource;
@Column(name="cust_industry")//指定和表中cust_industry字段的映射关系
private String custIndustry;
@Column(name="cust_level")//指定和表中cust_level字段的映射关系
private String custLevel;
@Column(name="cust_address")//指定和表中cust_address字段的映射关系
private String custAddress;
@Column(name="cust_phone")//指定和表中cust_phone字段的映射关系
private String custPhone;
/**
* 配置 一对多 关系
* 使用注解配置多表关系
* 1. 声明关系
* @oneToMany: 配置一对多关系
* targetEntity; 对方对象的字节码
* 2. 配置外键(中间表)
* @JoinColumn: 配置外键
* name: 外键字段名称
* referencedColumnName: 参照的主表的主键名称
* 在客户实体(一的一方)上面配置了外键,所以客户也具备了维护外键的作用
*/
// @OneToMany(targetEntity = Contacts.class)
// @JoinColumn(name = "cust_id", referencedColumnName = "custId")
/**
* 放弃外键维护权,只需要声明关系即可
* mappedBy:对方配置关系的属性名称(即Contacts类中的 customer)
*/
@OneToMany(mappedBy = "customer")
private Set<Contacts> contactsSet = new HashSet<Contacts>();
}
修改后就只能通过使用contacts.setCustomer(customer)
来维护外键了。
3.5.2 删除
删除操作的说明如下:
删除从表数据:可以随时任意删除。
删除主表数据:
-
有从表数据
1、在默认情况下,它会把外键字段置为null,然后删除主表数据。如果在数据库的表结构上,外键字段有非空约束,默认情况就会报错了。
2、如果配置了放弃维护关联关系的权利,则不能删除(与外键字段是否允许为null, 没有关系)因为在删除时,它根本不会去更新从表的外键字段了。
3、如果还想删除,使用级联删除引用
-
没有从表数据引用:随便删
/**
* 删除
*/
@Test
@Transactional
@Rollback(false)
public void deleteTest(){
Customer customer = new Customer();
customer.setCustName("阿里巴巴");
// 保存
customerDao.save(customer);
//删除
customerDao.delete(1L);
}
在实际开发中,级联删除请慎用!(在一对多的情况下)
3.5.3 级联操作
级联操作:
- 指操作一个对象同时操作它的关联对象
- 需要区分操作主题
- 需要在操作主体的实体类上添加级联属性(添加到多表映射关系的注解上)
- 配置cascade
级联操作有以下几种:
- 级联添加:
- 当保存客户的同时保存联系人
- 级联删除:
- 当删除客户的同时删除所有联系人
@Entity //声明实体类
@Table(name="cust_customer") //建立实体类和表的映射关系
public class Customer implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.TABLE, generator="payablemoney_gen")
@TableGenerator(name = "payablemoney_gen",
table="tb_generator",
pkColumnName="gen_name",
valueColumnName="gen_value",
pkColumnValue="CUSTOMER_PK",
allocationSize=1
)
private Long custId;
@Column(name="cust_name") //指定和表中cust_name字段的映射关系
private String custName;
@Column(name="cust_source")//指定和表中cust_source字段的映射关系
private String custSource;
@Column(name="cust_industry")//指定和表中cust_industry字段的映射关系
private String custIndustry;
@Column(name="cust_level")//指定和表中cust_level字段的映射关系
private String custLevel;
@Column(name="cust_address")//指定和表中cust_address字段的映射关系
private String custAddress;
@Column(name="cust_phone")//指定和表中cust_phone字段的映射关系
private String custPhone;
@OneToMany(mappedBy = "customer", cascade = CascadeType.ALL)
private Set<Contacts> contactsSet = new HashSet<Contacts>();
// 省略 getter/setter 方法
}
/**
* 级联添加: 保存一个客户的同时,保存联系人
* 需要在操作主体的实体类上配置cascade属性
* 我们配置了 cascade = CascadeType.ALL
*/
@Test
@Transactional
@Rollback(false)
public void testCascadeAdd(){
Customer customer = new Customer();
customer.setCustName("阿里巴巴");
Contacts contacts = new Contacts();
contacts.setContactsName("小马");
contacts.setCustomer(customer);
customer.getContactsSet().add(contacts);
// 保存
customerDao.save(customer);
}
/**
* 级联删除
* 删除一号客户的同时删除一号客户的所有联系人
* 注:将 spring-application.xml 中
* hibernate.hbm2ddl.auto 的值改为 update
*/
@Test
@Transactional
@Rollback(false)
public void testCascadeDelete(){
// 两种方式都可以
customerDao.delete(1L);
// // 查询客户信息
// Customer customer = customerDao.findOne(1L);
// // 删除
// customerDao.delete(customer);
}
cascade:配置级联属性
-
CascadeType.ALL 所有 (推荐)
-
CascadeType.MERGE 更新
-
CascadeType.PERSIST 保存
-
CascadeType.REMOVE 删除
第4章 JPA中的多对多
4.1 示例分析
我们采用的示例为用户和角色。
-
用户:指的是班上的每一个同学。
-
角色:指的是班上同学的身份信息。
比如A同学,他是我的学生,其中有个身份就是学生,还是家里的孩子,那么他还有个身份是子女。同时B同学,它也具有学生和子女的身份。
那么任何一个同学都可能具有多个身份。同时学生这个身份可以被多个同学所具有。
所以我们说,用户和角色之间的关系是多对多。
步骤:
- 明确表关系 - 多对多关系
- 确定表关系 - 中间表
- 编写实体类
- 用户: 包含角色的集合
- 角色: 包含用户的集合
- 配置映射关系
4.2 表关系建立
多对多的表关系建立靠的是中间表,其中用户表和中间表的关系是一对多,角色表和中间表的关系也是一对多,如下图所示:
4.3 实体类关系建立以及映射配置
一个用户可以具有多个角色,所以在用户实体类中应该包含多个角色的信息,代码如下:
package com.itlaobing.entity;
import javax.persistence.*;
import java.util.HashSet;
import java.util.Set;
/**
* @Classname SysUser
* @Description TODO()
* @Date 2020/4/26 15:27
* @Author by Administrator
* @Version v1.0
*/
@Entity
@Table(name = "sys_user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id")
private long userId;
@Column(name = "user_name")
private String userName;
@Column(name = "age")
private int age;
/**
* 配置用户到角色多对多关系
* 配置多对多关系:
* 1. 声明表关系
* targetEntity: 对方的实体类字节码
* 2. 配置中间表(包含两个外键)
* @JoinTable:
* name: 中间表的名称
* joinColumns:当前对象在中间表中的外键
* @JoinColumn:
* name: 中间表外键名
* referencedColumnName: 主表主键名
* inverseJoinColumns:对方对象在中间表的外键
* @JoinColumn:
* name: 中间表外键名
* referencedColumnName: 主表主键名
*/
@ManyToMany(targetEntity = Role.class)
@JoinTable(name = "sys_user_role",
// joinColumns 当前对象在中间表中的外键
joinColumns = {@JoinColumn(name = "sys_user_id", referencedColumnName = "user_id")},
// inverseJoinColumns 对方对象在中间表的外键
inverseJoinColumns = {@JoinColumn(name = "sys_role_id", referencedColumnName = "role_id")}
)
private Set<Role> roles = new HashSet<>();
// 省略 getter/setter 方法
}
一个角色可以赋予多个用户,所以在角色实体类中应该包含多个用户的信息,代码如下:
package com.itlaobing.entity;
import javax.persistence.*;
import java.util.HashSet;
import java.util.Set;
/**
* @Classname Role
* @Description TODO()
* @Date 2020/4/26 15:32
* @Author by Administrator
* @Version v1.0
*/
@Entity
@Table(name = "sys_role")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "role_id")
private long roleId;
@Column(name = "role_name")
private String roleName;
@ManyToMany(targetEntity = User.class)
@JoinTable(name = "sys_user_role",
// joinColumns 当前对象在中间表中的外键
joinColumns = {@JoinColumn(name = "sys_role_id", referencedColumnName = "role_id")},
// inverseJoinColumns 对方对象在中间表的外键
inverseJoinColumns = {@JoinColumn(name = "sys_user_id", referencedColumnName = "user_id")}
)
private Set<User> users = new HashSet<>();
// 省略 getter/setter 方法
}
4.4 映射的注解说明
@ManyToMany
作用:用于映射多对多关系
属性:
cascade:配置级联操作。
fetch:配置是否采用延迟加载。
targetEntity:配置目标的实体类。映射多对多的时候不用写。
mappedBy:指定从表实体类中引用主表对象的名称。
@JoinTable
作用:针对中间表的配置
属性:
name:配置中间表的名称
joinColumns:中间表的外键字段关联当前实体类所对应表的主键字段
inverseJoinColumn:中间表的外键字段关联对方表的主键字段
@JoinColumn
作用:用于定义主键字段和外键字段的对应关系。
属性:
name:指定外键字段的名称
referencedColumnName:指定引用主表的主键字段名称
unique:是否唯一。默认值不唯一
nullable:是否允许为空。默认值允许。
insertable:是否允许插入。默认值允许。
updatable:是否允许更新。默认值允许。
columnDefinition:列的定义信息。
4.5 多对多的操作
4.5.1 保存
package com.itlaobing.test;
import com.itlaobing.dao.RoleDao;
import com.itlaobing.dao.UserDao;
import com.itlaobing.entity.Role;
import com.itlaobing.entity.User;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.annotation.Transactional;
/**
* @Classname ManyToManyTest
* @Description TODO()
* @Date 2020/4/26 16:00
* @Author by Administrator
* @Version v1.0
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring-application.xml")
public class ManyToManyTest {
@Autowired
private UserDao userDao;
@Autowired
private RoleDao roleDao;
/**
* 保存用户和角色
*/
@Test
@Transactional
@Rollback(false)
public void testAdd(){
// 创建用户
User user = new User();
user.setUserName("后羿");
// 创建角色
Role role = new Role();
role.setRoleName("射手");
/**
* 如果同时使用两个配置代码,会因为两者都对中间表添加 1 - 1 而报错(主键冲突)
* 解决方式就是其中一方放弃维护权
* 多对多中放弃维护权:被动的一方放弃维护权
*/
// 配置用户到角色的关联关系,可以对中间表数据进行维护 1 - 1
user.getRoles().add(role);
// 配置角色到用户的关联关系,可以对中间表数据进行维护 1 - 1
role.getUsers().add(user);
// 保存
userDao.save(user);
roleDao.save(role);
}
}
在多对多(保存)中,如果双向都设置关系,意味着双方都维护中间表,都会往中间表插入数据,中间表的2个字段又作为联合主键,所以报错,主键重复,解决保存失败的问题:只需要在任意一方放弃对中间表的维护权即可,推荐在被动的一方放弃,修改配置如下:
(Role.java)
/**
* 放弃维护中间表数据权
* mappedBy: 对方实体中对象的名称
*/
@ManyToMany(mappedBy = "roles")
private Set<User> users = new HashSet<>();
4.5.2 删除
将spring-application.xml
配置文件做出如下修改
<property name="jpaProperties">
<props>
<prop key="hibernate.hbm2ddl.auto">update</prop>
</props>
</property>
/**
* 删除操作
* 在多对多的删除时,双向级联删除根本不能配置
* 禁用
* 如果配了的话,如果数据之间有相互引用关系,可能会清空所有数据
*/
@Test
@Transactional
@Rollback(false)//设置为不回滚
public void testDelete() {
// 会删除 sys_user 表中的相应数以及 sys_user_role 表中相关数据
userDao.delete(1l);
}
4.5.3 级联操作
在操作主体(User.java)实体中,配置多表关系中添加 cascade
配置。如下:
User.java
@ManyToMany(targetEntity = Role.class, cascade = CascadeType.ALL)
@JoinTable(name = "sys_user_role",
// joinColumns 当前对象在中间表中的外键
joinColumns = {@JoinColumn(name = "sys_user_id", referencedColumnName = "user_id")},
// inverseJoinColumns 对方对象在中间表的外键
inverseJoinColumns = {@JoinColumn(name = "sys_role_id", referencedColumnName = "role_id")}
)
private Set<Role> roles = new HashSet<>();
/**
* 级联添加
* 保存一个用户的同时,保存用户的关联角色
*/
@Test
@Transactional
@Rollback(false)
public void testCascadeAdd(){
// 创建用户
User user = new User();
user.setUserName("后羿");
// 创建角色
Role role = new Role();
role.setRoleName("射手");
// 配置用户到角色的关联关系,可以对中间表数据进行维护 1 - 1
user.getRoles().add(role);
// 配置角色到用户的关联关系,可以对中间表数据进行维护 1 - 1
role.getUsers().add(user);
// 保存
userDao.save(user);
}
/**
* 级联删除
* 删除ID为1的用户,同时删除它的关联对象
*/
@Test
@Transactional
@Rollback(false)
public void testCascadeRemove(){
// 删除
userDao.delete(1L);
// 或者使用
// User user = userDao.findOne(1L);
// userDao.delete(user);
}
第5章 Spring Data JPA中的多表查询
5.1 对象导航查询
对象图导航检索方式是根据已经加载的对象,导航到他的关联对象。它利用类与类之间的关系来检索对象。例如:我们通过ID查询方式查出一个客户,可以调用Customer类中的getContactsSet()方法来获取该客户的所有联系人。对象导航查询的使用要求是:两个对象之间必须存在关联关系。
查询一个客户,获取该客户下的所有联系人
@Test
@Transactional // 解决 no session 问题
public void findOneTest(){
Customer customer = customerDao.findOne(1L);
// 对象导航查询
Set<Contacts> contactSet = customer.getContactsSet();
for (Contacts contacts : contactSet) {
System.out.println(contacts);
}
}
查询一个联系人,获取该联系人的所有客户
@Test
@Transactional // 解决 no session 问题
public void findOneTest2(){
Contacts contacts = contactsDao.findOne(1L);
// 对象导航查询
Customer customer = contacts.getCustomer();
System.out.println(customer);
}
根据主表对象加载从表数据时,是延时加载(使用时发送sql查询)。根据从表对象加载主表数据时,是立即加载(一次查询出来)
对象导航查询的问题分析
问题1:我们查询客户时,要不要把联系人查询出来?
分析:如果我们不查的话,在用的时候还要自己写代码,调用方法去查询。如果我们查出来的,不使用时又会白白的浪费了服务器内存。
解决:采用延迟加载的思想。通过配置的方式来设定当我们在需要使用时,发起真正的查询。
配置方式:
/*
* fetch:
* FetchType.LAZY 延迟加载
* FetchType.EAGER 立即加载
*/
@OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private Set<Contacts> contactsSet = new HashSet<Contacts>();
问题2:我们查询联系人时,要不要把客户查询出来?
分析:例如:查询联系人详情时,肯定会看看该联系人的所属客户。如果我们不查的话,在用的时候还要自己写代码,调用方法去查询。如果我们查出来的话,一个对象不会消耗太多的内存。而且多数情况下我们都是要使用的。
解决: 采用立即加载的思想。通过配置的方式来设定,只要查询从表实体,就把主表实体对象同时查出来
配置方式:
@ManyToOne(targetEntity = Customer.class, fetch = FetchType.EAGER)
@JoinColumn(name = "cust_id", referencedColumnName = "custId")
private Customer customer;
5.2 使用Specification查询
/**
* Specification的多表查询
*/
@Test
public void testFind(){
Specification<Contacts> specification = new Specification<Contacts>() {
@Override
public Predicate toPredicate(Root<Contacts> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
// Join代表链接查询,通过root对象获取
//创建的过程中,第一个参数为关联对象的属性名称,第二个参数为连接查询的方式(left,inner,right)
//JoinType.LEFT : 左外连接,JoinType.INNER:内连接,JoinType.RIGHT:右外连接
Join<Contacts, Customer> join = root.join("customer",JoinType.INNER);
return cb.like(join.get("custName").as(String.class),"%云创%");
}
};
List<Contacts> list = contactsDao.findAll(specification);
for (Contacts contacts : list) {
System.out.println(contacts);
}
}
近期评论