前言
Spring Data JPA是Spring Data的一個(gè)子項(xiàng)目拜轨,通過(guò)提供基于JPA的Repository極大的減少了JPA作為數(shù)據(jù)訪問(wèn)方案的代碼量印颤,你僅僅需要編寫(xiě)一個(gè)接口集成下SpringDataJPA內(nèi)部定義的接口即可完成簡(jiǎn)單的CRUD操作事甜。
本文從構(gòu)建項(xiàng)目到對(duì)JPA的詳細(xì)使用馒疹,爭(zhēng)取能夠盡量全的演示JPA的相關(guān)應(yīng)用佳簸,大體內(nèi)容如下:
- JPA環(huán)境搭建、配置
- 表關(guān)系配置演示:多對(duì)多颖变、多對(duì)一生均、一對(duì)多
- 基本CRUD操作
- JPA實(shí)體對(duì)象的4種狀態(tài)詳解
- Example查詢(xún)
- 接口規(guī)范方法名查詢(xún)
- @Query注解使用
- Criteria查詢(xún)
- 性能問(wèn)題解決(循環(huán)引用、N+1查詢(xún))
一腥刹、構(gòu)建項(xiàng)目
引入依賴(lài)
新建springboot項(xiàng)目马胧,在pom文件中引入jpa的相關(guān)依賴(lài),如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
另外我們?cè)谝肓硗獾膚eb及數(shù)據(jù)庫(kù)連接相關(guān)的依賴(lài):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- alibaba的druid數(shù)據(jù)庫(kù)連接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
數(shù)據(jù)源及jpa配置
在application.yml文件中加入如下配置:
server:
port: 8080
spring:
datasource:
name: mysql_test
#基本屬性
url: jdbc:mysql://localhost:3306/test?useSSL=false&useUnicode=true&characterEncoding=utf8&autoReconnect=true&rewriteBatchedStatements=TRUE&serverTimezone=UTC&allowMultiQueries=true
username: root
password: root
type: com.alibaba.druid.pool.DruidDataSource
#druid相關(guān)配置
druid:
#監(jiān)控統(tǒng)計(jì)攔截的filters
filters: stat
#配置初始化大小/最小/最大
initial-size: 1
min-idle: 1
max-active: 20
#獲取連接等待超時(shí)時(shí)間
max-wait: 60000
#間隔多久進(jìn)行一次檢測(cè)衔峰,檢測(cè)需要關(guān)閉的空閑連接
time-between-eviction-runs-millis: 60000
#一個(gè)連接在池中最小生存的時(shí)間
min-evictable-idle-time-millis: 300000
validation-query: SELECT 'x'
test-while-idle: true
test-on-borrow: false
test-on-return: false
#打開(kāi)PSCache佩脊,并指定每個(gè)連接上PSCache的大小。oracle設(shè)為true垫卤,mysql設(shè)為false邻吞。分庫(kù)分表較多推薦設(shè)置為false
pool-prepared-statements: false
max-pool-prepared-statement-per-connection-size: 20
jpa:
show-sql: true
# 指定生成表名的存儲(chǔ)引擎為InnoDBD
database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
hibernate:
# 自動(dòng)創(chuàng)建|更新|驗(yàn)證數(shù)據(jù)庫(kù)表結(jié)構(gòu)配置
ddl-auto: update
jackson:
date-format: yyyy-MM-dd HH:mm:ss
logging:
level:
com.along: debug
jpa相關(guān)的配置跟簡(jiǎn)單,這里特別要說(shuō)明spring.jpa.hibernate.ddl-auto
這個(gè)配置葫男,該配置有四個(gè)可選值抱冷,下面是詳細(xì)說(shuō)明:
- create:每次運(yùn)行該程序,沒(méi)有表格會(huì)新建表格梢褐,表內(nèi)有數(shù)據(jù)會(huì)清空
- create-drop:每次程序結(jié)束的時(shí)候會(huì)清空表
- update:每次運(yùn)行程序旺遮,沒(méi)有表格會(huì)新建表格,表內(nèi)有數(shù)據(jù)不會(huì)清空盈咳,只會(huì)更新
- validate:運(yùn)行程序會(huì)校驗(yàn)數(shù)據(jù)與數(shù)據(jù)庫(kù)的字段類(lèi)型是否相同耿眉,不同會(huì)報(bào)錯(cuò)
線上環(huán)境我們validate,開(kāi)發(fā)環(huán)境一般用update
定義實(shí)體類(lèi)
為了后面演示多表關(guān)系操作鱼响,這里設(shè)計(jì)了三張表鸣剪,分別是用戶(hù)表(User)、權(quán)限表(Role)和文章表(Article),用戶(hù)與權(quán)限是多對(duì)多關(guān)系筐骇,用戶(hù)與文章是一對(duì)多關(guān)系债鸡。
為了代碼的簡(jiǎn)潔,我們先創(chuàng)建一個(gè)基類(lèi)BaseData铛纬,在這個(gè)類(lèi)里寫(xiě)每個(gè)表的共了有字段厌均,繼承Serializable、主鍵id告唆、創(chuàng)建時(shí)間棺弊、更新時(shí)間,后面三個(gè)表都繼承這個(gè)類(lèi)擒悬,內(nèi)容如下:
@MappedSuperclass
public abstract class BaseData implements Serializable {
private static final long serialVersionUID = -3013776712039356819L;
@Id
@GeneratedValue(generator = "uuid")
@GenericGenerator(name = "uuid", strategy = "uuid2")
private String id;
@Temporal(TemporalType.TIMESTAMP)
private Date createTime;
@Temporal(javax.persistence.TemporalType.TIMESTAMP)
private Date updateTime;
@PrePersist
void createdAt() {
this.createTime = this.updateTime = new Date();
}
@PreUpdate
void updatedAt() {
this.updateTime = new Date();
}
// getter setter...
}
下面定義用戶(hù)表(User.class)
package com.along.model.entity;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import javax.persistence.*;
import java.util.Date;
import java.util.List;
import java.util.Set;
/**
* @Description: 用戶(hù)實(shí)體類(lèi)
* @Author along
* @Date 2019/1/8 16:50
*/
@Entity
@Table(name = "user") //對(duì)應(yīng)數(shù)據(jù)庫(kù)中的表名
public class User extends BaseData {
private static final long serialVersionUID = -5103936306962248929L;
private String name;
private String password;
private Integer sex; // 1:男模她;0:女
private Integer status = 1; //-1:刪除;0 禁用 1啟用
private String email;
@Temporal(TemporalType.DATE)
@JsonFormat(pattern = "yyyy-MM-dd")
private Date birthday;
/**
* 一對(duì)多配置演示
* 級(jí)聯(lián)保存懂牧、更新侈净、刪除、刷新;延遲加載归苍。當(dāng)刪除用戶(hù)用狱,會(huì)級(jí)聯(lián)刪除該用戶(hù)的所有文章
* 擁有mappedBy注解的實(shí)體類(lèi)為關(guān)系被維護(hù)端
* mappedBy="user"中的user是Article中的user屬性
*/
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Article> articleList; // 文章
/**
* 多對(duì)多配置演示
*/
@ManyToMany(cascade = CascadeType.MERGE, fetch = FetchType.LAZY)
@JoinTable(
name = "user_role", // 定義中間表的名稱(chēng)
joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")}, // 定義中間表中關(guān)聯(lián)User表的外鍵名
inverseJoinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")} // 定義中間表中關(guān)聯(lián)role表的外鍵名
)
private Set<Role> roles; // 角色外鍵
// getter and setter...
配置說(shuō)明:這里采用實(shí)實(shí)體類(lèi)自動(dòng)生成數(shù)據(jù)庫(kù)表,字段名會(huì)和數(shù)據(jù)庫(kù)字列名一樣拼弃,這樣就可以省略@Column(name = "")
注解夏伊,默認(rèn)每個(gè)字段都是可以為空的,如果需要不能為空吻氧,就加@Column(nullable = false)
注解溺忧。同時(shí)上面代碼改做了多對(duì)多和一對(duì)多的配置,有詳細(xì)的注解說(shuō)明盯孙。
下面定義權(quán)限表(Role.class)
package com.along.model.entity;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import javax.persistence.*;
import java.util.Set;
/**
* @Description: 角色實(shí)體類(lèi)
* @Author along
* @Date 2019/1/8 16:56
*/
@Entity
@Table(name = "role")
public class Role extends BaseData {
private static final long serialVersionUID = 5012235295240129244L;
private String roleName; // 角色名
private Integer roleType; // 1: 超級(jí)管理員 2: 系統(tǒng)管理員 3:一般用戶(hù)
private Integer state; // 0禁用 1 啟用
@ManyToMany(cascade = CascadeType.MERGE, fetch = FetchType.LAZY)
@JoinTable(
name = "user_role",
joinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")},
inverseJoinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")}
)
private Set<User> users; // 與用戶(hù)多對(duì)多
// getter and setter ...
}
下面是文章表(Article.class)
package com.along.model.entity;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import javax.persistence.*;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Size;
/**
* @Description: 文章實(shí)體類(lèi)
* @Author along
* @Date 2019/1/8 17:38
*/
@Entity
@Table(name = "article")
public class Article extends BaseData{
private static final long serialVersionUID = -4817984675096952797L;
@NotEmpty(message = "標(biāo)題不能為空")
@Column(nullable = false, length = 50)
private String title;
@Lob // 大對(duì)象鲁森,映射 MySQL 的 Long Text 類(lèi)型
@Basic(fetch = FetchType.LAZY) // 懶加載
@Column(nullable = false) // 映射為字段,值不能為空
private String content; // 文章全文內(nèi)容
/**
* 多對(duì)一配置演示:
* 可選屬性optional=false,表示sysUser不能為空
* 配置了級(jí)聯(lián)更新(合并)和刷新振惰,刪除文章歌溉,不影響用戶(hù)
*/
@ManyToOne(cascade = {CascadeType.MERGE, CascadeType.REFRESH}, optional = false)
@JoinColumn(name = "user_id") // 設(shè)置在article表中的關(guān)聯(lián)字段(外鍵)名
private User user; // 所屬用戶(hù)
// getter and setter ...
}
寫(xiě)到這里我們就可以啟動(dòng)項(xiàng)目了,運(yùn)行啟動(dòng)類(lèi)就能在數(shù)據(jù)庫(kù)中生成和實(shí)體類(lèi)對(duì)應(yīng)的表骑晶。
DAO層編寫(xiě)痛垛,使用JpaRepository接口
我們創(chuàng)建Dao接口繼承JpaRepository接口,JpaRepository需要泛型接口參數(shù)桶蛔,第一個(gè)參數(shù)是實(shí)體匙头,第二則是主鍵的類(lèi)型,也可以是Serializable仔雷。下面只給出UserDao的代碼蹂析,剩下兩個(gè)類(lèi)似
/**
* @Description: 用戶(hù)表dao
* @Author along
* @Date 2019/1/9 14:07
*/
public interface UserDao extends JpaRepository<User, String> {
}
繼承了JpaRepository后會(huì)自動(dòng)被spring注冊(cè)成為bean舔示,這樣用戶(hù)表的dao層就編寫(xiě)好了,可以進(jìn)行基本的crud操作电抚。
JpaRepository為我們做了什么惕稻?來(lái)看下JpaRepository的源碼
@NoRepositoryBean
public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
List<T> findAll(); // 查詢(xún)所有
List<T> findAll(Sort sort); // 查詢(xún)所有,帶排序
List<T> findAllById(Iterable<ID> ids); // 根據(jù)id列表查詢(xún)
<S extends T> List<S> saveAll(Iterable<S> entities); // 批量保存
void flush(); // 立即寫(xiě)入數(shù)據(jù)庫(kù),正常情況下在事務(wù)提交的時(shí)候喻频,JPA會(huì)自動(dòng)執(zhí)行flush()一次性保存所有數(shù)據(jù)缩宜。
<S extends T> S saveAndFlush(S entity); // 插入數(shù)據(jù)并且立即將更改寫(xiě)入數(shù)據(jù)庫(kù)
void deleteInBatch(Iterable<T> entities); // 批量刪除
void deleteAllInBatch(); // 刪除批量調(diào)用中的所有實(shí)體(清空表)
T getOne(ID id); // 根據(jù)id得到一個(gè)對(duì)象
@Override
<S extends T> List<S> findAll(Example<S> example); // 實(shí)例查詢(xún)
@Override
<S extends T> List<S> findAll(Example<S> example, Sort sort); // 實(shí)例查詢(xún),排序
}
可以看到JpaRepository實(shí)現(xiàn)了基本的crud操作肘迎,JpaRepository同時(shí)繼承了PagingAndSortingRepository和QueryByExampleExecutor接口
PagingAndSortingRepository接口包含了全表查詢(xún)時(shí)的分頁(yè)查詢(xún)和排序甥温,源碼如下:
@NoRepositoryBean
public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> {
Iterable<T> findAll(Sort sort); // 查詢(xún)多有,帶排序
Page<T> findAll(Pageable pageable); // 分頁(yè)查詢(xún)多有
}
QueryByExampleExecutor接口是用來(lái)做復(fù)雜查詢(xún)的妓布,十分好用姻蚓,會(huì)在下文詳細(xì)介紹,下面是該接口的源碼:
public interface QueryByExampleExecutor<T> {
<S extends T> Optional<S> findOne(Example<S> example); // 根據(jù)實(shí)例查詢(xún)一個(gè)實(shí)體
<S extends T> Iterable<S> findAll(Example<S> example); // 查詢(xún)所有符合給定實(shí)例的實(shí)體
<S extends T> Iterable<S> findAll(Example<S> example, Sort sort); // 查詢(xún)所有符合給定實(shí)例的實(shí)體匣沼,帶排序
<S extends T> Page<S> findAll(Example<S> example, Pageable pageable); // 分頁(yè)查詢(xún)所有符合給定實(shí)例的實(shí)體
<S extends T> long count(Example<S> example); // 得到符合給定實(shí)例的數(shù)量
<S extends T> boolean exists(Example<S> example); // 判斷是否存在
}
現(xiàn)在我們對(duì)繼承了JpaRepository的UserDao可以做到多少數(shù)據(jù)庫(kù)操作已經(jīng)有了大概的認(rèn)識(shí)狰挡。
二、JPA的使用
1. 基本CRUD操作
下面我們編寫(xiě)UserService來(lái)演示基本的crud操作释涛,代碼如下:
@Service(value = "userService")
@Transactional
public class UserService {
private UserDao userDao;
@Autowired
public UserServiceImpl(UserDao userDao) {
this.userDao = userDao;
}
/**
* 保存
*/
public User save(User user) {
return userDao.save(user);
}
/**
* 批量添加
*/
public List<User> saveAll(List<User> list) {
return userDao.saveAll(list);
}
/**
* 分頁(yè)查詢(xún)所有加叁,帶排序功能
*/
public Page<User> findAll() {
//分頁(yè)+排序查詢(xún)演示:
//Pageable pageable = new PageRequest(page, size);//2.0版本后,該方法以過(guò)時(shí)
Sort sort = new Sort(Sort.Direction.DESC, "updateTime","createTime");
Pageable pageable = PageRequest.of(page, size, sort);
Page<User> users = userService.findAll(pageable);
return userDao.findAll(pageable);
}
/**
* 更新
*/
public Boolean update(User user) {
Optional<User> u = userDao.findById(user.getId());
if (u.isPresent()) {
User oldUser = u.get();
oldUser.setName(user.getName());
oldUser.setRoles(user.getRoles());
oldUser.setBirthday(user.getBirthday());
oldUser.setEmail(user.getEmail());
oldUser.setUpdateTime(new Date());
userDao.save(oldUser);
return Boolean.TRUE;
}
return Boolean.FALSE;
}
/**
* 刪除
*/
@Override
public void delete(String id) {
userDao.deleteById(id);
}
}
上面代碼舉例了簡(jiǎn)單的幾個(gè)crud操作,這里要對(duì)分頁(yè)查詢(xún)和更新操作做特別的說(shuō)明:
分頁(yè)查詢(xún):
分頁(yè)查詢(xún)的關(guān)鍵在于創(chuàng)建Pageable對(duì)象唇撬,一般通過(guò)實(shí)現(xiàn)類(lèi)PageRequest創(chuàng)建它匕,早先的版本我們同伙new的方式創(chuàng)建Pageable對(duì)象,如下
Pageable pageable = new PageRequest(page, size)
但是在2.0版本后該方法已經(jīng)過(guò)時(shí)窖认,我們轉(zhuǎn)而使用PageRequest的of
方法創(chuàng)建Pageable實(shí)例豫柬,下面是源碼片段:
/**
* 不帶排序
*/
public static PageRequest of(int page, int size) {
return of(page, size, Sort.unsorted());
}
/**
* 帶排序
*/
public static PageRequest of(int page, int size, Sort sort) {
return new PageRequest(page, size, sort);
}
/**
* 帶排序信息
*/
public static PageRequest of(int page, int size, Direction direction, String... properties) {
return of(page, size, Sort.by(direction, properties));
}
下面是幾個(gè)模擬場(chǎng)景舉例:
- 第1頁(yè)每頁(yè)顯示20條
Pageable pageable = PageRequest.of(0, 20);
- 第1頁(yè)顯示20條,倒序排序扑浸,按創(chuàng)建時(shí)間字段排序烧给,如果創(chuàng)建時(shí)間相同,按更新時(shí)間排序
方式一:構(gòu)建Sort對(duì)象方式創(chuàng)建
Sort sort = new Sort(Sort.Direction.DESC, "updateTime","createTime");
Pageable pageable = PageRequest.of(page, size, sort);
方式二:直接傳入排序信息
Pageable pageable =
PageRequest.of(page, size, Sort.Direction.DESC, "updateTime","createTime");
更新:
JpaRepository并沒(méi)有提供專(zhuān)門(mén)的update方法喝噪,而是將更新操作放在save中完成了础嫡,下面是save方法的源碼實(shí)現(xiàn):
@Transactional
public <S extends T> S save(S entity) {
if (entityInformation.isNew(entity)) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
}
}
我們看到調(diào)用save方法傳入一個(gè)實(shí)例,首先會(huì)通過(guò)entityInformation.isNew(entity)
來(lái)判斷該實(shí)體是否是一個(gè)新的對(duì)象酝惧,具體的是先判斷有無(wú)id榴鼎,如果有就通過(guò)id在數(shù)據(jù)庫(kù)中查找是否存在對(duì)應(yīng)的數(shù)據(jù),如果存在就是更新操作系奉,會(huì)調(diào)用EntityManager
的merge()
方法執(zhí)行更新檬贰,如果不存在就說(shuō)明是插入操作,會(huì)調(diào)用EntityManager
的persist()
方法執(zhí)行插入缺亮。
EntityManager管理器
通過(guò)源碼我們可以看到save方法實(shí)質(zhì)上是調(diào)用的EntityManager的方法完成的數(shù)據(jù)庫(kù)操作翁涤,所以這里有必要介紹下EntityManager接口桥言,在此之前得了解jpa中實(shí)體對(duì)象擁有的四種狀態(tài):
- 瞬時(shí)狀態(tài)(new/transient):沒(méi)有主鍵,不與持久化上下文關(guān)聯(lián)葵礼,即 new 出的對(duì)象(但不能指定id的值号阿,若指定則是游離態(tài)而非瞬時(shí)態(tài))
- 托管狀態(tài)(persistent):使用EntityManager進(jìn)行find或者persist操作返回的對(duì)象即處于托管狀態(tài),此時(shí)該對(duì)象已經(jīng)處于持久化上下文中(被EntityManager監(jiān)控)鸳粉,任何對(duì)該實(shí)體的修改都會(huì)在提交事務(wù)時(shí)同步到數(shù)據(jù)庫(kù)中扔涧。
- 游離狀態(tài)(detached):有主鍵剔桨,但是沒(méi)有跟持久化上下文關(guān)聯(lián)的實(shí)體對(duì)象耗美。
- 刪除狀態(tài) (deleted):當(dāng)調(diào)用EntityManger對(duì)實(shí)體進(jìn)行remove后,該實(shí)體對(duì)象就處于刪除狀態(tài)祷愉。其本質(zhì)也就是一個(gè)瞬時(shí)狀態(tài)的對(duì)象艰山。
下面的圖清晰的表示了各個(gè)狀態(tài)間的轉(zhuǎn)化關(guān)系:
下面介紹下EntityManager接口的幾個(gè)常用方法:
- persist():將臨時(shí)狀態(tài)(無(wú)主鍵)的對(duì)象轉(zhuǎn)化為托管狀態(tài)湖雹。由于涉及數(shù)據(jù)庫(kù)增刪改,執(zhí)行該語(yǔ)句前需啟用事務(wù)
entityManager.persist(modelObject);
- merge():將游離狀態(tài)(有主鍵)的對(duì)象轉(zhuǎn)化為托管托管狀態(tài)曙搬,不同于persist()摔吏,merger()對(duì)于操作的對(duì)象,如果對(duì)象存在于數(shù)據(jù)庫(kù)則對(duì)對(duì)象進(jìn)行修改纵装,如果對(duì)象在數(shù)據(jù)庫(kù)中不存在征讲,則將該對(duì)象作為一條新記錄插入數(shù)據(jù)庫(kù)。
entityManager.merge(modelObject);
- find()與getReference():從數(shù)據(jù)庫(kù)中查找對(duì)象橡娄。不同點(diǎn):當(dāng)對(duì)象不存在時(shí)诗箍,find()會(huì)返回null,getReference()則會(huì)拋出javax.persistence.EntityNotFoundException異常瀑踢。
// 參數(shù)一:實(shí)體類(lèi)的class扳还,參數(shù)二:實(shí)體主鍵值
entityManager.find(Class<T> ModelObject.class , int key);
- remove():將托管狀態(tài)的對(duì)象轉(zhuǎn)化為刪除狀態(tài)。由于涉及數(shù)據(jù)庫(kù)增刪改橱夭,執(zhí)行該語(yǔ)句前需啟用事務(wù)
entityManager.remove(entityManager.getReference(ModelObject.class, key));
- refresh(Object obj):重新從數(shù)據(jù)庫(kù)中讀取數(shù)據(jù)氨距。可以保證當(dāng)前的實(shí)例與數(shù)據(jù)庫(kù)中的實(shí)例的內(nèi)容一致棘劣。該方法用來(lái)操作托管狀態(tài)的對(duì)象俏让。
- contains(Object obj):判斷對(duì)象在持久化上下文(不是數(shù)據(jù)庫(kù))中是否存在,返回true/false茬暇。
-
flush():立即將對(duì)托管狀態(tài)對(duì)象所做的修改(包括刪除)寫(xiě)入數(shù)據(jù)庫(kù)首昔。
從上面內(nèi)容我們發(fā)現(xiàn)通過(guò)EntityManager對(duì)實(shí)體對(duì)象所做的操作實(shí)質(zhì)是讓對(duì)象在不同的狀態(tài)間轉(zhuǎn)換,而這些修改是在執(zhí)行flush()后才會(huì)真正的寫(xiě)入數(shù)據(jù)庫(kù)糙俗。正常情況下不需要手動(dòng)執(zhí)行flash()勒奇,在事務(wù)提交的時(shí)候,JPA會(huì)自動(dòng)執(zhí)行flush()一次性保存所有數(shù)據(jù)巧骚。
如果要立即保存修改赊颠,可以手動(dòng)執(zhí)行flush()格二。
同時(shí)我們可以通過(guò)setFlushModel()
方法修改EntityManager的刷新模式。默認(rèn)為AUTO
竣蹦,這種模式下顶猜,會(huì)在執(zhí)行查詢(xún)(指使用JPQL語(yǔ)句查詢(xún)前,不包括find()和getReference()查詢(xún))前或事務(wù)提交時(shí)自動(dòng)執(zhí)行flush()痘括。通過(guò)entityManager.setFlushMode(FlushModeType.COMMIT)
設(shè)置為COMMIT
模式长窄,該模式下只有在事務(wù)提交時(shí)才會(huì)執(zhí)行flush()。 - clear():把實(shí)體管理器中所有的實(shí)體對(duì)象(托管狀態(tài))變成游離狀態(tài)纲菌,clear()之后挠日,對(duì)實(shí)體類(lèi)所做的修改也會(huì)丟失。
現(xiàn)在我們?cè)倩氐礁路椒ǔ酆螅瑸榱朔奖悴榭此磷剩覀冋〕錾厦鎸?xiě)好的更新方法實(shí)現(xiàn)
public Boolean update(User user) {
Optional<User> u = userDao.findById(user.getId());
if (u.isPresent()) {
User oldUser = u.get();
oldUser.setName(user.getName());
oldUser.setRoles(user.getRoles());
oldUser.setBirthday(user.getBirthday());
oldUser.setEmail(user.getEmail());
oldUser.setUpdateTime(new Date());
userDao.save(oldUser);
return Boolean.TRUE;
}
return Boolean.FALSE;
}
如果你已經(jīng)理解了上文中的JPA的四種狀態(tài)矗愧,那你應(yīng)該就能看出這段代碼存在的問(wèn)題灶芝,oldUser是從數(shù)據(jù)庫(kù)中查出來(lái)的,是托管狀態(tài)對(duì)象唉韭,受EntityManager管理夜涕,我們后面對(duì)該對(duì)象所做的修改會(huì)在事務(wù)提交時(shí)自動(dòng)調(diào)用flush()
將修改寫(xiě)入數(shù)據(jù)庫(kù)完成更新,所以并不需要再調(diào)用save()方法執(zhí)行更新属愤,這樣顯得多此一舉女器。當(dāng)然還要注意這樣實(shí)現(xiàn)更新需要在方法上加@Transactional
啟動(dòng)事務(wù)。
下面是修改后的代碼實(shí)現(xiàn):
@Transactional
public Boolean update(User user) {
Optional<User> u = userDao.findById(user.getId());
if (u.isPresent()) {
User oldUser = u.get();
oldUser.setName(user.getName());
oldUser.setRoles(user.getRoles());
oldUser.setBirthday(user.getBirthday());
oldUser.setEmail(user.getEmail());
oldUser.setUpdateTime(new Date());
return Boolean.TRUE;
}
return Boolean.FALSE;
}
2. Example查詢(xún)
上文中說(shuō)到JpaRepository繼承了QueryByExampleExecutor接口住诸,利用該接口可以實(shí)現(xiàn)相對(duì)復(fù)雜的實(shí)例查詢(xún)驾胆,下面再來(lái)看看QueryByExampleExecutor接口的源碼:
public interface QueryByExampleExecutor<T> {
<S extends T> Optional<S> findOne(Example<S> example); // 根據(jù)實(shí)例查詢(xún)一個(gè)實(shí)體
<S extends T> Iterable<S> findAll(Example<S> example); // 查詢(xún)所有符合給定實(shí)例的實(shí)體
<S extends T> Iterable<S> findAll(Example<S> example, Sort sort); // 查詢(xún)所有符合給定實(shí)例的實(shí)體,帶排序
<S extends T> Page<S> findAll(Example<S> example, Pageable pageable); // 分頁(yè)查詢(xún)所有符合給定實(shí)例的實(shí)體
<S extends T> long count(Example<S> example); // 得到符合給定實(shí)例的數(shù)量
<S extends T> boolean exists(Example<S> example); // 判斷是否存在
}
可以看到所有api都需要傳入Example對(duì)象贱呐,下面是Example api的組成:
- Probe:實(shí)體對(duì)象丧诺,比如我們要查詢(xún)User表,那User對(duì)象就是可以作為Probe奄薇。
- ExampleMatcher:匹配器驳阎,用來(lái)詳細(xì)描述實(shí)體對(duì)象中的內(nèi)容的查詢(xún)方式,如規(guī)定某個(gè)屬性為模糊查詢(xún)馁蒂。
- Example:實(shí)例呵晚,代表的是完整的查詢(xún)條件,由Probe和ExampleMatcher共同創(chuàng)建沫屡。
寫(xiě)一個(gè)簡(jiǎn)單的測(cè)試方法來(lái)感受下實(shí)例查詢(xún)
模擬需求:查詢(xún)姓名為along和性別為男的用戶(hù)
@Autowired
private UserDao userDao;
@Test
public void test() {
// 創(chuàng)建查詢(xún)條件數(shù)據(jù)對(duì)象
User user = new User();
user.setName("along");
user.setSex(1);
// 創(chuàng)建實(shí)例
Example<User> example = Example.of(user);
// 查詢(xún)
List<User> users = userDao.findAll(example);
//輸出結(jié)果
System.out.println("數(shù)量:" + users.size());
for (User u : users) {
System.out.println(u);
}
}
先創(chuàng)建實(shí)體對(duì)象user饵隙,因?yàn)槲覀兪歉鶕?jù)姓名和性別查詢(xún),所以為user對(duì)象的姓名和性別屬性復(fù)制內(nèi)容沮脖,然后同通過(guò)Example.of()
方法創(chuàng)建Example實(shí)例最后執(zhí)行查詢(xún)金矛。這里我們調(diào)用Example.of()
只傳入了一個(gè)實(shí)體對(duì)象user劫瞳,這樣jpa會(huì)在處理時(shí)默認(rèn)傳入一個(gè)默認(rèn)的ExampleMatcher
。
下面是默認(rèn)ExampleMatcher
規(guī)定的查詢(xún)方式:
- 忽略空值绷柒,只將實(shí)體對(duì)象中中不為空的字段作為查詢(xún)條件
- 所有查詢(xún)條件都采用精確匹配
- 查詢(xún)條件大小寫(xiě)敏感
運(yùn)行測(cè)試志于,控制臺(tái)輸出如下內(nèi)容:
Hibernate: select
user0_.id as id1_2_,
user0_.create_time as create_t2_2_,
user0_.update_time as update_t3_2_,
user0_.birthday as birthday4_2_,
user0_.email as email5_2_,
user0_.name as name6_2_,
user0_.password as password7_2_,
user0_.sex as sex8_2_,
user0_.status as status9_2_
from
user user0_
where
user0_.sex=1 and user0_.name=? and user0_.status=1
數(shù)量:4
along
along
along
along
成功查詢(xún)出了4條數(shù)據(jù),我們接下來(lái)觀察打印出來(lái)的sql废睦,查詢(xún)條件正是name伺绽、sex和status,而且都是精確查詢(xún)嗜湃,這時(shí)候發(fā)現(xiàn)多了一個(gè)查詢(xún)條件status奈应,因?yàn)閷?shí)例查詢(xún)默認(rèn)會(huì)將實(shí)例中不為空的內(nèi)容作為查詢(xún)條件,而我們?cè)诙xUser實(shí)體類(lèi)的時(shí)候?yàn)閟tatus屬性設(shè)了默認(rèn)值1购披。
如果我們需要查詢(xún)條件只有name和sex杖挣,這時(shí)候就要定義ExampleMatcher
來(lái)忽略status屬性。我們修改測(cè)試代碼如下
@Autowired
private UserDao userDao;
@Test
public void test() {
// 創(chuàng)建查詢(xún)條件數(shù)據(jù)對(duì)象
User user = new User();
user.setName("along");
user.setSex(1);
// 創(chuàng)建匹配器刚陡,即規(guī)定如何使用查詢(xún)條件
ExampleMatcher matcher = ExampleMatcher.matching() // 構(gòu)建對(duì)象
.withIgnorePaths("status"); // 忽略status屬性
// 創(chuàng)建實(shí)例
Example<User> example = Example.of(user, matcher);
// 查詢(xún)
List<User> users = userDao.findAll(example);
//輸出結(jié)果
System.out.println("數(shù)量:" + users.size());
for (User u : users) {
System.out.println(u.getName());
}
}
運(yùn)行測(cè)試惩妇,控制帶輸出sql如下,可以看到查詢(xún)條件已經(jīng)沒(méi)有status了
select
user0_.id as id1_2_,
user0_.create_time as create_t2_2_,
user0_.update_time as update_t3_2_,
user0_.birthday as birthday4_2_,
user0_.email as email5_2_,
user0_.name as name6_2_,
user0_.password as password7_2_,
user0_.sex as sex8_2_,
user0_.status as status9_2_
from
user user0_
where
user0_.sex=1 and user0_.name=?
理解ExampleMatcher
我們可以通過(guò)下面代碼創(chuàng)建一個(gè)默認(rèn)的ExampleMatcher對(duì)象
ExampleMatcher matcher = ExampleMatcher.matching();
下面是ExampleMatcher
的實(shí)現(xiàn)類(lèi)的部分源碼筐乳,展示了 ExampleMatcher
的六個(gè)配置項(xiàng)
class TypedExampleMatcher implements ExampleMatcher {
private final NullHandler nullHandler; // Null值處理方式
private final StringMatcher defaultStringMatcher; // 默認(rèn)字符串匹配方式
private final PropertySpecifiers propertySpecifiers; // 各個(gè)屬性特定查詢(xún)方式
private final Set<String> ignoredPaths; // 忽略屬性列表
private final boolean defaultIgnoreCase; // 默認(rèn)大小寫(xiě)忽略方式
private final MatchMode mode; // 默認(rèn)為all,目前沒(méi)看出有什么特別的作用
/**
* 空參構(gòu)造歌殃,為上面的屬性設(shè)置默認(rèn)值
* null處理方式為忽略
* 默認(rèn)字符串匹配方式為default(精確匹配)
* 各屬性特定查詢(xún)方式默認(rèn)為空
* 忽略屬性列表默認(rèn)為空列表
* 默認(rèn)大小寫(xiě)忽略方式為false,不忽略
* MatchMode默認(rèn)為all
*/
TypedExampleMatcher() {
this(NullHandler.IGNORE, StringMatcher.DEFAULT, new PropertySpecifiers(), Collections.emptySet(), false, MatchMode.ALL);
}
......
}
從源碼我們可以看出ExampleMatcher的默認(rèn)配置如下:
- nullHandler:IGNORE。Null值處理方式:忽略
- defaultStringMatcher:DEFAULT蝙云。默認(rèn)字符串匹配方式:默認(rèn)(相等)
- defaultIgnoreCase:false氓皱。默認(rèn)大小寫(xiě)忽略方式:不忽略
- propertySpecifiers:空。各屬性特定查詢(xún)方式勃刨,空波材。
- ignoredPaths:空列表。忽略屬性列表身隐,空列表廷区。
但是只創(chuàng)建一個(gè)默認(rèn)的ExampleMatcher沒(méi)有什么意義,上文說(shuō)到就算你創(chuàng)建Example實(shí)例時(shí)不傳ExampleMatcher對(duì)象抡医,jpa也會(huì)自動(dòng)加上默認(rèn)的ExampleMatcher躲因。
下面對(duì)每個(gè)配置項(xiàng)進(jìn)行詳細(xì)講解
-
nullHandler:null值處理方式,枚舉類(lèi)型忌傻,兩個(gè)可選值:
INCLUDE(包括)
和IGNORE(忽略)
大脉,默認(rèn)為IGNORE
。
通過(guò)下面代碼改變默認(rèn)的null值處理方式:
// 默認(rèn)就是忽略水孩,所以再設(shè)置為忽略就沒(méi)有意義了镰矿,下面設(shè)置為不忽略
ExampleMatcher matcher = ExampleMatcher.matching()
.withNullHandler(ExampleMatcher.NullHandler.INCLUDE) // 方式一
.withIncludeNullValues(); // 方式二
-
defaultStringMatcher:默認(rèn)字符串匹配方式,枚舉類(lèi)型俘种,六個(gè)可選值:
DEFAULT(默認(rèn)秤标,效果同EXACT)
绝淡,EXACT(精確匹配,即 = )
苍姜,STARTING(開(kāi)始匹配牢酵,即 ?% )
,ENDING(結(jié)束匹配衙猪,即 %? )
馍乙,CONTAINING(包含,模糊匹配垫释,即 %?% )
丝格,REGEX(正則表達(dá)式匹配)
下面是改變默認(rèn)字符串匹配方式的代碼示例:
ExampleMatcher matcher = ExampleMatcher.matching()
//下面只用配置一個(gè)
.withStringMatcher(ExampleMatcher.StringMatcher.STARTING) // 開(kāi)始匹配 %?
.withStringMatcher(ExampleMatcher.StringMatcher.ENDING) // 結(jié)束匹配 ?%
.withStringMatcher(ExampleMatcher.StringMatcher.CONTAINING) // 包含,模糊匹配 %?%
.withStringMatcher(ExampleMatcher.StringMatcher.REGEX); // 正則匹配
-
propertySpecifiers:各屬性特定查詢(xún)方式棵譬,描述了各個(gè)屬性單獨(dú)定義的查詢(xún)方式显蝌,每個(gè)查詢(xún)方式中包含4個(gè)元素:屬性名、字符串匹配方式订咸、大小寫(xiě)忽略方式曼尊、屬性轉(zhuǎn)換器。如果屬性未單獨(dú)定義查詢(xún)方式算谈,或單獨(dú)查詢(xún)方式中涩禀,某個(gè)元素未定義(如:字符串匹配方式),則采用 ExampleMatcher 中定義的默認(rèn)值然眼,即上面介紹的 defaultStringMatcher 和 defaultIgnoreCase 的值。
一個(gè)屬性的特定查詢(xún)方式葵腹,包含了3個(gè)信息:字符串匹配方式高每、大小寫(xiě)忽略方式、屬性轉(zhuǎn)換器践宴,存儲(chǔ)在 propertySpecifiers 中鲸匿,操作時(shí)用 GenericPropertyMatcher 類(lèi)來(lái)傳遞配置信息。
下面是該屬性配置的代碼示例:
ExampleMatcher matcher = ExampleMatcher.matching()
//方式一:?jiǎn)为?dú)設(shè)置name字段為模糊查詢(xún)方式
.withMatcher("name", ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.CONTAINING))
//方式二:設(shè)置name字段為模糊查詢(xún)阻肩,忽略大小寫(xiě)
.withMatcher("name", ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.CONTAINING, true))
//方式三(推薦):鏈?zhǔn)皆O(shè)置
.withMatcher("name", ExampleMatcher.GenericPropertyMatchers.contains().ignoreCase());
-
ignoredPaths:忽略屬性列表带欢,忽略的屬性不參與查詢(xún)過(guò)濾。
下面是添加忽略屬性的代碼示例:
ExampleMatcher matcher = ExampleMatcher.matching()
.withIgnorePaths("status", "sex");
-
defaultIgnoreCase:默認(rèn)大小寫(xiě)忽略方式烤惊,布爾型乔煞,當(dāng)值為false時(shí),即不忽略柒室,大小不相等渡贾。該配置對(duì)所有字符串屬性過(guò)濾有效,除非該屬性在 propertySpecifiers 中單獨(dú)定義自己的忽略大小寫(xiě)方式雄右。
下面是改變默認(rèn)大小寫(xiě)忽略方式的代碼示例:
ExampleMatcher matcher = ExampleMatcher.matching()
// 下面兩個(gè)等價(jià)
.withIgnoreCase()
.withIgnoreCase(true)
// 單獨(dú)為name屬性設(shè)置忽略大小寫(xiě)空骚,可設(shè)置多個(gè)值
.withIgnoreCase("name")
實(shí)例查詢(xún)的限制
- 不支持過(guò)濾條件分組纺讲。即不支持過(guò)濾條件用 or(或) 來(lái)連接,所有的過(guò)濾查件囤屹,都是簡(jiǎn)單一層的用 and(并且) 連接熬甚。
- 靈活匹配只支持字符串類(lèi)型,其他類(lèi)型只支持精確匹配
參考文章:https://blog.csdn.net/zhao_tuo/article/details/78604324
3. 接口規(guī)范方法名查詢(xún)(最讓我驚喜的查詢(xún)方式)
根據(jù)可讀性極強(qiáng)的方法名就能創(chuàng)建查詢(xún)肋坚,初次接觸時(shí)會(huì)讓你覺(jué)得很不可思議
說(shuō)明:按照Spring data 定義的規(guī)則则涯,查詢(xún)方法以find|read|get開(kāi)頭,涉及條件查詢(xún)時(shí)冲簿,條件的屬性用條件關(guān)鍵字連接粟判,整個(gè)查詢(xún)方法名按駝峰式命名。
我們來(lái)看下面代碼
/**
* @Description: 接口規(guī)范方法名查詢(xún)示例
* @Author along
* @Date 2019/1/9 14:07
*/
public interface UserDao extends JpaRepository<User, String>, JpaSpecificationExecutor<User> {
/**
* and條件查詢(xún)
* 對(duì)應(yīng)sql:select u from User u where u.name = ?1 and u.email = ?2
* 參數(shù)名大寫(xiě)峦剔,條件名首字母大寫(xiě)档礁,并且接口名中參數(shù)出現(xiàn)的順序必須和參數(shù)列表中的參數(shù)順序一致
*/
User findByNameAndEmail(String name, String email);
/**
* or條件查詢(xún)
* 對(duì)應(yīng)sql:select u from User u where u.name = ?1 or u.password = ?2
*/
List<User> findByNameOrPassword(String name, String password);
/**
* between查詢(xún)
* 對(duì)應(yīng)sql:select u from User u where u.id between ?1 and ?2
*/
List<User> findByCreateTimeBetween(Date startTime, Date endTime);
/**
* less查詢(xún)
* 對(duì)應(yīng)sql:select u from User u where u.id < ?1
*/
List<User> findByCreateTimeLessThan(Date time);
/**
* greater查詢(xún)
* 對(duì)應(yīng)sql:select u from User u where u.id > ?1
*/
List<User> findByCreateTimeGreaterThan(Date time);
/**
* is null查詢(xún)
* 對(duì)應(yīng)sql:select u from User u where u.name is null
*/
List<User> findByNameIsNull();
/**
* Is Not Null查詢(xún)
* 對(duì)應(yīng)sql:select u from User u where u.name is not null
*/
List<User> findByNameIsNotNull();
/**
* like模糊查詢(xún)
* 這里的模糊查詢(xún)并不會(huì)自動(dòng)在name兩邊加"%"秽澳,需要手動(dòng)對(duì)參數(shù)加"%"
* 對(duì)應(yīng)sql:select u from User u where u.name like ?1
*/
List<User> findByNameLike(String name);
/**
* Not Like模糊查詢(xún)
* 對(duì)應(yīng)sql:select u from User u where u.name not like ?1
*/
List<User> findByNameNotLike(String name);
/**
* 倒序排序查詢(xún)
* 對(duì)應(yīng)sql:select u from User u where u.password = ?1 order by u.id desc
*/
List<User> findByPasswordOrderByCreateTimeDesc(String password);
/**
* <>查詢(xún)
* 對(duì)應(yīng)sql:select u from User u where u.name <> ?1
*/
List<User> findByNameNot(String name);
/**
* in 查詢(xún)矫俺,方法的參數(shù)可以是 Collection 類(lèi)型,也可以是數(shù)組或者不定長(zhǎng)參數(shù)
* 對(duì)應(yīng)sql:select u from User u where u.id in ?1
*/
List<User> findByIdIn(List<String> ids);
/**
* not in 查詢(xún)涂臣,方法的參數(shù)可以是 Collection 類(lèi)型惨险,也可以是數(shù)組或者不定長(zhǎng)參數(shù)
* 對(duì)應(yīng)sql:select u from User u where u.id not in ?1
*/
List<User> findByIdNotIn(List<String> ids);
/**
* 分頁(yè)查詢(xún)羹幸,方法的參數(shù)可以是 Collection 類(lèi)型,也可以是數(shù)組或者不定長(zhǎng)參數(shù)
* 對(duì)應(yīng)sql:select u from User u where u.id not in ?1 limit ?
*/
Page<User> findByIdNotIn(List<String> ids, Pageable pageable);
}
dao編寫(xiě)完畢辫愉,只需在service中調(diào)用即可栅受,是不是感覺(jué)比上面的Example要優(yōu)雅很多!而且可以應(yīng)付大多數(shù)的查詢(xún)需求恭朗。如果你用idea屏镊,idea還會(huì)在你編寫(xiě)方法是提供智能提示,簡(jiǎn)直不要太貼心痰腮。
這里簡(jiǎn)單地介紹下原理:jpa框架在進(jìn)行方法名解析時(shí)而芥,如果遇到以 find、findBy膀值、read棍丐、readBy、get沧踏、getBy為前綴的方法名歌逢,會(huì)忽略前綴,對(duì)剩下部分進(jìn)行解析悦冀,再后面會(huì)識(shí)別如And趋翻、Or這樣的關(guān)鍵字,來(lái)判斷以何種方式連接查詢(xún)關(guān)鍵字。而且如果方法的最后一個(gè)參數(shù)是 Sort 或 Pageable 類(lèi)型踏烙,就會(huì)提取相關(guān)的信息师骗,以便按規(guī)則進(jìn)行排序或者分頁(yè)查詢(xún)。
4. @Query創(chuàng)建查詢(xún)
比起接口規(guī)范方法名查詢(xún)讨惩,@Query顯得稍微麻煩一點(diǎn)辟癌,需要自己寫(xiě)JPQL查詢(xún)語(yǔ)句,但是卻更加強(qiáng)大荐捻,對(duì)方法名沒(méi)有要求黍少,可以準(zhǔn)確控制JPQL語(yǔ)句,而且不局限于查詢(xún)处面,還可以和@Modifying一起使用實(shí)現(xiàn)跟新操作厂置,你甚至可以使用@Query來(lái)指定本地查詢(xún),寫(xiě)真正的sql語(yǔ)句魂角,只要設(shè)置nativeQuery=true(但是個(gè)人建議不要用本地查詢(xún)昵济,這樣就失去了JPA的優(yōu)勢(shì),如果喜歡寫(xiě)sql野揪,為什么不直接用mybatis呢访忿?)
下面是示例代碼
/**
* @Description: @Query示例
* @Author along
* @Date 2019/1/9 14:07
*/
public interface UserDao extends JpaRepository<User, String> {
/**
* 根據(jù)name查詢(xún),支持命名參數(shù)
*/
@Query("select u from User u where u.name = :mame")
List<User> findUserByName(@Param("name")String name);
/**
* 根據(jù)sex查詢(xún),縮影參數(shù)
*/
@Query("select u from User u where u.sex = ?1")
List<User> findUsersBySex(Integer sex);
/**
* 模糊查詢(xún)
*/
@Query("select u from User u where name like concat('%',?1,'%') ")
List<User> findByName(String name);
/**
* 本地查詢(xún)
*/
@Query(value = "select * from user where name like CONCAT('%',?1,'%')", nativeQuery = true)
List<User> findByNameLocal(String name);
/**
* 跟新密碼斯稳,需要加@Modifying
*/
@Modifying
@Query("update user u set u.password = ?1 where u.id = ?2")
Integer updatePasswordById(String password, String id);
}
注意模糊查詢(xún)的 JPQL 寫(xiě)法海铆,不要寫(xiě)成like '%?1%'
,這樣是查不出來(lái)東西的挣惰。
用命名參數(shù)需要在參數(shù)前面用@Param()
注解制定參數(shù)名卧斟,不然會(huì)查詢(xún)失敗。
跟新需要加上@Modifying
通熄,而且Modifying queries的返回值只能為void或者是int/Integer唆涝,調(diào)用更新方法前需要開(kāi)啟事務(wù),否則會(huì)跟新失敗唇辨。
5. Criteria查詢(xún)
上文介紹的查詢(xún)方法面簡(jiǎn)單的查詢(xún)需求已經(jīng)足夠了,但是如果給定的查詢(xún)條件是不固定的能耻,需要?jiǎng)討B(tài)的創(chuàng)建查詢(xún)語(yǔ)句赏枚,那上文的方法都就都不適用了,這里可以用Criteria查詢(xún)解決晓猛。
Criteria API查詢(xún)是通過(guò)面向?qū)ο蟮姆绞綐?gòu)建查詢(xún)饿幅,相比于傳統(tǒng)的基于字符串的JPQL查詢(xún),優(yōu)勢(shì)是類(lèi)型安全戒职,更加的面向?qū)ο蟆?br>
這里推薦一篇文章栗恩,對(duì) Criteria API 講解的十分詳細(xì):詳解JPA 2.0動(dòng)態(tài)查詢(xún)機(jī)制:Criteria API
下面我們先用 Criteria API 寫(xiě)一個(gè)service方法來(lái)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的查詢(xún)需求
@Autoware
private EntityManager entityManager;
public List<User> findUserByNameAndSex0(String name, Integer sex) {
// 1. 利用entityManager構(gòu)建出CriteriaQuery類(lèi)型的參數(shù)
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<User> query = builder.createQuery(User.class);
// 2. 獲取User的Root,也就是包裝對(duì)象
Root<User> root = query.from(User.class);
// 3. 構(gòu)建查詢(xún)條件洪燥,這里相當(dāng)于where user.id = id;
Predicate predicate = builder.and(
builder.like(root.get("name").as(String.class), "%" + name+ "%"),
builder.equal(root.get("sex").as(Integer.class), sex)
);
query.where(predicate); // 到這里一個(gè)完整的動(dòng)態(tài)查詢(xún)就構(gòu)建完成了
// 指定查詢(xún)結(jié)果集磕秤,相當(dāng)于“select id乳乌,name...”,如果不設(shè)置市咆,默認(rèn)查詢(xún)r(jià)oot中所有字段
query.select(root);
// 4. 執(zhí)行查詢(xún),獲取查詢(xún)結(jié)果
TypedQuery<User> typeQuery = entityManager.createQuery(query);
List<User> resultList = typeQuery.getResultList();
return resultList;
}
上面代碼完整的實(shí)現(xiàn)了一次通過(guò)Criteria API查詢(xún)的過(guò)程汉操,下面編寫(xiě)的測(cè)試方法測(cè)試下查詢(xún)結(jié)果
@Test
public void queryTest() {
List<User> userList = userService.findUserByNameAndSex0("along", 1);
System.out.println(userList.size());
for (User user : userList) {
System.out.println(user.getName());
}
}
下面是運(yùn)行后控制臺(tái)打印出來(lái)的查詢(xún)sql
select
user0_.id as id1_2_,
user0_.create_time as create_t2_2_,
user0_.update_time as update_t3_2_,
user0_.birthday as birthday4_2_,
user0_.email as email5_2_,
user0_.name as name6_2_,
user0_.password as password7_2_,
user0_.sex as sex8_2_,
user0_.status as status9_2_
from
user user0_
where
(user0_.name like ?) and user0_.sex=1
sql中結(jié)果集并不包括articleList和roles,原因是我們?cè)賃ser實(shí)體中做表關(guān)聯(lián)配置時(shí)將這兩個(gè)字段定義為了懶加載(fetch = FetchType.LAZY)
。
盡管這達(dá)到了我們的目的蒙兰,但這未免太麻煩了磷瘤,創(chuàng)建一個(gè)查詢(xún)需要這么多步驟,完全可以利用JPQL語(yǔ)句實(shí)現(xiàn)相同的需求搜变,如下:
@Query("select u from User u where u.name like concat('%',:name,'%') and u.sex=:sex")
List<User> findUserByNameAndSex(@Param("name")String name,@Param("sex")Integer sex);
幸運(yùn)的是JPA為我們考慮到了這一點(diǎn)采缚,我們可以發(fā)現(xiàn)上面完成一次查詢(xún)一共有四個(gè)步驟,其中除了第3步構(gòu)建查詢(xún)條件挠他,其他的都是固定的代碼扳抽,JPA提供了JpaSpecificationExecutor
接口幫我們實(shí)現(xiàn)了步驟1、2绩社、4摔蓝,我們?cè)诰帉?xiě)代碼時(shí)只需要實(shí)現(xiàn)第3步即可。
接下來(lái)我們重新實(shí)現(xiàn)下上面的需求愉耙。
首先是要讓UserDao繼承JpaSpecificationExecutor接口贮尉,如下:
public interface UserDao extends JpaRepository<User, String>, JpaSpecificationExecutor<User> {
}
JpaSpecificationExecutor接口并不在JpaRepository接口體系中,需要額外繼承朴沿,而且Spring data JPA不會(huì)自動(dòng)掃描識(shí)別猜谚,所以要和任意一個(gè)Repository的子接口一起使用。
接下來(lái)我們?cè)赨serService中編寫(xiě)實(shí)現(xiàn)方法赌渣,如下
@Autowired
private UserDao userDao;
public List<User> findUserByNameAndSex(String name, Integer sex) {
return userDao.findAll(new Specification<User>() {
@Override
public Predicate toPredicate(Root<User> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
// 構(gòu)建查詢(xún)條件并返回
return criteriaBuilder.and(
criteriaBuilder.equal(root.get("name"), name),
criteriaBuilder.equal(root.get("sex"), sex)
);
}
});
}
我們只需要重寫(xiě)Sepcfication接口的toPredicate方法魏铅,而toPredicate方法自動(dòng)攜帶了我們需要的三個(gè)參數(shù),這都是JPA提前為我們創(chuàng)建好的坚芜,不需要我們手動(dòng)創(chuàng)建览芳,我們只需要在方法中構(gòu)建一個(gè)Predicate即可,也就是我們自開(kāi)始的實(shí)現(xiàn)的第3步的代碼鸿竖。是不是簡(jiǎn)潔了很多沧竟?
下面深入源碼看看JPA是怎么幫我們實(shí)現(xiàn)的,首先要看JpaSpecificationExecutor接口的源碼
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);
}
可以看出改接口都是圍繞著Specification構(gòu)建的缚忧,每個(gè)方法的功能也一目了然悟泵,接下來(lái)看看Specification的源碼
@SuppressWarnings("deprecation")
public interface Specification<T> extends Serializable {
long serialVersionUID = 1L;
/**
* 否定給定的{@link Specification}。
*/
static <T> Specification<T> not(Specification<T> spec) {
return Specifications.negated(spec);
}
/**
* 簡(jiǎn)單的靜態(tài)工廠方法闪水,在{@link Specification}周?chē)砑右恍┱Z(yǔ)法糖糕非。
*/
static <T> Specification<T> where(Specification<T> spec) {
return Specifications.where(spec);
}
/**
* 將給定的{@link Specification}與當(dāng)前的一個(gè)進(jìn)行對(duì)比。
*
* @param other can be {@literal null}.
* @return The conjunction of the specifications
* @since 2.0
*/
default Specification<T> and(Specification<T> other) {
return Specifications.composed(this, other, AND);
}
/**
* 將給定的規(guī)范與當(dāng)前規(guī)范進(jìn)行或運(yùn)算。
*/
default Specification<T> or(Specification<T> other) {
return Specifications.composed(this, other, OR);
}
/**
* 為給定的{@link Predicate}形式的被引用實(shí)體的查詢(xún)創(chuàng)建WHERE子句
*/
@Nullable
Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder);
}
我們只需要把關(guān)注點(diǎn)放到最下面的toPredicate方法上朽肥,這也是唯一需要我們手動(dòng)實(shí)現(xiàn)的方法禁筏。
我們?cè)賮?lái)看findAll()方法的具體實(shí)現(xiàn):
public List<T> findAll(@Nullable Specification<T> spec) {
return getQuery(spec, Sort.unsorted()).getResultList();
}
因?yàn)槲覀儧](méi)有傳入排序?qū)ο骃ort,這里生成了一個(gè)默認(rèn)的空的排序?qū)ο缶铣省T龠M(jìn)入getQuery方法
protected TypedQuery<T> getQuery(@Nullable Specification<T> spec, Sort sort) {
return getQuery(spec, getDomainClass(), sort);
}
這里通過(guò)getDomainClass()方法得到了當(dāng)前要查詢(xún)的User實(shí)體的字節(jié)碼類(lèi)型融师,下面再進(jìn)入getQuery()方法
protected <S extends T> TypedQuery<S> getQuery(
@Nullable Specification<S> spec, Class<S> domainClass, Sort sort) {
// 1. 構(gòu)建出CriteriaQuery類(lèi)型的參數(shù)
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<S> query = builder.createQuery(domainClass);
// 2. 獲取User的Root,也就是包裝對(duì)象
Root<S> root = applySpecificationToCriteria(spec, domainClass, query);
// 指定查詢(xún)結(jié)果集
query.select(root);
if (sort.isSorted()) {
query.orderBy(toOrders(sort, root, builder));
}
// 4. 執(zhí)行查詢(xún),獲取查詢(xún)結(jié)果
return applyRepositoryMethodMetadata(em.createQuery(query));
}
注意我在源碼中加的1蚁吝、2旱爆、4注釋?zhuān)龠M(jìn)入applySpecificationToCriteria方法
private <S, U extends T> Root<U> applySpecificationToCriteria(
@Nullable Specification<U> spec, Class<U> domainClass, CriteriaQuery<S> query) {
Assert.notNull(domainClass, "Domain class must not be null!");
Assert.notNull(query, "CriteriaQuery must not be null!");
// 2. 獲取User的Root,也就是包裝對(duì)象
Root<U> root = query.from(domainClass);
if (spec == null) {
return root;
}
CriteriaBuilder builder = em.getCriteriaBuilder();
// 重點(diǎn)>阶隆;陈住!山林!在這里執(zhí)行我們實(shí)現(xiàn)的toPredicate()方法
Predicate predicate = spec.toPredicate(root, query, builder);
if (predicate != null) {
// 到這里一個(gè)完整的動(dòng)態(tài)查詢(xún)就構(gòu)建完成了
query.where(predicate);
}
return root;
}
看到這你會(huì)發(fā)現(xiàn)我們最開(kāi)始的原始實(shí)現(xiàn)中的1房待、2、4步JPA都做了實(shí)現(xiàn)驼抹,并且源碼中spec參數(shù)調(diào)用了toPredicate方法桑孩,也就是我們自己在service中做的實(shí)現(xiàn),而且在調(diào)動(dòng)toPredicate方法之前Root框冀、CriteriaQuery和CriteriaBuilder都已經(jīng)創(chuàng)建好了流椒。
到這里我們已經(jīng)知道JpaSpecificationExecutor底層是如何實(shí)現(xiàn)的封裝,下面我們來(lái)探究下具體的使用明也。
動(dòng)態(tài)語(yǔ)句查詢(xún)
下面我們利用JpaSpecificationExecutor實(shí)現(xiàn)一個(gè)動(dòng)態(tài)語(yǔ)句查詢(xún)宣虾,并實(shí)現(xiàn)分頁(yè)排序功能
public Page<User> findUser(User user, int page, int size) {
Sort sort = new Sort(Sort.Direction.DESC, "updateTime","createTime");
Pageable pageable = PageRequest.of(page, size, sort);
return userDao.findAll(new Specification<User>() {
@Override
public Predicate toPredicate(Root<User> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
List<Predicate> predicateList = new ArrayList<>();
if (!StringUtils.isEmpty(user.getId())) {
predicateList.add(criteriaBuilder.equal(root.get("id"), user.getId()));
}
if (!StringUtils.isEmpty(user.getName())) {
predicateList.add(criteriaBuilder.like(root.get("name"), user.getName()));
}
if (null != user.getCreateTime()) {
predicateList.add(criteriaBuilder.greaterThan(root.get("createTime"), user.getCreateTime()));
}
if (null != user.getUpdateTime()) {
predicateList.add(criteriaBuilder.lessThanOrEqualTo(root.get("updateTime"), user.getUpdateTime()));
}
Predicate[] predicateArr = new Predicate[predicateList.size()];
return criteriaBuilder.and(predicateList.toArray(predicateArr));
}
}, pageable);
}
效果是不是跟mybatis的xml實(shí)現(xiàn)的動(dòng)態(tài)查詢(xún)有些類(lèi)似?
多表關(guān)聯(lián)查詢(xún)
User表關(guān)聯(lián)了兩張表温数,分別是Article表和Role表绣硝,和Article表是一對(duì)多關(guān)系,和Role表是多對(duì)多關(guān)系撑刺。下面是關(guān)聯(lián)查詢(xún)代碼示例
/**
* 表關(guān)聯(lián)查詢(xún)
* @param articleId
* @param roleId
* @return
*/
public List<User> findUserByArticleAndRole(String articleId, String roleId) {
return userDao.findAll(new Specification<User>() {
@Override
public Predicate toPredicate(Root<User> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
// 方式1
ListJoin<User, Article> articleJoin = root.join(root.getModel().getList("articleList", Article.class), JoinType.LEFT);
SetJoin<User, Role> roleJoin = root.join(root.getModel().getSet("roles", Role.class), JoinType.LEFT);
// 方式2
//Join<User, Article> articleJoin = root.join("articleList", JoinType.LEFT);
//Join<User, Role> roleJoin = root.join("roles", JoinType.LEFT);
Predicate predicate = criteriaBuilder.or(
criteriaBuilder.equal(articleJoin.get("id"), articleId),
criteriaBuilder.equal(roleJoin.get("id"), roleId)
);
return predicate;
}
});
}
下面是運(yùn)行結(jié)果生成的sql
select
user0_.id as id1_2_,
user0_.create_time as create_t2_2_,
user0_.update_time as update_t3_2_,
user0_.birthday as birthday4_2_,
user0_.email as email5_2_,
user0_.name as name6_2_,
user0_.password as password7_2_,
user0_.sex as sex8_2_,
user0_.status as status9_2_
from
user user0_
left outer join
article articlelis1_ // 關(guān)聯(lián)article
on
user0_.id=articlelis1_.user_id
left outer join
user_role roles2_ // 關(guān)聯(lián)中間表user_role
on
user0_.id=roles2_.user_id
left outer join
role role3_ // 關(guān)聯(lián)role
on
roles2_.role_id=role3_.id
where
articlelis1_.id=? or role3_.id=?
Criteria查詢(xún)到這里也就介紹完了鹉胖,能力有限,介紹的比較粗略够傍。
如果你熟練地掌握了 Criteria API 的使用次员,你幾乎可以使用該方式來(lái)應(yīng)對(duì)所有的查詢(xún)需求,但是面對(duì)簡(jiǎn)單的查詢(xún)需求王带,還是建議使用更加簡(jiǎn)潔的規(guī)范方法名查詢(xún),只有在其他方式不方便解決時(shí)再考慮用 Criteria API 來(lái)解決市殷。具體實(shí)際開(kāi)發(fā)中如何選擇愕撰,還要根據(jù)實(shí)際開(kāi)發(fā)情況而定。
三、性能問(wèn)題解決
上文為了單純地介紹jpa的用法搞挣,隱藏了很多問(wèn)題带迟,在這一節(jié)中集中介紹并處理。
循環(huán)引用問(wèn)題
再來(lái)回顧下User囱桨、Role仓犬、Article的關(guān)系,User與Role是多對(duì)多關(guān)系舍肠,User與Article是一對(duì)多關(guān)系搀继。因?yàn)槲恼螺^長(zhǎng),再往上翻回顧比較麻煩翠语,這里再放上這三個(gè)類(lèi)的源碼叽躯,如下
/**
* 用戶(hù)實(shí)體類(lèi)
*/
@Entity
@Table(name = "user") //對(duì)應(yīng)數(shù)據(jù)庫(kù)中的表名
public class User extends BaseData {
private static final long serialVersionUID = -5103936306962248929L;
private String name;
private String password;
private Integer sex; // 1:男;0:女
private Integer status = 1; //-1:刪除肌括;0 禁用 1啟用
private String email;
@Temporal(TemporalType.DATE)
@JsonFormat(pattern = "yyyy-MM-dd")
private Date birthday;
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Article> articleList; // 文章
@ManyToMany(cascade = CascadeType.MERGE, fetch = FetchType.LAZY)
@JoinTable(
name = "user_role", // 定義中間表的名稱(chēng)
joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")}, // 定義中間表中關(guān)聯(lián)User表的外鍵名
inverseJoinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")} // 定義中間表中關(guān)聯(lián)role表的外鍵名
)
private Set<Role> roles; // 角色外鍵
// getter and setter...
}
/**
* 角色實(shí)體類(lèi)
*/
@Entity
@Table(name = "role")
public class Role extends BaseData {
private static final long serialVersionUID = 5012235295240129244L;
private String roleName; // 角色名
private Integer roleType; // 1: 超級(jí)管理員 2: 系統(tǒng)管理員 3:一般用戶(hù)
private Integer state; // 0禁用 1 啟用
@ManyToMany(cascade = CascadeType.MERGE, fetch = FetchType.LAZY)
@JoinTable(
name = "user_role",
joinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")},
inverseJoinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")}
)
private Set<User> users; // 與用戶(hù)多對(duì)多
// getter and setter ...
}
/**
* 文章實(shí)體類(lèi)
*/
@Entity
@Table(name = "article")
public class Article extends BaseData{
private static final long serialVersionUID = -4817984675096952797L;
@NotEmpty(message = "標(biāo)題不能為空")
@Column(nullable = false, length = 50)
private String title;
@Lob // 大對(duì)象点骑,映射 MySQL 的 Long Text 類(lèi)型
@Basic(fetch = FetchType.LAZY) // 懶加載
@Column(nullable = false) // 映射為字段,值不能為空
private String content; // 文章全文內(nèi)容
@ManyToOne(cascade = {CascadeType.MERGE, CascadeType.REFRESH}, optional = false)
@JoinColumn(name = "user_id") // 設(shè)置在article表中的關(guān)聯(lián)字段(外鍵)名
private User user; // 所屬用戶(hù)
// getter and setter ...
}
現(xiàn)在我們編寫(xiě)查詢(xún)代碼來(lái)看看這樣存在什么問(wèn)題谍夭,在userDao中編寫(xiě)查詢(xún)方法
List<User> findByName(String name);
下面是UserService中的實(shí)現(xiàn)方法
public List<User> findByName(String name) {
List<User> users = userDao.findByName(name);
return users;
}
在編寫(xiě)UserController
@GetMapping("/findByName/{name}")
public ResultMapper findByName(@PathVariable String name) {
List<User> users = userService.findByName(name);
return ResultMapperUtil.success(users);
}
最后在瀏覽器輸入http://localhost:8080/user/findByName/along 發(fā)起查詢(xún)請(qǐng)求黑滴。
雖然查詢(xún)出了結(jié)果,但是結(jié)果集非常巨大紧索,并且控制臺(tái)會(huì)報(bào)兩個(gè)異常袁辈,分別是IllegalStateException
和StackOverflowError
,第一個(gè)是無(wú)效狀態(tài)異常齐板,重點(diǎn)是第二個(gè)異常吵瞻,棧溢出,數(shù)據(jù)庫(kù)中名字為along的用戶(hù)只有幾條甘磨,結(jié)果卻發(fā)生了棧溢出橡羞,為什么會(huì)這樣?
我們?cè)倏纯创蛴〉膕ql日志
Hibernate:
select
user0_.id as id1_2_,
user0_.create_time as create_t2_2_,
user0_.update_time as update_t3_2_,
user0_.birthday as birthday4_2_,
user0_.email as email5_2_,
user0_.name as name6_2_,
user0_.password as password7_2_,
user0_.sex as sex8_2_,
user0_.status as status9_2_
from
user user0_ where user0_.name=?
Hibernate:
select
articlelis0_.user_id as user_id6_0_0_,
articlelis0_.id as id1_0_0_,
articlelis0_.id as id1_0_1_,
articlelis0_.create_time as create_t2_0_1_,
articlelis0_.update_time as update_t3_0_1_,
articlelis0_.content as content4_0_1_,
articlelis0_.title as title5_0_1_,
articlelis0_.user_id as user_id6_0_1_
from
article articlelis0_
where
articlelis0_.user_id=?
共打印了兩條sql济舆,第一條sql正是我們所希望的卿泽,根據(jù)name查詢(xún)user,由于user關(guān)聯(lián)了article滋觉,jpa于是再發(fā)起sql签夭,去查詢(xún)article,又由于article中又關(guān)聯(lián)著user椎侠,于是又會(huì)再查一遍user第租,如此循環(huán)反復(fù),根本停不下來(lái)我纪,很快就棧溢出了慎宾。
按這樣說(shuō)丐吓,你可能還會(huì)有疑問(wèn)蚜退,這樣的話不是應(yīng)該打印3條sql嗎讲岁?應(yīng)該該有一條sql去查詢(xún)r(jià)ole嚎货,為什么只有兩條呢啥繁?原因是第二條sql查詢(xún)article時(shí)就已經(jīng)進(jìn)入了死循環(huán)并報(bào)了異常淑翼,所以也就不會(huì)再發(fā)起第三條sql去查詢(xún)r(jià)ole了要尔。
解決方案一:使用@JsonIgnore注解
@JsonIgnore是Jackson提供的注解橄仆,在實(shí)體類(lèi)的屬性上加上該注解貌笨,這樣在json序列化時(shí)會(huì)將java bean中的對(duì)應(yīng)的屬性忽略掉咳促,同樣jpa在查詢(xún)時(shí)也會(huì)忽略對(duì)應(yīng)的屬性稚新,如此便可以解決循環(huán)查詢(xún)的問(wèn)題。
@JsonIgnore的使用十分靈活等缀,你可以只在User中使用枷莉,這樣在查詢(xún)User時(shí)結(jié)果集中就不會(huì)包含添加了改注解的屬性,你也可以只在Role和Article實(shí)體中與User關(guān)聯(lián)的屬性上加@JsonIgnore尺迂,這樣在查詢(xún)User時(shí)還是會(huì)關(guān)聯(lián)查詢(xún)Role和Article笤妙,但是不會(huì)發(fā)生循環(huán)查詢(xún)。
這里只在User中加上@JsonIgnore注解噪裕,改造后的User類(lèi)代碼如下
/**
* 用戶(hù)實(shí)體類(lèi)
*/
@Entity
@Table(name = "user") //對(duì)應(yīng)數(shù)據(jù)庫(kù)中的表名
public class User extends BaseData {
private static final long serialVersionUID = -5103936306962248929L;
private String name;
private String password;
private Integer sex; // 1:男蹲盘;0:女
private Integer status = 1; //-1:刪除;0 禁用 1啟用
private String email;
@Temporal(TemporalType.DATE)
@JsonFormat(pattern = "yyyy-MM-dd")
private Date birthday;
@JsonIgnore
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Article> articleList; // 文章
@JsonIgnore
@ManyToMany(cascade = CascadeType.MERGE, fetch = FetchType.LAZY)
@JoinTable(
name = "user_role", // 定義中間表的名稱(chēng)
joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")}, // 定義中間表中關(guān)聯(lián)User表的外鍵名
inverseJoinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")} // 定義中間表中關(guān)聯(lián)role表的外鍵名
)
private Set<Role> roles; // 角色外鍵
// getter and setter...
}
再次查詢(xún)膳音,成功查詢(xún)出結(jié)果召衔,結(jié)果中不包含article和role,控制臺(tái)也只打印出了一條查詢(xún)user的sql祭陷。
解決方案二:使用@JsonIgnoreProperties注解
同樣是Jackson提供的注解苍凛,@JsonIgnoreProperties和@JsonIgnore用法差不多,@JsonIgnoreProperties可以更加細(xì)粒度的選擇忽略關(guān)聯(lián)實(shí)體中的屬性兵志。也就是說(shuō)如果你需要關(guān)聯(lián)查詢(xún)醇蝴,但是又想控制關(guān)聯(lián)查詢(xún)的類(lèi)的屬性,那么可以使用該注解想罕。
改造后的User類(lèi)如下
/**
* 用戶(hù)實(shí)體類(lèi)
*/
@Entity
@Table(name = "user") //對(duì)應(yīng)數(shù)據(jù)庫(kù)中的表名
public class User extends BaseData {
private static final long serialVersionUID = -5103936306962248929L;
private String name;
private String password;
private Integer sex; // 1:男悠栓;0:女
private Integer status = 1; //-1:刪除;0 禁用 1啟用
private String email;
@Temporal(TemporalType.DATE)
@JsonFormat(pattern = "yyyy-MM-dd")
private Date birthday;
@JsonIgnoreProperties(value = {"user", "content"}) //解決循環(huán)引用問(wèn)題按价,content內(nèi)容大惭适,不加載
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Article> articleList; // 文章
@JsonIgnoreProperties(value = {"users"}) //解決循環(huán)引用問(wèn)題
@ManyToMany(cascade = CascadeType.MERGE, fetch = FetchType.LAZY)
@JoinTable(
name = "user_role", // 定義中間表的名稱(chēng)
joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")}, // 定義中間表中關(guān)聯(lián)User表的外鍵名
inverseJoinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")} // 定義中間表中關(guān)聯(lián)role表的外鍵名
)
private Set<Role> roles; // 角色外鍵
// getter and setter...
}
上面我在關(guān)聯(lián)屬性aritcleList和roles上加上@JsonIgnoreProperties注解忽略了會(huì)引發(fā)循環(huán)引用的屬性,article中的content是大文本楼镐,也將其忽略癞志。
再次發(fā)起查詢(xún),沒(méi)有發(fā)生循環(huán)查詢(xún)框产,控制臺(tái)打印的sql如下:
Hibernate: select user0_.id as id1_2_, user0_.create_time as create_t2_2_, user0_.update_time as update_t3_2_, user0_.birthday as birthday4_2_, user0_.email as email5_2_, user0_.name as name6_2_, user0_.password as password7_2_, user0_.sex as sex8_2_, user0_.status as status9_2_ from user user0_ where user0_.name=?
Hibernate: select articlelis0_.user_id as user_id6_0_0_, articlelis0_.id as id1_0_0_, articlelis0_.id as id1_0_1_, articlelis0_.create_time as create_t2_0_1_, articlelis0_.update_time as update_t3_0_1_, articlelis0_.content as content4_0_1_, articlelis0_.title as title5_0_1_, articlelis0_.user_id as user_id6_0_1_ from article articlelis0_ where articlelis0_.user_id=?
Hibernate: select roles0_.user_id as user_id2_3_0_, roles0_.role_id as role_id1_3_0_, role1_.id as id1_1_1_, role1_.create_time as create_t2_1_1_, role1_.update_time as update_t3_1_1_, role1_.role_name as role_nam4_1_1_, role1_.role_type as role_typ5_1_1_, role1_.state as state6_1_1_ from user_role roles0_ inner join role role1_ on roles0_.role_id=role1_.id where roles0_.user_id=?
共打印了3條sql今阳,分別查詢(xún)對(duì)應(yīng)的三個(gè)表师溅。
解決方案三:返回自定義包裝類(lèi)
上面我們的查詢(xún)接口都是直接用實(shí)體類(lèi)來(lái)接收查詢(xún)結(jié)果并返回,但是更加規(guī)范的是創(chuàng)建一個(gè)Vo類(lèi)來(lái)接收查詢(xún)結(jié)果盾舌,Vo中的屬性名跟實(shí)體類(lèi)相同,這樣我們可以實(shí)現(xiàn)不改變實(shí)體類(lèi)就能決定返回結(jié)果的內(nèi)容蘸鲸。
下面我們定義一個(gè)Vo
/**
* @Description: 返回前端的類(lèi)妖谴,vo可以控制返回的字段,也是解決循環(huán)引用的一種方案
* @Author along
* @Date 2019/1/10 13:29
*/
public interface UserVo {
String getId();
String getName();
String getPassword();
Integer getSex(); // 1:男酌摇;0:女
Integer getStatus(); //-1:刪除膝舅;0 禁用 1啟用
String getEmail();
Date getBirthday();
//@JsonIgnoreProperties(value = {"user", "content"}) //解決循環(huán)引用問(wèn)題
//List<Article> getArticleList(); // 文章列表
//@JsonIgnoreProperties(value = {"users"}) //解決循環(huán)引用問(wèn)題
//Set<Role> getRoles(); // 角色外鍵
}
注意該 UserVo 是一個(gè)接口,里面的屬性是實(shí)體類(lèi)中對(duì)應(yīng)屬性的 get 方法窑多。使用Vo可以靈活決定返回結(jié)果的字段仍稀。
下面我們改造userDao中的查詢(xún)方法,改用UserVo來(lái)接收
List<UserVo> findByName(String name);
下面是UserService中的實(shí)現(xiàn)方法埂息,同樣改用UserVo來(lái)接收
public List<UserVo> findByName(String name) {
List<UserVo> userVos = userDao.findByName(name);
return userVos;
}
再下面是UserController接口
@GetMapping("/findByName/{name}")
public ResultMapper findByName(@PathVariable String name) {
List<UserVo> userVos = userService.findByName(name);
return ResultMapperUtil.success(userVos);
}
調(diào)用查詢(xún)技潘,可以實(shí)現(xiàn)與方案一和方案二相同的效果,使用也更加靈活簡(jiǎn)單千康,個(gè)人最推薦該方案享幽。
N+1查詢(xún)問(wèn)題
上面雖然解決了循環(huán)引用的問(wèn)題,但隨后又出現(xiàn)一個(gè)更加頭疼的問(wèn)題拾弃,也是Jpa使用了表關(guān)聯(lián)屬性后出現(xiàn)的N+1查詢(xún)問(wèn)題值桩,具體這是個(gè)什么現(xiàn)象呢,項(xiàng)目中會(huì)有很多的實(shí)體類(lèi)豪椿,實(shí)體類(lèi)之間通常又會(huì)有一對(duì)多奔坟、多對(duì)多的關(guān)聯(lián),通常我們會(huì)將多的一方設(shè)置成懶加載搭盾,這樣我們?cè)诓樵?xún)一個(gè)實(shí)體時(shí)只會(huì)查詢(xún)出該實(shí)體的基本屬性(不包括被設(shè)置為懶加載的屬性)咳秉,然后當(dāng)我們需要關(guān)聯(lián)對(duì)象的某些屬性時(shí),ORM就會(huì)再次發(fā)出sql語(yǔ)句查詢(xún)關(guān)聯(lián)的屬性增蹭。這也就解釋了為什么上文中查詢(xún)一個(gè)user滴某,結(jié)果卻發(fā)出了三條sql語(yǔ)句。
數(shù)據(jù)小時(shí)不會(huì)有明顯的問(wèn)題滋迈,可當(dāng)查詢(xún)的數(shù)據(jù)量變大時(shí)霎奢,查詢(xún)發(fā)出的sql數(shù)量也會(huì)非常大,會(huì)引發(fā)嚴(yán)重的性能問(wèn)題饼灿。
如下面的例子幕侠,我們調(diào)用userDao.findAll()方法查詢(xún)所有用戶(hù)數(shù)據(jù),并且設(shè)置了分頁(yè)碍彭,查詢(xún)20條晤硕,結(jié)果控制臺(tái)打印的sql日志如下
Hibernate: select user0_.id as id1_2_, user0_.create_time as create_t2_2_, user0_.update_time as update_t3_2_, user0_.birthday as birthday4_2_, user0_.email as email5_2_, user0_.name as name6_2_, user0_.password as password7_2_, user0_.sex as sex8_2_, user0_.status as status9_2_ from user user0_ order by user0_.update_time desc, user0_.create_time desc limit ?
Hibernate: select articlelis0_.user_id as user_id6_0_0_, articlelis0_.id as id1_0_0_, articlelis0_.id as id1_0_1_, articlelis0_.create_time as create_t2_0_1_, articlelis0_.update_time as update_t3_0_1_, articlelis0_.content as content4_0_1_, articlelis0_.title as title5_0_1_, articlelis0_.user_id as user_id6_0_1_ from article articlelis0_ where articlelis0_.user_id=?
Hibernate: select roles0_.user_id as user_id2_3_0_, roles0_.role_id as role_id1_3_0_, role1_.id as id1_1_1_, role1_.create_time as create_t2_1_1_, role1_.update_time as update_t3_1_1_, role1_.role_name as role_nam4_1_1_, role1_.role_type as role_typ5_1_1_, role1_.state as state6_1_1_ from user_role roles0_ inner join role role1_ on roles0_.role_id=role1_.id where roles0_.user_id=?
Hibernate: select articlelis0_.user_id as user_id6_0_0_, articlelis0_.id as id1_0_0_, articlelis0_.id as id1_0_1_, articlelis0_.create_time as create_t2_0_1_, articlelis0_.update_time as update_t3_0_1_, articlelis0_.content as content4_0_1_, articlelis0_.title as title5_0_1_, articlelis0_.user_id as user_id6_0_1_ from article articlelis0_ where articlelis0_.user_id=?
Hibernate: select roles0_.user_id as user_id2_3_0_, roles0_.role_id as role_id1_3_0_, role1_.id as id1_1_1_, role1_.create_time as create_t2_1_1_, role1_.update_time as update_t3_1_1_, role1_.role_name as role_nam4_1_1_, role1_.role_type as role_typ5_1_1_, role1_.state as state6_1_1_ from user_role roles0_ inner join role role1_ on roles0_.role_id=role1_.id where roles0_.user_id=?
Hibernate: select articlelis0_.user_id as user_id6_0_0_, articlelis0_.id as id1_0_0_, articlelis0_.id as id1_0_1_, articlelis0_.create_time as create_t2_0_1_, articlelis0_.update_time as update_t3_0_1_, articlelis0_.content as content4_0_1_, articlelis0_.title as title5_0_1_, articlelis0_.user_id as user_id6_0_1_ from article articlelis0_ where articlelis0_.user_id=?
Hibernate: select roles0_.user_id as user_id2_3_0_, roles0_.role_id as role_id1_3_0_, role1_.id as id1_1_1_, role1_.create_time as create_t2_1_1_, role1_.update_time as update_t3_1_1_, role1_.role_name as role_nam4_1_1_, role1_.role_type as role_typ5_1_1_, role1_.state as state6_1_1_ from user_role roles0_ inner join role role1_ on roles0_.role_id=role1_.id where roles0_.user_id=?
Hibernate: select articlelis0_.user_id as user_id6_0_0_, articlelis0_.id as id1_0_0_, articlelis0_.id as id1_0_1_, articlelis0_.create_time as create_t2_0_1_, articlelis0_.update_time as update_t3_0_1_, articlelis0_.content as content4_0_1_, articlelis0_.title as title5_0_1_, articlelis0_.user_id as user_id6_0_1_ from article articlelis0_ where articlelis0_.user_id=?
Hibernate: select roles0_.user_id as user_id2_3_0_, roles0_.role_id as role_id1_3_0_, role1_.id as id1_1_1_, role1_.create_time as create_t2_1_1_, role1_.update_time as update_t3_1_1_, role1_.role_name as role_nam4_1_1_, role1_.role_type as role_typ5_1_1_, role1_.state as state6_1_1_ from user_role roles0_ inner join role role1_ on roles0_.role_id=role1_.id where roles0_.user_id=?
Hibernate: select articlelis0_.user_id as user_id6_0_0_, articlelis0_.id as id1_0_0_, articlelis0_.id as id1_0_1_, articlelis0_.create_time as create_t2_0_1_, articlelis0_.update_time as update_t3_0_1_, articlelis0_.content as content4_0_1_, articlelis0_.title as title5_0_1_, articlelis0_.user_id as user_id6_0_1_ from article articlelis0_ where articlelis0_.user_id=?
Hibernate: select roles0_.user_id as user_id2_3_0_, roles0_.role_id as role_id1_3_0_, role1_.id as id1_1_1_, role1_.create_time as create_t2_1_1_, role1_.update_time as update_t3_1_1_, role1_.role_name as role_nam4_1_1_, role1_.role_type as role_typ5_1_1_, role1_.state as state6_1_1_ from user_role roles0_ inner join role role1_ on roles0_.role_id=role1_.id where roles0_.user_id=?
Hibernate: select articlelis0_.user_id as user_id6_0_0_, articlelis0_.id as id1_0_0_, articlelis0_.id as id1_0_1_, articlelis0_.create_time as create_t2_0_1_, articlelis0_.update_time as update_t3_0_1_, articlelis0_.content as content4_0_1_, articlelis0_.title as title5_0_1_, articlelis0_.user_id as user_id6_0_1_ from article articlelis0_ where articlelis0_.user_id=?
Hibernate: select roles0_.user_id as user_id2_3_0_, roles0_.role_id as role_id1_3_0_, role1_.id as id1_1_1_, role1_.create_time as create_t2_1_1_, role1_.update_time as update_t3_1_1_, role1_.role_name as role_nam4_1_1_, role1_.role_type as role_typ5_1_1_, role1_.state as state6_1_1_ from user_role roles0_ inner join role role1_ on roles0_.role_id=role1_.id where roles0_.user_id=?
Hibernate: select articlelis0_.user_id as user_id6_0_0_, articlelis0_.id as id1_0_0_, articlelis0_.id as id1_0_1_, articlelis0_.create_time as create_t2_0_1_, articlelis0_.update_time as update_t3_0_1_, articlelis0_.content as content4_0_1_, articlelis0_.title as title5_0_1_, articlelis0_.user_id as user_id6_0_1_ from article articlelis0_ where articlelis0_.user_id=?
Hibernate: select roles0_.user_id as user_id2_3_0_, roles0_.role_id as role_id1_3_0_, role1_.id as id1_1_1_, role1_.create_time as create_t2_1_1_, role1_.update_time as update_t3_1_1_, role1_.role_name as role_nam4_1_1_, role1_.role_type as role_typ5_1_1_, role1_.state as state6_1_1_ from user_role roles0_ inner join role role1_ on roles0_.role_id=role1_.id where roles0_.user_id=?
雖然只用分頁(yè)控制了結(jié)果集悼潭,但是sql的量還是非常大的。
JPA2.1提供了新的特性來(lái)解決N+1查詢(xún)的問(wèn)題舞箍,就是實(shí)體圖(EntityGraph)舰褪,由于實(shí)體間的關(guān)系錯(cuò)綜復(fù)雜,如果實(shí)體間存在關(guān)系那么就用線將實(shí)體進(jìn)行連接疏橄,那么實(shí)體間將形成一個(gè)網(wǎng)狀的圖占拍,而實(shí)體圖技術(shù)就是對(duì)這個(gè)關(guān)系圖的操作,它允許開(kāi)發(fā)者指定某個(gè)實(shí)體及其發(fā)展出來(lái)的關(guān)系網(wǎng)中的某些節(jié)點(diǎn)捎迫,這些節(jié)點(diǎn)間形成一條路徑晃酒,當(dāng)調(diào)用查詢(xún)方法時(shí),查詢(xún)方法會(huì)按照實(shí)體圖中的路徑將路徑中的這些節(jié)點(diǎn)立即加載窄绒,而繞過(guò)延遲加載贝次,從而更高效的實(shí)現(xiàn)數(shù)據(jù)的檢索。
下面是實(shí)體圖的應(yīng)用彰导,我們?cè)赨ser表上使用@NamedEntityGraphs注解定義實(shí)體圖蛔翅,你可以定義多個(gè)實(shí)體圖,如下
/**
* 用戶(hù)實(shí)體類(lèi)
*/
@Entity
@Table(name = "user") //對(duì)應(yīng)數(shù)據(jù)庫(kù)中的表名
//EntityGraph(實(shí)體圖)使用演示螺戳,解決查詢(xún)N+1問(wèn)題
@NamedEntityGraphs({
@NamedEntityGraph(name = "user.all",
attributeNodes = { // attributeNodes 用來(lái)定義需要立即加載的屬性
@NamedAttributeNode("articleList"), // 無(wú)延伸
@NamedAttributeNode("roles"), // 無(wú)延伸
}
),
})
public class User extends BaseData {
...
}
@NamedEntityGraph代表著一個(gè)實(shí)體圖搁宾,通過(guò)name屬性設(shè)置實(shí)體圖的名字,通過(guò)attributeNodes屬性來(lái)定義需要立即加載的屬性倔幼,如果role還關(guān)聯(lián)了另外一張表盖腿,并且設(shè)置為了懶加載,那如果想要立即加載改表损同,就通過(guò)subgraphs屬性來(lái)進(jìn)行描述翩腐,下面是在Role實(shí)體中定義的實(shí)體圖
@Entity
@Table(name = "role")
// EntityGraph(實(shí)體圖)使用演示,解決查詢(xún)N+1問(wèn)題
@NamedEntityGraphs({
@NamedEntityGraph(name = "role.all",
attributeNodes = { // attributeNodes 用來(lái)指定要立即加載的節(jié)點(diǎn)膏燃,節(jié)點(diǎn)用 @NamedAttributeNode 定義
@NamedAttributeNode(value = "users", subgraph = "userWithArticleList"), // 要立即加載users屬性中的articleList元素
},
subgraphs = { // subgraphs 用來(lái)定義關(guān)聯(lián)對(duì)象的屬性,也就是對(duì)上面的 userWithArticleList 進(jìn)行描述
@NamedSubgraph(name = "userWithArticleList", attributeNodes = @NamedAttributeNode("articleList")), // 一層延伸
}
),
})
public class Role extends BaseData {
...
}
光是定義了實(shí)體圖還不夠茂卦,我們需要在查詢(xún)時(shí)指定使用哪一個(gè)實(shí)體圖進(jìn)行查詢(xún),如下组哩,重寫(xiě)findAll()方法等龙,使用@EntityGraph注解指定通過(guò)名為user.all的實(shí)體圖查詢(xún)
public interface UserDao extends JpaRepository<User, String>, JpaSpecificationExecutor<User> {
//重寫(xiě)findAll(Pageable pageable)方法,用實(shí)體圖查詢(xún)
@EntityGraph(value = "user.all", type = EntityGraph.EntityGraphType.FETCH)
@Override
Page<User> findAll(Pageable pageable);
}
執(zhí)行查詢(xún),打印出的sql日志如下伶贰,為了方便閱讀將其格式化輸出
Hibernate:
select
user0_.id as id1_2_0_,
articlelis1_.id as id1_0_1_,
role3_.id as id1_1_2_,
user0_.create_time as create_t2_2_0_,
user0_.update_time as update_t3_2_0_,
user0_.birthday as birthday4_2_0_,
user0_.email as email5_2_0_,
user0_.name as name6_2_0_,
user0_.password as password7_2_0_,
user0_.sex as sex8_2_0_,
user0_.status as status9_2_0_,
articlelis1_.create_time as create_t2_0_1_,
articlelis1_.update_time as update_t3_0_1_,
articlelis1_.content as content4_0_1_,
articlelis1_.title as title5_0_1_,
articlelis1_.user_id as user_id6_0_1_,
articlelis1_.user_id as user_id6_0_0__,
articlelis1_.id as id1_0_0__,
role3_.create_time as create_t2_1_2_,
role3_.update_time as update_t3_1_2_,
role3_.role_name as role_nam4_1_2_,
role3_.role_type as role_typ5_1_2_,
role3_.state as state6_1_2_,
roles2_.user_id as user_id2_3_1__,
roles2_.role_id as role_id1_3_1__
from user user0_
left outer join
article articlelis1_
on
user0_.id=articlelis1_.user_id
left outer join
user_role roles2_
on
user0_.id=roles2_.user_id
left outer join
role role3_
on
roles2_.role_id=role3_.id
order by
user0_.update_time desc, user0_.create_time desc
可以看到只打印了一條sql語(yǔ)句蛛砰。
本文相關(guān)源碼地址
https://github.com/alonglong/spring-boot-all/tree/master/spring-boot-jpa