Spring Data JPA(自学笔记&参考手册),转载自:https://blog.csdn.net/SerikaOnoe/article/details/91662599
开篇说明
最近做的一个项目中,表和表之间的关联属性非常多,因此就考虑使用JPA。以前学过一点Hibernate,而Spring Data JPA和Hibernate非常相似,在Spring Boot的项目中引入也很方便,因此为了在项目中能简单驾驭JPA,我花了3天收集整理了这篇学习笔记。
网上的关于JPA文章很多,但很少有很全面的。JPA的内容点非常多,我查找学习时也没见过一篇很完整的,作为一个小白,我在从无到有的自学过程中对遇到的每一个不懂的知识点,都查阅参考了大量文章,将一些比较复杂、模糊
的概念有条理的梳理、整合
,最后进行了总结
。因此有了这篇我自认为比较详细的学习笔记。
要感谢网上那些博主的无私奉献~此篇文章将网上的各个知识点中我觉得介绍的比较详细的文章内容进行了整合,具体可以参考扩展阅读
章节,参考过的文章中比较好的我都列出来了,不过有些杂乱。
本文中的示例代码大都是我自己测试过的,并且做了补充。文章中的一些原理分析、接口说明,有些是参考别人的,有些是自己分析的,但由于本学生的水平有限,可能有错误或不足之处,欢迎留言指出!
对于JPA的动态查询,这个是重中之重,如果有基础的建议直接看动态查询
相关的示例,我将我目前遇到的所有情况的示例都写出来了,如果你们有什么别的特殊业务需求无法解决的,欢迎留言讨论!
最后,我还放了一些可能会遇到的问题,并提供了解决方案。
Spring Data JPA
三大框架介绍
Hibernate、Mybatis、Spring Data JPA
Hibernate VS Mybatis
1、简介
Hibernate对数据库结构提供了较为完整的封装,Hibernate的O/R Mapping实现了POJO 和数据库表之间的映射,以及SQL 的自动生成和执行。程序员往往只需定义好了POJO 到数据库表的映射关系,即可通过Hibernate 提供的方法完成持久层操作。程序员甚至不需要对SQL 的熟练掌握, Hibernate/OJB 会根据制定的存储逻辑,自动生成对应的SQL 并调用JDBC 接口加以执行。
iBATIS 的着力点,则在于POJO 与SQL之间的映射关系。然后通过映射配置文件,将SQL所需的参数,以及返回的结果字段映射到指定POJO。 相对Hibernate“O/R”而言,iBATIS 是一种“Sql Mapping”的ORM实现。
2、开发对比
Hibernate的真正掌握要比Mybatis来得难些。Mybatis框架相对简单很容易上手,但也相对简陋些。个人觉得要用好Mybatis还是首先要先理解好Hibernate。针对高级查询,Mybatis需要手动编写SQL语句,以及ResultMap。而Hibernate有良好的映射机制,开发者无需关心SQL的生成与结果映射,可以更专注于业务流程。
3、系统调优对比
- Hibernate调优方案:
- 制定合理的缓存策略;
- 尽量使用延迟加载特性;
- 采用合理的Session管理机制;
- 使用批量抓取,设定合理的批处理参数(batch_size);
- 进行合理的O/R映射设计
- Mybatis调优方案:
- MyBatis在Session方面和Hibernate的Session生命周期是一致的,同样需要合理的Session管理机制。MyBatis同样具有二级缓存机制。 MyBatis可以进行详细的SQL优化设计。
- SQL优化方面
- Hibernate的查询会将表中的所有字段查询出来,这一点会有性能消耗。Hibernate也可以自己写SQL来指定需要查询的字段,但这样就破坏了Hibernate开发的简洁性。而Mybatis的SQL是手动编写的,所以可以按需求指定查询的字段。
- Hibernate HQL语句的调优需要将SQL打印出来,而Hibernate的SQL被很多人嫌弃因为太丑了。MyBatis的SQL是自己手动写的所以调整方便。但Hibernate具有自己的日志统计。Mybatis本身不带日志统计,使用Log4j进行日志记录。
4.缓存机制对比
Hibernate缓存
Hibernate一级缓存是Session缓存,利用好一级缓存就需要对Session的生命周期进行管理好。建议在一个Action操作中使用一个Session。一级缓存需要对Session进行严格管理。Hibernate二级缓存是SessionFactory级的缓存。 SessionFactory的缓存分为内置缓存和外置缓存。内置缓存中存放的是SessionFactory对象的一些集合属性包含的数据(映射元素据及预定SQL语句等),对于应用程序来说,它是只读的。外置缓存中存放的是数据库数据的副本,其作用和一级缓存类似.二级缓存除了以内存作为存储介质外,还可以选用硬盘等外部存储设备。二级缓存称为进程级缓存或SessionFactory级缓存,它可以被所有session共享,它的生命周期伴随着SessionFactory的生命周期存在和消亡。
Mybatis缓存
MyBatis 包含一个非常强大的查询缓存特性,它可以非常方便地配置和定制。MyBatis 3 中的缓存实现的很多改进都已经实现了,使得它更加强大而且易于配置。
默认情况下是没有开启缓存的,除了局部的 session 缓存,可以增强变现而且处理循环 依赖也是必须的。要开启二级缓存,你需要在你的 SQL 映射文件中添加一行:
<cache/>
字面上看就是这样。这个简单语句的效果如下:- 映射语句文件中的所有 select 语句将会被缓存。
- 映射语句文件中的所有 insert,update 和 delete 语句会刷新缓存。
- 缓存会使用 Least Recently Used(LRU,最近最少使用的)算法来收回。
- 根据时间表(比如 no Flush Interval,没有刷新间隔), 缓存不会以任何时间顺序 来刷新。
- 缓存会存储列表集合或对象(无论查询方法返回什么)的 1024 个引用。
- 缓存会被视为是 read/write(可读/可写)的缓存,意味着对象检索不是共享的,而 且可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。
所有的这些属性都可以通过缓存元素的属性来修改。
5.总结
Mybatis:小巧、方便、高效、简单、直接、半自动
Hibernate:强大、方便、高效、复杂、绕弯子、全自动
Hibernate VS Mybatis
1.JPA
全称Java Persistence API,通过JDK 5.0注解或XML描述对象-关系表的映射关系,并将运行期的实体对象持久化到数据库中。
- JPA的出现有两个原因:
- 其一,简化现有Java EE和Java SE应用的对象持久化的开发工作
- 其二,Sun希望整合对ORM技术,实现持久化领域的统一
- JPA提供的技术:
- ORM映射元数据:JPA支持XML和JDK 5.0注解两种元数据的形式,元数据描述对象和表之间的映射关系,框架据此将实体对象持久化到数据库表中
- JPA 的API:用来操作实体对象,执行CRUD操作,框架在后台替我们完成所有的事情,开发者从繁琐的JDBC和SQL代码中解脱出来
- 查询语言:通过面向对象而非面向数据库的查询语言查询数据,避免程序的SQL语句紧密耦合
JPA & Hibernate 关系
JPA是需要Provider来实现其功能的,Hibernate就是JPA Provider中很强的一个。从功能上来说,JPA现在就是Hibernate功能的一个子集。可以简单的理解为JPA是标准接口,Hibernate是实现。Hibernate主要是通过三个组件来实现的,及hibernate-annotation、hibernate-entitymanager 和hibernate-core。
- hibernate-annotation是Hibernate支持annotation方式配置的基础,它包括了标准的JPA annotation以及 Hibernate自身特殊功能的annotation。
- hibernate-core是Hibernate的核心实现,提供了Hibernate所有的核心功能。
- hibernate-entitymanager实现了标准的JPA,可以把它看成hibernate-core和JPA之间的适配器,它并不直接提供ORM的功能,而是对hibernate-core进行封装,使得Hibernate符合JPA的规范。
总的来说,JPA是规范,Hibernate是框架,JPA是持久化规范,而Hibernate实现了JPA。
JPA 概要
1.概述
JPA维护一个Persistence Context(持久化上下文),在持久化上下文中维护实体的生命周期。主要包含三个方面的内容:
- ORM元数据。JPA支持annotion或xml两种形式描述对象-关系映射。
- 实体操作API。实现对实体对象的CRUD操作。
- 查询语言。约定了面向对象的查询语言JPQL(Java Persistence Query Language。
JPA的主要API都定义在javax.persistence包中。如果你熟悉Hibernate,可以很容易做出对应:
org.hibernate | javax.persistence | 说明 |
---|---|---|
cfg.Configuration | Persistence | 读取配置信息 |
SessionFactory | EntityManagerFactory | 用于创建会话/实体管理器的工厂类 |
Session | EntityManager | 提供实体操作API,管理事务,创建查询 |
Transaction | EntityTransaction | 管理事务 |
Query | Query | 执行查询 |
扩展阅读
实体类相关
1.1.注解相关
@GeneratedValue 四种标准用法为TABLE,SEQUENCE,IDENTITY,AUTO.
hibernate中的@GeneratedValue与@GenericGenerator
JPA @Id 和 @GeneratedValue 注解详解(重要)
1.2.实体关系映射
1.3.级联概念(重要)
1.4.了解即可
查询相关
1.1.简单入门
1.2.完整流程
1.3.查询进阶
spring boot 配置及使用 spring data jpa
spring data jpa 实现多条件复杂查询及多表联查
spring-data-jpa 介绍 复杂查询,包括多表关联,分页,排序等
1.4.扩展阅读
简单入门
创建Spring Boot项目,在pom.xml引入jpa坐标
1
2
3
4
5
6
7
8
9
10
11
12<!--Spring Data Jpa,会自动导入依赖的包如hibernate的实现类-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!--Mysql,指定了5.XX版本,JPA依赖此包-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
1234567891011配置application.properties
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21#数据库连接设置
jdbc:mysql://xxxx =
root =
xxxx =
com.mysql.jdbc.Driver =
#JPA自动建表
mysql =
## update:若数据接表不存在,则自动创建表,若存在,不做表创建操作
update =
## 是否注册OpenEntityManagerInViewInterceptor。将JPA EntityManager绑定到用于整个请求处理的线程。
## true可以解决web测试下懒加载的session关闭问题
true =
## 非web环境下的懒加载解决方案
true =
## 是否显示sql语句
true =
## 其中spring.jpa.hibernate.naming.physical-strategy是为了修改生成的表和属性的命名策略
## 默认是自动转成小写和下划线形式,versionCode就变成了version_code,其实这种命名策略是比较好的。
## 但是有时候我们可能更加希望属性名称和数据库名称统一,所以增加这个配置后生成的表和属性就和Java类一致
org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl =创建实体类并配置(这举一个一对多的实体类例子,详细的理解往下看)
FairyAdmin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27package cn.dmdream.entity;
import lombok.Data;
import lombok.ToString;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
//自动生成get/set/tostring
"catList")//防止内存溢出 (exclude =
//交给jpa管理实体,并设置映射到数据库的表名
"tab_admin") (name =
public class FairyAdmin {
//主键
"adminId",unique = true,nullable = false) //name属性不写默认和字段一样 (name =
//主键由数据库自动生成(主要是自动增长型) (strategy = GenerationType.IDENTITY)
private Integer adminId;
100,unique = true)//设置长度和唯一性 (length =
private String adminUsername;
100) (length =
private String adminPassword;
100) (length =
private String adminNickname;
255) (length =
private String adminNicpic;
//一的一方
"admin",cascade = CascadeType.ALL,fetch = FetchType.LAZY) (mappedBy =
private List<FairyCat> catList = new ArrayList<FairyCat>();
}FairyCat
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17package cn.dmdream.entity;
import lombok.Data;
import javax.persistence.*;
//@ToString(exclude = {"admin"})
"tab_cat") (name =
public class FairyCat {
(strategy = GenerationType.IDENTITY)
private Integer catId;
false) (nullable =
private String catName;
//多的一方
(cascade = {CascadeType.PERSIST,CascadeType.MERGE,CascadeType.REFRESH},fetch = FetchType.EAGER)
private FairyAdmin admin;
}创建Dao并继承JpaRepository接口,所有基础的增删改查方法都提供了
FairyAdminDao
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26package cn.dmdream.dao;
import cn.dmdream.entity.FairyAdmin;
import cn.dmdream.entity.FairyCat;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import java.util.List;
public interface FairyAdminDao extends JpaRepository<FairyAdmin,Integer> {//两个参数,一个是对应的实体类,一个是该实体类主键的类型
//自定义方法,使用Jpa标准化查询语言
public FairyAdmin findByAdminUsername(String username);
//使用原生的sql进行查询
"select * from tab_admin a,tab_cat c where a.adminId=c.admin_adminId and a.adminUsername like " + (value =
"%?1%",nativeQuery = true)
public List<FairyAdmin> myFindAllByUsernameLike(String username);
//使用JPQL查询,这里会有深坑
"select a,c from FairyAdmin a,FairyCat c where a.adminId=c.admin.adminId and a.adminUsername like " + (value =
"%?1%")
public List<FairyAdmin> myFindAllByUsernameLike_JPQL(String username);
}FairyCatDao
1
2
3
4
5
6
7
8package cn.dmdream.dao;
import cn.dmdream.entity.FairyCat;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
public interface FairyCatDao extends JpaRepository<FairyCat,Integer> {
public FairyCat findByCatName(String name);
}编写测试类TestFairyAdminDao
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211package cn.dmdream.dao;
import cn.dmdream.entity.FairyAdmin;
import cn.dmdream.entity.FairyCat;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.*;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.test.context.junit4.SpringRunner;
import javax.persistence.criteria.*;
import java.util.ArrayList;
import java.util.List;
.class)//指定测试运行类 (SpringRunner
@SpringBootTest(properties = {"classpath:application.properties"})//使用springboot测试
public class TestFairyAdminDao {
private FairyAdminDao fairyAdminDao;
private FairyCatDao fairyCatDao;
//新增
public void saveTest() {
FairyAdmin admin = new FairyAdmin();
admin.setAdminUsername("yoko");
FairyCat cat = new FairyCat();
cat.setCatName("yoko_Cat");
cat.setAdmin(admin);//双向关联保存
admin.getCatList().add(cat);
fairyAdminDao.save(admin);
}
//按用户名查询
public void findByUsernameTest() {
FairyAdmin admin = fairyAdminDao.findByAdminUsername("yoko");
System.out.println(admin);
System.out.println(admin.getCatList());//关联查询
}
//修改
public void updateTest() {
FairyAdmin admin = fairyAdminDao.findByAdminUsername("yoko");
admin.setAdminNickname("yokoNicname");
admin.setAdminPassword("yokoPassword");
admin.getCatList().get(0).setCatName("yoko_cat_edit1");
fairyAdminDao.save(admin);//这里会自动根据主键判断是否是更新
}
//删除
public void deleteTest() {
FairyAdmin admin = fairyAdminDao.findByAdminUsername("yoko");
fairyAdminDao.delete(admin);//级联删除,猫也会被删除
}
//批量保存
public void saveListTest() {
List<FairyAdmin> list = new ArrayList<>();
for (int i = 1; i <= 10; i++) {
FairyAdmin admin = new FairyAdmin();
admin.setAdminUsername("yoko_" + i);
FairyCat cat = new FairyCat();
cat.setCatName("yoko_cat_" + i);
cat.setAdmin(admin);//双向关联保存
admin.getCatList().add(cat);
list.add(admin);
}
fairyAdminDao.saveAll(list);
}
//查询所有
public void findAllTest() {
List<FairyAdmin> list = fairyAdminDao.findAll();
list.forEach(System.out::println);
}
//原生查询语句测试
public void myFindAllByUsernameLikeTest() {
List<FairyAdmin> list = fairyAdminDao.myFindAllByUsernameLike("yoko_1");
list.forEach(System.out::println);
}
//JPQL查询语句测试
public void myFindAllByUsernameLike_JPQLTest() {
List<FairyAdmin> list = fairyAdminDao.myFindAllByUsernameLike_JPQL("yoko_1");
list.forEach(System.out::println);
}
//排序和分页
public void sortAndPageSearchTest() {
//排序的几种方式
//方式一:多字段排序查询,无法自定义顺序,输出语句...order by adminId desc, adminUsername desc
//Sort sort = new Sort(Sort.Direction.DESC,"adminId","adminUsername");
//方式二:多字段排序查询,可自定义顺序,x输出语句:...order by adminUsername asc, adminId desc
Sort sort = new Sort(Sort.Direction.DESC,"adminId");//第一个参数,排序类型:ASC/DESC,第二个参数:按照排序的字段,可以设置多个
Sort sort1 = new Sort(Sort.Direction.ASC,"adminUsername");
final Sort mergeSort = sort1.and(sort);
//方式三:多字段排序查询,先创建order对象,再构造sort
List<Sort.Order> orders = new ArrayList<Sort.Order>();
Sort.Order order1 = new Sort.Order(Sort.Direction.DESC,"adminId");
Sort.Order order2 = new Sort.Order(Sort.Direction.ASC,"adminUsername");
//可以直接单对象创建
Sort orderSort = Sort.by(order2);//...order by adminUsername asc
//可以使用order列表创建
orders.add(order1);
orders.add(order2);
Sort orderListSort = Sort.by(orders);//...order by adminId desc, adminUsername desc
//需要注意的是:page从0开始,不是从1开始!
PageRequest pageRequest = PageRequest.of(0,5, orderListSort);
Page<FairyAdmin> content = fairyAdminDao.findAll(pageRequest);
if (content.hasContent()) {
System.out.println("总记录数:"+content.getTotalElements());
System.out.println("总页数:"+content.getTotalPages());
System.out.println("当前页:"+(content.getPageable().getPageNumber()+1));
System.out.println("当前页面大小:"+content.getPageable().getPageSize());
List<FairyAdmin> list = content.getContent();
list.forEach(System.out::println);
System.out.println(content);
}
System.out.println("==========================================");
}
//测试Example查询,简单测试
public void exampleQueryTest() {
FairyAdmin admin = new FairyAdmin();
admin.setAdminUsername("yoko_5");//精确匹配
Example<FairyAdmin> example = Example.of(admin);//默认会忽略空值的字段
List<FairyAdmin> list = fairyAdminDao.findAll(example);
list.forEach(System.out::println);
}
//测试Example查询,多条件
public void exampleQueryTest2() {
FairyAdmin admin = new FairyAdmin();
admin.setAdminUsername("YOKO");
admin.setAdminPassword("123");
//ExampleMatcher matcher = ExampleMatcher.matching().withIgnoreCase("adminUsername").withMatcher("adminUsername", ExampleMatcher.GenericPropertyMatchers.startsWith()).withIgnorePaths("adminPassword");
//忽略adminUsername,模糊查询开头匹配,忽略adminPassword(无论是否由值都忽略)
//使用lambda版本
ExampleMatcher matcher1 = ExampleMatcher.matching().withIgnoreCase().withMatcher("adminUsername", matcher ->
matcher.startsWith())
.withIgnorePaths("adminPassword");
Example<FairyAdmin> example = Example.of(admin,matcher1);
List<FairyAdmin> list = fairyAdminDao.findAll(example);
list.forEach(System.out::println);
}
//测试Criteria查询
public void criteriaQueryTest() {
//1.新建排序
Sort sort = new Sort(Sort.Direction.DESC,"adminUsername");
//2.新建分页
PageRequest pageRequest = PageRequest.of(0, 5, sort);
//3.实现接口方法specification,添加条件
Specification<FairyAdmin> specification = new Specification<FairyAdmin>() {
public Predicate toPredicate(Root<FairyAdmin> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
//3.1.混合条件查询
Path<String> path1 = root.get("adminUsername");//一定要类型匹配
//Path<String> path2 = root.get("adminPassword");//可以像下面直接写到参数里
Predicate predicate = cb.and(cb.like(path1, "%yoko%"),cb.like(root.get("adminPassword"),
"%456%"));
//3.2.多表查询
//Join<FairyAdmin, FairyCat> join = root.join("catList", JoinType.INNER);
Join<FairyAdmin, FairyCat> join = root.join(root.getModel().getList("catList",FairyCat.class), JoinType
.INNER);
Path<String> catName = join.get("catName");
Predicate predicate2 = cb.and(cb.like(path1, "%yoko%"),cb.like(root.get("adminPassword"),
"%456%"),cb.like(catName,"%cat%"));
//输3.2.1出语句...inner join tab_cat c on adminId=c.admin_adminId where
// (adminUsername like ?) and (adminPassword like ?) and (c.catName like ?) order by adminUsername
// desc limit ?,?
//return predicate2;
//3.3.使用CriteriaQuery直接设置条件,不再需要返回值
query.where(predicate2);//这里可以设置任意条查询条件
return null;//这种方式使用JPA的API设置了查询条件,所以不需要再返回查询条件Predicate给Spring Data Jpa,故最后return null
}
};
//4.执行查询
List<FairyAdmin> list = fairyAdminDao.findAll(specification, pageRequest).getContent();
//list.forEach(System.out::println);
for (FairyAdmin admin:
list) {
System.out.println(admin);
System.out.println(admin.getCatList());//设置了级联关系,默认自动查询了
}
}
}PS:如果都能理解的,看到这里就可以不用看了( ̄3 ̄)a
实体关系映射(重要)
1.作用
简化编程操作。把冗余的操作交给底层框架来处理。
例如,如果我要给一位新入学的学生添加一位新的老师。而这个老师又是新来的,在学生数据库与教师数据库中均不存在对应的数据。那么我需要先在教师数据库中保存新来的老师的数据,同时在学生数据库中保存新学生的数据,然后再给两者建立关联。
而如果我们使用了实体关系映射,我们只需要将该新教师实体交给该学生实体,然后保存该学生实体即可完成。
2.多对多
举例:学生和课程的关系。一位学生,会修多门课程;而一门课程,也会被多位学生修习。此时,双方的关系即为多对多关系。拥有多对多关系的两个实体将会有一个中间表来记录两者之间的关联关系。
Student
1 | package cn.dmdream.entity; |
Course
1 | package cn.dmdream.entity; |
- PS:关于mappedBy可以参考 有关mappedBy的思考
- PS:fetch管读取,cascade管增删改,关于fetch解释如下
- FetchType.LAZY:懒加载,加载一个实体时,定义懒加载的属性不会马上从数据库中加载。
- FetchType.EAGER:急加载,加载一个实体时,定义急加载的属性会立即从数据库中加载。
- @OneToMany默认的是LAZY,@ManyToOne默认是EAGER
测试代码ManyToManyTest
PS:如果发现测试时建表失败,是Mysql数据库版本的问题,要不更换到5.7版本要不将实体的主键增长策略从uuid换成Int自增类型。
1 | package cn.dmdream.dao; |
2.一对多和多对一
- 一对多关系即数据库中的一行数据关联另一个数据库中的多行关系。多对一与之相反。
- 一对多与多对一关系也可能会有中间表关联两者。但是我们一般不建议使用中间表。使用mapperBy可以避免系统生成中间表(会在多的一方数据库中增加一个字段记录外键)。
- 在一的一方使用@OneToMany,fetch默认的是LAZY
- 在多的一方使用@ManyToOne,fetch默认是EAGER
举例一:学生和班级的关系,学生是多的一方,班级是一的一方。如果不在多的一方(学生)中使用mapperBy来增加外键字段,那么将会生成中间表来维护,不建议这么做。
Student
1 | public class Student { |
ClassEntity(班级)
1 | public class ClassEntity { |
举例二(带测试代码):管理员和猫,一个管理员可以有多只猫,管理员能对猫设置所有级联操作,但是猫不能对管理员设置删除级联。
FairyAdmin(一的一方,被维护方)
1 | package cn.dmdream.entity; |
FairyCat(多的一方,关系的维护方)
1 | package cn.dmdream.entity; |
测试代码OneToManyAndReverseTest
1 | package cn.dmdream.dao; |
3.一对一
- 一对一关系即两个数据库中的数据一一对应,这个比较简单
举例:员工和身份证
Emp(员工)
1 | //双向 一对一 |
Identity(身份证)
1 |
|
分析
1 | 以上例子,双向一对一,Emp 为关系拥有方,Identity 为关系被拥有方。 |
4.级联关系(重要)
4.1.级联类型
CascadeType.PERSIST
Cascade persist operation,级联保存操作。
持久保存拥有对方实体时,也会持久保存该实体的所有相关数据。
CascadeType.REMOVE
Cascade remove operation,级联删除操作。
删除当前实体时,与它有映射关系的实体也会跟着被删除。CascadeType.MERGE
Cascade merge operation,级联更新(合并)操作。
当Student中的数据改变,会相应地更新Course中的数据。CascadeType.DETACH
Cascade detach operation,级联脱管/游离操作。
如果你要删除一个实体,但是它有外键无法删除,你就需要这个级联权限了。它会撤销所有相关的外键关联。CascadeType.REFRESH
Cascade refresh operation,级联刷新操作。
假设场景 有一个订单,订单里面关联了许多商品,这个订单可以被很多人操作,那么这个时候A对此订单和关联的商品进行了修改,与此同时,B也进行了相同的操作,但是B先一步比A保存了数据,那么当A保存数据的时候,就需要先刷新订单信息及关联的商品信息后,再将订单及商品保存。CascadeType.ALL
Cascade all operations,清晰明确,拥有以上所有级联操作权限。
4.2.级联保存Demo
1 | public class Student { |
解释:
上面的代码中给了Student对Course进行级联保存(cascade=CascadeType.PERSIST)的权限。此时,若Student实体持有的Course实体在数据库中不存在时,保存该Student时,系统将自动在Course实体对应的数据库中保存这条Course数据。而如果没有这个权限,则无法保存该Course数据。
进一步:如何设置级联保存?
简单理解:想要级联保存谁,就在这个想要保存对象的字段属性上进行级联的设置cascade=CascadeType.类型
实体中需要有别的实体对象,如A类中有一个B的Set集合(需要new出来?看清空)
1
2
3class A{
private Set<B> bSet = new HashSet<B>();//B的Set集合,此对象需要new创建出来?看情况
}需要在这个
bSet
集合属性上使用关系映射的注解,并配置级联关系1
2(cascade=CascadeType.PERSIST,fetch=FetchType.LAZY)
private Set<B> bSet = new HashSet<B>();然后直接保存对象A,当A中有bSet对象时,就会级联保存了
5.封装抽象实体(了解)
5.1.通用抽象基类
BaseEntity(用于继承,因此是抽象类)
1 | package cn.dmdream.entity.base; |
5.2.用户抽象基类
BaseUserEntity
1 | package cn.dmdream.entity.base; |
JPA的注解和属性
PS:常用的注解都已加粗标明
@MappedSuperclass(标注当前实体类是基类,该基类不会被映射)
- @MappedSuperclass注解使用在父类上面,是用来标识父类的
- 标注为@MappedSuperclass的类将不是一个完整的实体类,他将不会映射到数据库表,但是他的属性都将映射到其子类的数据库字段中。
- 标注为@MappedSuperclass的类不能再标注@Entity或@Table注解,也无需实现序列化接口
@Entity(标注实体类,也可以设置表名)
1
2
3
4被Entity标注的实体类将会被JPA管理控制,在程序运行时,JPA会识别并映射到指定的数据库表
唯一参数name:指定实体类名称,默认为当前实体类的非限定名称。
若给了name属性值即@Entity(name="XXX"),则jpa在仓储层(数据层)进行自定义查询时,所查的表名应是XXX。
如:select s from XXX s@Table(用于指定表名,但通常可以用@Entity(name=“table_name”)替代)
1
2
3
4
5
6
7当你想生成的数据库表名与实体类名称不同时,使用 "数据库表名"),与 标注并列使用,置于实体 (name=
类声明语句之前
"t_student") (name=
public class student{
...
}@Table中的参数(不常用)
- catalog: 用于设置表所映射到的数据库的目录
- schema: 用于设置表所映射到的数据库的模式
- uniqueConstraints: 设置约束条件
@Id(用于标明主键)
1
用于实体类的一个属性或者属性对应的getter方法的标注,被标注的的属性将映射为数据库主键
@GeneratedValue(用于指定主键自增策略)
1
2
3
4
5
6与 一同使用,用于标注主键的生成策略,通过 strategy 属性指定。默认是JPA自动选择合适的策略
在 javax.persistence.GenerationType 中定义了以下几种可供选择的策略:
- IDENTITY:采用数据库ID自增长的方式产生主键,Oracle 不支持这种方式。
- AUTO: JPA 自动选择合适的策略,是默认选项。
- SEQUENCE:通过序列产生主键,通过 标注指定序列名,MySQL 不支持这种方式。
- TABLE:通过表产生主键,框架借由表模拟序列产生主键,使用该策略更易于做数据库移植。@Basic(用于标注字段是一个数据库映射字段,默认有)
1
2
3
4
5
6
7表示一个简单的属性到数据库表的字段的映射,对于没有任何标注的 getXxxx() 方法,默认即为 (fetch=FetechType.EAGER)
参数:
1. fetch 表示该属性的加载读取策略
1.1 EAGER 主动抓取 (默认为EAGER)
1.2 LAZY 延迟加载,只有用到该属性时才会去加载
2. optional (默认为 true)
表示该属性是否允许为null@Transient(和上面@Basic相对,建表时忽略有该注解的属性)
1
JPA会忽略该属性,不会映射到数据库中,即程序运行后数据库中将不会有该字段
@Column(设置实体的属性声明)
1
2
3
4
5
6
7
8通常置于实体的属性声明之前,可与 标注一起使用
参数:
1. name: 指定映射到数据库中的字段名
2. unique: 是否唯一,默认为false
3. nullable: 是否允许为null,默认为true
5. insertable: 是否允许插入,默认为true
6. updatetable: 是否允许更新,默认为true
7. columnDefinition: 指定该属性映射到数据库中的实际类型,通常是自动判断。@Temporal(设置时间精度)
1
2
3
4Java中没有定义 Date 类型的精度,而数据库中,表示时间类型的数据有 DATE,TIME,TIMESTAMP三种精度
- (TemporalType.DATE) 表示映射到数据库中的时间类型为 DATE,只有日期
- (TemporalType.TIME) 表示映射到数据库中的时间类型为 TIME,只有时间
- (TemporalType.TIMESTAMP) 表示映射到数据库中的时间类型为 TIMESTAMP,日期和时间都有@Embedded 和 @Embeddable(了解即可,深入理解)
1
2用于一个实体类要在多个不同的实体类中进行使用,而本身又不需要独立生成一个数据库表
1@JoinColumn(用于设置外键字段)
1
2
3
4
5
6
7
8
9定义表关联的外键字段名
常用参数有:
1. name: 指定映射到数据库中的外键的字段名
2. unique: 是否唯一,默认为false
3. nullable: 是否允许为null,默认为true
4. insertable: 是否允许插入,默认为true
5. updatetable: 是否允许更新,默认为true
6. columnDefinition: 指定该属性映射到数据库中的实际类型,通常是自动判断。
7. referencedColumnName:设置引用的类的属性名PS:@JoinColumn常与@ManyToOne(optional=false)一起用,也就是用在多的一方,用于添加外键字段
常用属性名 说明 name = “你要添加的数据库字段名” 设置对应数据表的列名(用作外键) referencedColumnName = “引用的类的属性名” 设置引用的类的属性名 @OneToOne(一对一关系)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
211.targetEntity: 指定关联实体类型,默认为被注解的属性或方法所属的类
2.cascade: 级联操作策略
CascadeType.ALL 级联所有操作
CascadeType.PERSIST 级联新增
CascadeType.MERGE 级联归并更新
CascadeType.REMOVE 级联删除
CascadeType.REFRESH 级联刷新
CascadeType.DETACH 级联分离
3.fetch: fetch 表示该属性的加载读取策略 (默认值为 EAGER)
EAGER 主动抓取
LAZY 延迟加载,只有用到该属性时才会去加载
4.optional: 默认为true,关联字段是否为空
如果为false,则常与 一起使用
5.mappedBy: 指定关联关系,该参数只用于关联关系的被拥有方
只用于双向关联 , , 。而 中没有
(mappedBy = “xxx”)
表示xxx所对应的类为关系被拥有方,而关联的另一方为关系拥有方
* 关系拥有方:对应拥有外键的数据库表
* 关系被拥有方:对应主键被子表引用为外键的数据库表
6.orphanRemoval:默认值为false
判断是否自动删除与关系拥有方不存在联系的关系被拥有方(关系被拥有方的一个主键在关系拥有方中未被引用,当jpa执行更新操作时,是否删除数据库中此主键所对应的一条记录,若为true则删除)@ManyToOne、@OneToMany(多对一和一对多)
多对一(也可叫一对多,只是前后表颠倒一下而已),只有双向多对一时才用得到@OneToMany。多对一中多的一方必定是对应数据库中拥有外键的表,即是关系拥有方,
@ManyToOne
只用在多对一中代表多的一类中,因为mappedBy
只用于关系被拥有方,所以@ManyToOne参数中不包含mappedBy。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25//@ManyToOne参数:
1.targetEntity: 指定关联实体类型,默认为被注解的属性或方法所属的类
2.cascade: 级联操作策略
CascadeType.ALL 级联所有操作
CascadeType.PERSIST 级联新增
CascadeType.MERGE 级联归并更新
CascadeType.REMOVE 级联删除
CascadeType.REFRESH 级联刷新
CascadeType.DETACH 级联分离
3.fetch: fetch 表示该属性的加载读取策略( 的默认值是 EAGER, 的默认值是 LAZY)
EAGER 主动抓取
LAZY 延迟加载,只有用到该属性时才会去加载
4.optional: 默认为true,关联字段是否为空
如果为false,则常与 一起使用
//@OneToMany 参数除上述以外还有:
1.mappedBy: 指定关联关系,该参数只用于关联关系被拥有方
* 一对多与多对一关系也可能会有中间表关联两者。但是我们一般不建议使用中间表。使用mapperBy可以避免系统生成中间表(会在多的一方数据库中增加一个字段记录外键)
只用于双向关联 , , 。而 中没有
(mappedBy = “xxx”)
表示xxx所对应的类为关系被拥有方,而关联的另一方为关系拥有方
* 关系拥有方:对应拥有外键的数据库表
* 关系被拥有方:对应主键被子表引用为外键的数据库表
2.orphanRemoval:默认值为false
判断是否自动删除与关系拥有方不存在联系的关系被拥有方(关系被拥有方的一个主键在关系拥有方中未被引用,当jpa执行更新操作时,是否删除数据库中此主键所对应的一条记录,若为true则删除)@ManyToMany(多对多)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
171.targetEntity: 指定关联实体类型,默认为被注解的属性或方法所属的类
2.cascade: 级联操作策略
CascadeType.ALL 级联所有操作
CascadeType.PERSIST 级联新增
CascadeType.MERGE 级联归并更新
CascadeType.REMOVE 级联删除
CascadeType.REFRESH 级联刷新
CascadeType.DETACH 级联分离
3.fetch: fetch 表示该属性的加载读取策略 (默认值为 LAZY)
EAGER 主动抓取
LAZY 延迟加载,只有用到该属性时才会去加载
4.mappedBy: 指定关联关系,该参数只用于关联关系被拥有方
只用于双向关联 , , 。而 中没有。
(mappedBy = “xxx”)
表示xxx所对应的类为关系被拥有方,而关联的另一方为关系拥有方:
* 关系拥有方:对应拥有外键的数据库表
* 关系被拥有方:对应主键被子表引用为外键的数据库表PS:稍微总结一下关系注解一共4个:@OneToOne、@OneToMany、@ManyToOne、@ManyToMany
常见属性名 说明 cascade=CascadeType.XXX 值:CascadeType.PERSIST/REMOVE/MERGE/DETACH/REFRESH/ALL mappedBy=“自己在对方的属性名” 哪一方使用了此属性,代表这一方放弃维护,即成为被维护方。一对多与多对一关系也可能会有中间表关联两者。但是我们一般不建议使用中间表。使用mapperBy可以避免系统生成中间表(会在多的一方数据库中增加一个字段记录外键) fetch=FetchType.XXX FetchType.LAZY:懒加载,加载一个实体时,定义懒加载的属性不会马上从数据库中加载。 FetchType.EAGER:急加载,加载一个实体时,定义急加载的属性会立即从数据库中加载。 @OneToMany默认的是LAZY,@ManyToOne默认是EAGER optional=true/false optional 属性是定义该关联类对是否必须存在,值为false时,关联类双方都必须存在,如果关系被维护端不存在,查询的结果为null。 值为true 时, 关系被维护端可以不存在,查询的结果仍然会返回关系维护端,在关系维护端中指向关系被维护端的属性为null。 optional 属性的默认值是true。 @Enumerated(用于枚举,了解)
当实体类中有枚举类型的属性时,默认情况下自动生成的数据库表中对应的字段类型是枚举的索引值,是数字类型的,若希望数据库中存储的是枚举对应的String类型,在属性上加入
@Enumerated(EnumType.STRING)
注解即可。1
2
3(EnumType.STRING)
true) (nullable =
private RoleEnum role;
CRUD
1.JPA提供的接口
Repository
最顶层的接口,是一个空的接口,目的是为了统一所有Repository的类型,且能让组件扫描的时候自动识别。
CrudRepository
是Repository的子接口,提供基础的CRUD的功能
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public interface CrudRepository<T, ID> extends Repository<T, ID> {
<S extends T> S save(S entity);
<S extends T> Iterable<S> saveAll(Iterable<S> entities);
Optional<T> findById(ID id);
boolean existsById(ID id);
Iterable<T> findAll();
Iterable<T> findAllById(Iterable<ID> ids);
long count();
void deleteById(ID id);
void delete(T entity);
void deleteAll(Iterable<? extends T> entities);
void deleteAll();
}PagingAndSortingRepository
是CrudRepository的子接口,添加分页和排序的功能
1
2
3
4
5
public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> {
Iterable<T> findAll(Sort sort);
Page<T> findAll(Pageable pageable);
}JpaRepository(常用)(重要,一般直接继承此接口)
是PagingAndSortingRepository的子接口,增加了一些实用的功能,比如:批量操作等。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
List<T> findAll();
List<T> findAll(Sort sort);
List<T> findAllById(Iterable<ID> ids);
<S extends T> List<S> saveAll(Iterable<S> entities);
void flush();
<S extends T> S saveAndFlush(S entity);
void deleteInBatch(Iterable<T> entities);
void deleteAllInBatch();
T getOne(ID id);
<S extends T> List<S> findAll(Example<S> example);
<S extends T> List<S> findAll(Example<S> example, Sort sort);
}
JpaSpecificationExecutor(常用):用来做负责查询的接口,使用到criteria查询需要继承此接口
1
2
3
4
5
6
7public interface JpaSpecificationExecutor<T> {
Optional<T> findOne(@Nullable Specification<T> spec);
List<T> findAll(@Nullable Specification<T> spec);
Page<T> findAll(@Nullable Specification<T> spec, Pageable pageable);
List<T> findAll(@Nullable Specification<T> spec, Sort sort);
long count(@Nullable Specification<T> spec);
}JpaRepositoryImplementation(最佳版本)
:继承了JpaSpecificationExecutor和JpaRepository,做所有的CRUD,如果想要使用JPQL和Criteria两种查询,建议继承此接口
- SimpleJpaRepository:是上面接口的实现类,负责规范化查询语句,里面有EntityManager
- QuerydslJpaRepository:上面的子类,负责处理@Query的语句,里面有EntityManager
- SimpleJpaRepository:是上面接口的实现类,负责规范化查询语句,里面有EntityManager
2.JPA自带的方法
直接继承接口JpaRepository即可
1
public interface UserDao extends JpaRepository<对应的Entity类, 该类的主键类型>{...}
接口里自带的所有增删改查方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24<S extends T> S save(S entity);
<S extends T> Iterable<S> saveAll(Iterable<S> entities);
Optional<T> findById(ID id);
boolean existsById(ID id);
Iterable<T> findAll();
Iterable<T> findAllById(Iterable<ID> ids);
long count();
void deleteById(ID id);
void delete(T entity);
void deleteAll(Iterable<? extends T> entities);
void deleteAll();
Iterable<T> findAll(Sort sort);
Page<T> findAll(Pageable pageable);
List<T> findAll();
List<T> findAll(Sort sort);
List<T> findAllById(Iterable<ID> ids);
<S extends T> List<S> saveAll(Iterable<S> entities);
void flush();
<S extends T> S saveAndFlush(S entity);
void deleteInBatch(Iterable<T> entities);
void deleteAllInBatch();
T getOne(ID id);
<S extends T> List<S> findAll(Example<S> example);
<S extends T> List<S> findAll(Example<S> example, Sort sort);
3.标准查询(重要)
继承接口
JpaRepository<对应的Entity类, 该类的主键类型>
1
public interface UserRepository extends JpaRepository<User, Long>{...}
基础的增删上面继承接口后已经全部实现了,需要自定义的部分只有查询
查询方法需要按照语法规范书写,规范下面表格已详细给出,这里举一个例子
1
2
3
4public interface UserRepository extends JpaRepository<User, Long>{
//按照用户名模糊查询,username是User类的一个字段
findByUsernameLike(String username);
}
标准化查询对应的JPQL语法
Keyword | Sample | JPQL snippet |
---|---|---|
And | findByLastnameAndFirstname | … where x.lastname = ?1 and x.firstname = ?2 |
Or | findByLastnameOrFirstname | … where x.lastname = ?1 or x.firstname = ?2 |
Is,Equals | findByFirstname,findByFirstnameIs,findByFirstnameEquals | … where x.firstname = ?1 |
Between | findByStartDateBetween | … where x.startDate between ?1 and ?2 |
LessThan | findByAgeLessThan | … where x.age < ?1 |
LessThanEqual | findByAgeLessThanEqual | … where x.age ⇐ ?1 |
GreaterThan | findByAgeGreaterThan | … where x.age > ?1 |
GreaterThanEqual | findByAgeGreaterThanEqual | … where x.age >= ?1 |
After | findByStartDateAfter | … where x.startDate > ?1 |
Before | findByStartDateBefore | … where x.startDate < ?1 |
IsNull | findByAgeIsNull | … where x.age is null |
IsNotNull,NotNull | findByAge(Is)NotNull | … where x.age not null |
Like | findByFirstnameLike | … where x.firstname like ?1 |
NotLike | findByFirstnameNotLike | … where x.firstname not like ?1 |
StartingWith | findByFirstnameStartingWith | … where x.firstname like ?1(parameter bound with appended %) |
EndingWith | findByFirstnameEndingWith | … where x.firstname like ?1(parameter bound with prepended %) |
Containing | findByFirstnameContaining | … where x.firstname like ?1(parameter bound wrapped in%) |
OrderBy | findByAgeOrderByLastnameDesc | … where x.age = ?1 order by x.lastname desc |
Not | findByLastnameNot | … where x.lastname <> ?1 |
In | findByAgeIn(Collection ages) | … where x.age in ?1 |
NotIn | findByAgeNotIn(Collection age) | … where x.age not in ?1 |
True | findByActiveTrue() | … where x.active = true |
False | findByActiveFalse() | … where x.active = false |
IgnoreCase | findByFirstnameIgnoreCase | … where UPPER(x.firstame) = UPPER(?1) |
4.JPQL查询
4.1.使用
继承接口
JpaRepository<对应的Entity类, 该类的主键类型>
编写方法和对象查询语言,注意不是传统的Sql语句
在方法名上使用注解@Query,两种写法
- @Query(“原生的SQL语句”)
- @Query(value=“原生的SQL语句”)
占位符
方式一:使用
?1
、?2
… 占位符从一开始1
2"SELECT p FROM Person p WHERE name LIKE %?1% AND sex=?2") (
Person findByNameLikeAndSexIs(String name,Integer sex);//注意要按顺序方式二:Sql语句使用
:param
,并且在方法形参使用注解@Param("param")
注意注解中的值要与Sql中的:param
一致1
2"SELECT p FROM Person p WHERE name LIKE %:name% AND sex=:sex") (
Person findByNameLikeAndSexIs(@Param("sex")Integer sexInt,@Param("name")String nameStr);//可以不按顺序
4.2.示例
1 | public interface UserRepository extends JpaRepository<User, Long> { |
5.原生SQL查询
在@Query注解中添加属性
nativeQuery=true
即可,然后书写原生的Sql即可1
2"SELECT * FROM USERS WHERE EMAIL_ADDRESS = ?1", nativeQuery = true) (value =
User findByEmailAddress(String emailAddress);其余需要注意的参考上面的 JPQL查询
6.自定义增删改
在进行增删改的时候,一定要使用注解
@Modifying
进行标注,作用:提示 JPA 该操作是修改操作如果报错,添加事务:在方法上添加注解
@Transactional
1
2
3
4//更新语句,一定要用@Modifying声明,在service层一定要使用事务@Transactional,要不会报错
"update User u set u.firstname = ?1 where u.lastname = ?2") (
int setFixedFirstnameFor(String firstname, String lastname);有可能会有缓存存在而无法更新的情况: 阅读参考
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17//1.当进行 find 操作时,JPA 在 EntityManager 中缓存了 find 生成的对象,当再次 find 时会直接返回该对象。于是可能会出现下面这种情况 用 @Query 定义一个修改状态的方法
public interface EntityRepository extends JpaRepository<Entity, Integer> {
"update Entity set status = 'IGNORED' where id = ?1") (
int updateStatus(int id);
}
//2.先读取一个对象,再修改对象状态,再次读取对象
Optional<Entity> entityBefore = repository.findById(1);
repository.updateStatus(1);
Optional<Entity> entityAfter = repository.findById(1);
//3.结果会发现 entityBefore 和 entityAfter 中的 Entity 对象 id 是相同的,中间对状态的修改并没有体现出来!当然,其原因也很明确,@Query 跟 find 和 save 系列方法是两套不同的体系,@Query 引起的数据库变更 EntityManager 并不能发现,更进一步说,使用其它工具或者其它框架修改数据库中的数据,也不能及时反应到 JPA 的 find 系列方法上来。
//4.当然,只要有缓存机制就一定不可避免存在此类问题,这仅是个取舍问题而不要认为是 BUG。如果要解决 find 得到的值不是数据库中最新值的问题可以有几种方式,避免使用 @Query 是一种方式,在需要时显式清理 EntityManager 的缓存也是一种方式。Spring Data JPA 提供了另外一种方式则是 @Modifying(clearAutomatically = true),@Modifying 的 clearAutomatically 属性为 true 时,执行完 modifying query 之后就会清理缓存,从而在下次 find 时就可以读取到数据库中的最新值。
//5.自动清理之后还会带来一个新的问题,clear 操作清理的缓存中,还包括提交后未 flush 的数据,例如调用 save 而不是 saveAndFlush 就有可能不会立即将修改内容更新到数据库中,在 save 之后 flush 之前调用 @Modifying(clearAutomatically = true) 修饰的方法就有可能导致修改丢失。如果再要解决这个问题,还可以再加上另外一个属性 @Modifying(clearAutomatically = true, flushAutomatically = true),@Modifying 的 flushAutomatically 属性为 true 时,执行 modifying query 之前会先调用 flush 操作,从而避免数据丢失问题。
//6.在实际运行中,clear 和 flush 操作都可能需要消耗一定的时间,要根据系统实际情况可以选择使用其中的一个或两个属性,以保证系统的正确性。
7.动态查询
7.1.接口关系
AbstractQuery接口:可以获取root,构建查询条件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public interface AbstractQuery<T> extends CommonAbstractCriteria {
//最关键的是from方法,可以得到语句的条件部分根对象
<X> Root<X> from(Class<X> entityClass);
<X> Root<X> from(EntityType<X> entity);
AbstractQuery<T> where(Expression<Boolean> restriction);
AbstractQuery<T> where(Predicate... restrictions);
AbstractQuery<T> groupBy(Expression<?>... grouping);
AbstractQuery<T> groupBy(List<Expression<?>> grouping);
AbstractQuery<T> having(Expression<Boolean> restriction);
AbstractQuery<T> having(Predicate... restrictions);
AbstractQuery<T> distinct(boolean distinct);
Set<Root<?>> getRoots();
Selection<T> getSelection();
List<Expression<?>> getGroupList();
Predicate getGroupRestriction();
boolean isDistinct();
Class<T> getResultType();
}CriteriaQuery接口:继承了AbstractQuery接口,用于构建查询条件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public interface CriteriaQuery<T> extends AbstractQuery<T> {
CriteriaQuery<T> select(Selection<? extends T> selection);
CriteriaQuery<T> multiselect(Selection<?>... selections);
CriteriaQuery<T> multiselect(List<Selection<?>> selectionList);
CriteriaQuery<T> where(Expression<Boolean> restriction);//应用查询条件实例的方法
CriteriaQuery<T> where(Predicate... restrictions);//应用查询条件实例的方法
CriteriaQuery<T> groupBy(Expression<?>... grouping);
CriteriaQuery<T> groupBy(List<Expression<?>> grouping);
CriteriaQuery<T> having(Expression<Boolean> restriction);
CriteriaQuery<T> having(Predicate... restrictions);
CriteriaQuery<T> orderBy(Order... o);
CriteriaQuery<T> orderBy(List<Order> o);
CriteriaQuery<T> distinct(boolean distinct);
List<Order> getOrderList();
Set<ParameterExpression<?>> getParameters();
}
CriteriaBuilder接口,能创建CriteriaQuery对象,还能创建查询条件(Expression表达式),定义了几乎所有的sql逻辑判断相关的表达式创建方法,功能非常全,因此一般都是用此接口中的方法。实现类是由hibernate提供
1
2
3
4
5
6
7
8//这里列一下Expression的继承树,仅列出常见的
* Expression (定义了:isNull、isNotNull、in、as。。。)
* Predicate(定义了:not,还有枚举对象{AND,OR}。。。)
* Subquery(子查询接口,里面有select、where、groupBy、having。。。)
* Path(JPA定义的,定义了一些获取方法,有和实体相关的,有和表达式相关的)
* From(JPA定义的,定义了许多join相关的方法)
* Join(连接查询,定义了join的条件on。。。)
* Root(就一个获取实体的方法)Specification接口:处理复杂查询,如多条件分页查询 Specification接口使用和意义
里面定义了基本的连接语句动态添加的方法,
Specifications
是该接口的实现类注意方法toPredicate(断言方法),此方法负责处理criteria语句的条件,需要实现此方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25"deprecation") (
public interface Specification<T> extends Serializable {
long serialVersionUID = 1L;
static <T> Specification<T> not(Specification<T> spec) {
return Specifications.negated(spec);
}
static <T> Specification<T> where(Specification<T> spec) {
return Specifications.where(spec);
}
default Specification<T> and(Specification<T> other) {
return Specifications.composed(this, other, AND);
}
default Specification<T> or(Specification<T> other) {
return Specifications.composed(this, other, OR);
}
//上面几个方法中,Specifications是该接口的实现类,可能适用于语句的连接,方法都过期了,我没用过
Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder);
//需要实现改方法,默认绑定了3个参数,root和criteriaBuilder都十分有用
/*网上看到有人这么总结(其实就是上面的参考链接~)
Root:查询哪个表
CriteriaQuery:查询哪些字段,排序是什么
CriteriaBuilder:字段之间是什么关系,如何生成一个查询条件,每一个查询条件都是什么方式
Predicate(Expression):单独每一条查询条件的详细描述*/
}JpaSpecificationExecutor接口:是复杂查询的主要入口,传入Specification对象
这个接口里的方法全部是围绕着上面的接口
Specification
写的想使用Specification对象构造复杂查询语句,Dao层需要先继承该接口,然后实现Specification的toPredicate方法。
1
2
3
4
5
6
7
8
9import org.springframework.data.jpa.domain.Specification;
public interface JpaSpecificationExecutor<T> {
Optional<T> findOne(@Nullable Specification<T> spec);
List<T> findAll(@Nullable Specification<T> spec);
Page<T> findAll(@Nullable Specification<T> spec, Pageable pageable);//分页,推荐使用
List<T> findAll(@Nullable Specification<T> spec, Sort sort);
long count(@Nullable Specification<T> spec);
}JpaRepositoryImplementation接口(最佳partner):查看过JPA提供的接口我们可以发现,继承JpaRepositoryImplementation 将是我们最好的选择!既能使用JpaRepository的自带方法,又能使用specification自己构造查询,最全解决方案~接口定义如下:
1
2
public interface JpaRepositoryImplementation<T, ID> extends JpaRepository<T, ID>, JpaSpecificationExecutor<T> {...}
7.2.Criteria 查询原理
是一种类型安全和更面向对象的查询
- 基本对象的构建
- 通过EntityManager的getCriteriaBuilder或EntityManagerFactory的getCriteriaBuilder方法可以得到CriteriaBuilder对象(该对象由hibernate提供)
- 通过调用CriteriaBuilder的createQuery或createTupleQuery方法可以获得CriteriaQuery的实例
- 通过调用CriteriaQuery的from方法(方法定义在AbstractQuery,但是由CriteriaQueryImpl实现)可以获得Root实例
- 过滤条件
- 过滤条件会被应用到SQL语句的FROM子句中。在criteria 查询中,查询条件通过Predicate或Expression实例应用到CriteriaQuery对象上。
- 这些条件使用 CriteriaQuery .where 方法应用到CriteriaQuery 对象上
- CriteriaBuilder也作为Predicate实例的工厂,通过调用CriteriaBuilder 的条件方法( equal,notEqual, gt, ge,lt, le,between,like等)创建Predicate对象。
- 复合的Predicate 语句可以使用CriteriaBuilder的and, or andnot 方法构建
7.3.Criteria查询
Spring Data Jpa:分页、Specification、Criteria PS:这个写的也不错,最后才发现。。
使用条件
Dao层需要继承JpaSpecificationExecutor接口,但前面我也介绍了,推荐继承JpaRepositoryImplementation接口 ,这里我也继承它!
1
2
3
public interface FairyAdminDao extends JpaRepositoryImplementation<FairyAdmin,Integer> {//两个参数,一个是对应的实体类,一个是该实体类主键的类型}
12
7.4.综合查询示例
1 | //测试Criteria查询 |
7.5.封装条件列表示例
示例一
1 | public List<Student> queryStudent(final StudentParam studentParam) { |
网上大佬的详细示例代码二(建议使用这种方式抽取方法)
1 | /** |
7.6.双表连接动态查询
Service
1 |
|
Controller
1 | "user/personalAgent") ( |
7.7.三表连接动态查询
多:级联关系中的多方;一:级联关系中的一方
双向:双向关联;单向:单向关联
业务场景:
实体HouseEntity(多,双向)中有实体CommunityEntity(一,双向)
- 实体CommunityEntity(一,单向)中有实体AddressEntity(一)
- 实体AddressEntity中的字段areaName表示精确到区/县的地址(如杭州市的江干区/西湖区之类的)
- 需求:要根据areaName查询实体HouseEntity,因此要使用到三张表的关联
- 实体CommunityEntity(一,单向)中有实体AddressEntity(一)
Dao层继承接口
1
2
public interface HouseDao extends JpaRepositoryImplementation<HouseEntity, Long> {}Service编写动态查询方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49/**
* 动态创建条件
*/
private Specification<HouseEntity> getWhereClause(final HouseEntity house) {
return new Specification<HouseEntity>() {
public Predicate toPredicate(Root<HouseEntity> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
List<Predicate> predicates = new ArrayList<>();
//地址head查询,三表联查
if (!EmptyUtils.isEmpty(house.getCommunityEntity())&&!EmptyUtils.isEmpty(house.getCommunityEntity().getAddressHead()) && !EmptyUtils.isEmpty(house.getCommunityEntity().getAddressHead().getAreaName())) {//三个判空操作
Join<HouseEntity, CommunityEntity> entityJoin = root.join("communityEntity", JoinType.LEFT);
Join<Object, AddressEntity> JoinThird = entityJoin.join("addressHead", JoinType.LEFT);
predicates.add(cb.like(JoinThird.get("areaName").as(String.class), "%"+house.getCommunityEntity().getAddressHead().getAreaName()+"%"));
}
//售价范围,两表联查
if (!EmptyUtils.isEmpty(house.getPriceType()) && !EmptyUtils.isEmpty(house.getPriceType().getId())) {
Join<HouseEntity, DictEntity> join = root.join("priceType", JoinType.LEFT);
predicates.add(cb.equal(join.get("id").as(Long.class), house.getPriceType().getId()));
}
//其它查询条件略······
Predicate[] array = new Predicate[predicates.size()];
return query.where(predicates.toArray(array)).getRestriction();
}
};
}
/**
* 查询方法
* @param houseEntity
* @param sort
* @param page
* @param pageSize
* @return
*/
public Page<HouseEntity> findByHouseByPage(HouseEntity houseEntity, Sort sort, Integer page, Integer pageSize) {
try {
PageRequest pageRequest = PageRequest.of(page - 1, pageSize, sort);
Specification<HouseEntity> specification = getWhereClause(houseEntity);
Page<HouseEntity> pageModel = houseDao.findAll(specification, pageRequest);
return pageModel;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}Controller
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private HouseService houseService;
"toSecondList") (
public ModelAndView toSecondListPage(HouseEntity houseSearch, @RequestParam(value = "sortField", defaultValue = "createTime") String sortField, @RequestParam(defaultValue = "DESC") String order) {
//仅显示关键代码,其余略······
//2.2.设置排序,三个循环防止判断攻击
Sort sort = null;
if (order.equalsIgnoreCase("DESC")) {
sort = Sort.by(Sort.Direction.DESC, sortField);
} else if (order.equalsIgnoreCase("ASC")) {
sort = Sort.by(Sort.Direction.ASC, sortField);
} else {
sort = Sort.by(Sort.Direction.DESC, sortField);
}
//3.执行动态查询
Page<HouseEntity> pageModel = houseService.findByHouseByPage(houseSearch, sort, 1, 10);
List<HouseEntity> list = pageModel.getContent();
for (HouseEntity h :
list) {
System.out.println(h);
System.out.println(h.getCommunityEntity().getAddressHead().getAreaName());
}
//4.返回视图模型
ModelAndView modelAndView = new ModelAndView("user/SecondHousePage");
//4.1.分页对象存入域
modelAndView.addObject("pageModel", pageModel);
return modelAndView;
}
7.8.Example查询
和上面介绍的有所不同,不是一个体系,了解
Example api的组成
- Probe: 含有对应字段的实例对象。
- ExampleMatcher:ExampleMatcher携带有关如何匹配特定字段的详细信息,相当于匹配条件。
- Example:由Probe和ExampleMatcher组成,用于查询。
缺点
- 属性不支持嵌套或者分组约束,比如:
firstname = ?0 or (firstname = ?1 and lastname = ?2)
- 灵活匹配只支持字符串类型,其他类型只支持精确匹配
- 对于非字符串属性的只能精确匹配,比如想查询在某个时间段内注册的用户信息,就不能通过Example来查询
优点
- 通过在使用springdata jpa时可以通过Example来快速的实现动态查询,同时配合Pageable可以实现快速的分页查询功能。
示例代码
1 | //测试Example查询,简单测试 |
还有两个网上大佬写的例子
1 | //创建查询条件数据对象 |
8.排序和分页
8.1.排序
方式一:直接构造Sort对象排序
1
2
3//方式一:多字段排序查询,无法自定义顺序,输出语句...order by adminId desc, adminUsername desc
Sort sortMulti = new Sort(Sort.Direction.DESC,"adminId","adminUsername");//可多字段
Sort sort = new Sort(Sort.Direction.DESC,"adminId");//单字段方式二:构造多个sort对象,并合并
1
2
3
4//方式二:多字段排序查询,可自定义顺序,x输出语句:...order by adminUsername asc, adminId desc
Sort sort = new Sort(Sort.Direction.DESC,"adminId");//第一个参数,排序类型:ASC/DESC,第二个参数:按照排序的字段,可以设置多个
Sort sort1 = new Sort(Sort.Direction.ASC,"adminUsername");
final Sort mergeSort = sort1.and(sort);方式三:先构造多个Order对象,再构造Sort对象
1
2
3
4
5
6
7
8
9
10//方式三:多字段排序查询,先创建order对象,再构造sort
List<Sort.Order> orders = new ArrayList<Sort.Order>();
Sort.Order order1 = new Sort.Order(Sort.Direction.DESC,"adminId");
Sort.Order order2 = new Sort.Order(Sort.Direction.ASC,"adminUsername");
//可以直接单对象创建
Sort orderSort = Sort.by(order2);//...order by adminUsername asc
//可以使用orders列表创建
orders.add(order1);
orders.add(order2);
Sort orderListSort = Sort.by(orders);//...order by adminId desc, adminUsername desc
8.2.分页
Dao层需要继承接口
JpaRepository
(是PagingAndSortingRepository的子接口,主要方法再该接口内)
构建PageRequest对象
PS:注意page是从0开始的!
1
2
3
4
5
6
7
8
9
10
11
12//需要注意的是:page从0开始,不是从1开始!
PageRequest pageRequest = PageRequest.of(0,5, orderListSort);//最后一个参数是排序对象
Page<FairyAdmin> content = fairyAdminDao.findAll(pageRequest);
if (content.hasContent()) {
System.out.println("总记录数:"+content.getTotalElements());
System.out.println("总页数:"+content.getTotalPages());
System.out.println("当前页:"+(content.getPageable().getPageNumber()+1));
System.out.println("当前页面大小:"+content.getPageable().getPageSize());
List<FairyAdmin> list = content.getContent();
list.forEach(System.out::println);
System.out.println(content);
}
8.3.用关联表字段排序
直接
实体属性名.该实体的属性
即可CommunityEntity
1
2
3public class CommunityEntity{
private AddressEntity addressHead;
}AddressEntity
1
2
3public class AddressEntity{
private Date createTime;
}Controller
1
Sort sort = Sort.by(Sort.Direction.DESC, "addressHead.createTime");
8.4.完整示例
1 | //排序和分页 |
9.关联查询
只要在entity中为属性设置好了一对一、一对多、多对一、多对多之类的关系,就会自动关联查询了
要注意的点,在@ManyToMany这一类的注解有个属性
fetch=FetchType.LAZY/EAGER
使用EAGER会立即加载
使用Lazy会延迟加载(访问的时候才会发送sql语句加载)
但是,有些情况下,session如果被关闭,将无法调用session进行懒加载,从而报错
总结
套用的一个大佬的话
- Hibernate/JPA的DAO层开发比较简单,对于刚接触ORM的人来说,能够简化开发工程,提高开发速度。
- Hibernate/JPA对对象的维护和缓存做的很好,对增删改查的对象的维护要方便。
- Hibernate/JPA数据库移植性比较好。
- Hibernate/JPA功能强大,如果对其熟悉,对其进行一定的封装,那么项目的整个持久层代码会比较简单。
- 感谢阅读~
问题与解决
1.JPA不自动创建表
方式一:将spring boot的运行入口类放在顶级包目录下,entity、dao、service等包结构在其子目录下,包结构如下所示:
- cn.dmdream
- 入口类FairyhouseApplication.java
- entity
- dao
- service
- 。。。其它包
- cn.dmdream
方式二:在入口类使用注解
1
2"cn.dmdream.dao"}) (basePackages = {
"cn.dmdream.entity"})//注意此注解 (basePackages = {
2.多对一删除的问题
1 | /** |
3.JPQL语句查询问题
旧版(BUG版)实体类
1 | //自动生成get/set/tostring |
Dao
1 |
|
看起来一切OK?
结果:测试运行时一直提示错误
1 | java.lang.IllegalArgumentException: org.hibernate.hql.internal.ast.QuerySyntaxException |
反复的检查JPQL语法,没问题啊,网上查找类似的问题,也就是一直说需要写上实体类的类名,我也写上了啊,(我甚至还考虑到是不是LomBok的锅。。徒劳。。)但为什么还是出错呢?
终极原因
在JPQL语法中,select a from 类名 a
这语句中的类名
其实不只是参照你的实体类名,他会优先参照的是你在实体类上使用的注解@Entity(name = "tab_admin")
中的自定义name字段,因此,修改JPQL语句
1 | "select a from tab_admin a") ( |
这样子终于能查询出来了!好坑啊有木有!!!花了2小时找这BUG真的难受。。。网上怎么没人和我一样碰到呢,QAQ。。
还有个建议,在实体类上不要使用@Entity(name = "tab_admin")
来标注名称,需要指定映射的数据库名,最好使用@Table(name="XXX")
。
4.懒加载问题
方式一:Spring Boot的话,配置文件添加
1
2
3
4
5## 是否注册OpenEntityManagerInViewInterceptor。将JPA EntityManager绑定到用于整个请求处理的线程
## true可以解决web测试下懒加载的session关闭问题。
false =
## 非web环境下的懒加载解决方案
true =方式二:在查询中使用 fetch 的方式
1
2"from User u join fetch u.authorityList") (
public User findOne(Long id);
5.JPA建表失败
在实体类使用注解@columnDefinition的时候,注意一定要按 类型开头进行定义,下面就是正确的
1
2"createTime",columnDefinition="DATETIME comment '创建时间'") (name=
protected Date createTime; //创建时间如果定义成下面这样,就会报错误,分析sql语句可以知道,使用该注解就不会帮你默认添加类型了!
1
2"createTime",columnDefinition="comment '创建时间'")//是不会自己判断添加类型了 (name=
protected Date createTime; //创建时间
6.JPA双向关联时Json序列化失败
在双向关系的某一方属性上使用注解
@JsonIgnore
7.JPA普通保存和立即保存
- save不会立刻提交到数据库,flush则立刻提交生效,save可能只是修改在内存中的
发布时间: 2020-09-08
最后更新: 2023-01-27
本文标题: Spring Data JPA笔记参考
本文链接: https://blog-yilia.xiaojingge.com/posts/6151efe2.html
版权声明: 本作品采用 CC BY-NC-SA 4.0 许可协议进行许可。转载请注明出处!
