前言
Spring Data JPA是Spring Data的一個子項目,通過提供基于JPA的Repository極大的減少了JPA作為數據訪問方案的代碼量,你僅僅需要編寫一個接口集成下SpringDataJPA內部定義的接口即可完成簡單的CRUD操作。
本文從構建項目到對JPA的詳細使用,爭取能夠盡量全的演示JPA的相關應用,大體內容如下:
- JPA環境搭建、配置
- 表關系配置演示:多對多、多對一、一對多
- 基本CRUD操作
- JPA實體對象的4種狀態詳解
- Example查詢
- 接口規范方法名查詢
- @Query注解使用
- Criteria查詢
- 性能問題解決(循環引用、N+1查詢)
一、構建項目
引入依賴
新建springboot項目,在pom文件中引入jpa的相關依賴,如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
另外我們在引入另外的web及數據庫連接相關的依賴:
<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數據庫連接池 -->
<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>
數據源及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相關配置
druid:
#監控統計攔截的filters
filters: stat
#配置初始化大小/最小/最大
initial-size: 1
min-idle: 1
max-active: 20
#獲取連接等待超時時間
max-wait: 60000
#間隔多久進行一次檢測,檢測需要關閉的空閑連接
time-between-eviction-runs-millis: 60000
#一個連接在池中最小生存的時間
min-evictable-idle-time-millis: 300000
validation-query: SELECT 'x'
test-while-idle: true
test-on-borrow: false
test-on-return: false
#打開PSCache,并指定每個連接上PSCache的大小。oracle設為true,mysql設為false。分庫分表較多推薦設置為false
pool-prepared-statements: false
max-pool-prepared-statement-per-connection-size: 20
jpa:
show-sql: true
# 指定生成表名的存儲引擎為InnoDBD
database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
hibernate:
# 自動創建|更新|驗證數據庫表結構配置
ddl-auto: update
jackson:
date-format: yyyy-MM-dd HH:mm:ss
logging:
level:
com.along: debug
jpa相關的配置跟簡單,這里特別要說明spring.jpa.hibernate.ddl-auto
這個配置,該配置有四個可選值,下面是詳細說明:
- create:每次運行該程序,沒有表格會新建表格,表內有數據會清空
- create-drop:每次程序結束的時候會清空表
- update:每次運行程序,沒有表格會新建表格,表內有數據不會清空,只會更新
- validate:運行程序會校驗數據與數據庫的字段類型是否相同,不同會報錯
線上環境我們validate,開發環境一般用update
定義實體類
為了后面演示多表關系操作,這里設計了三張表,分別是用戶表(User)、權限表(Role)和文章表(Article),用戶與權限是多對多關系,用戶與文章是一對多關系。
為了代碼的簡潔,我們先創建一個基類BaseData,在這個類里寫每個表的共了有字段,繼承Serializable、主鍵id、創建時間、更新時間,后面三個表都繼承這個類,內容如下:
@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...
}
下面定義用戶表(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: 用戶實體類
* @Author along
* @Date 2019/1/8 16:50
*/
@Entity
@Table(name = "user") //對應數據庫中的表名
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;
/**
* 一對多配置演示
* 級聯保存、更新、刪除、刷新;延遲加載。當刪除用戶,會級聯刪除該用戶的所有文章
* 擁有mappedBy注解的實體類為關系被維護端
* mappedBy="user"中的user是Article中的user屬性
*/
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Article> articleList; // 文章
/**
* 多對多配置演示
*/
@ManyToMany(cascade = CascadeType.MERGE, fetch = FetchType.LAZY)
@JoinTable(
name = "user_role", // 定義中間表的名稱
joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")}, // 定義中間表中關聯User表的外鍵名
inverseJoinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")} // 定義中間表中關聯role表的外鍵名
)
private Set<Role> roles; // 角色外鍵
// getter and setter...
配置說明:這里采用實實體類自動生成數據庫表,字段名會和數據庫字列名一樣,這樣就可以省略@Column(name = "")
注解,默認每個字段都是可以為空的,如果需要不能為空,就加@Column(nullable = false)
注解。同時上面代碼改做了多對多和一對多的配置,有詳細的注解說明。
下面定義權限表(Role.class)
package com.along.model.entity;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import javax.persistence.*;
import java.util.Set;
/**
* @Description: 角色實體類
* @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: 超級管理員 2: 系統管理員 3:一般用戶
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; // 與用戶多對多
// 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: 文章實體類
* @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 = "標題不能為空")
@Column(nullable = false, length = 50)
private String title;
@Lob // 大對象,映射 MySQL 的 Long Text 類型
@Basic(fetch = FetchType.LAZY) // 懶加載
@Column(nullable = false) // 映射為字段,值不能為空
private String content; // 文章全文內容
/**
* 多對一配置演示:
* 可選屬性optional=false,表示sysUser不能為空
* 配置了級聯更新(合并)和刷新,刪除文章,不影響用戶
*/
@ManyToOne(cascade = {CascadeType.MERGE, CascadeType.REFRESH}, optional = false)
@JoinColumn(name = "user_id") // 設置在article表中的關聯字段(外鍵)名
private User user; // 所屬用戶
// getter and setter ...
}
寫到這里我們就可以啟動項目了,運行啟動類就能在數據庫中生成和實體類對應的表。
DAO層編寫,使用JpaRepository接口
我們創建Dao接口繼承JpaRepository接口,JpaRepository需要泛型接口參數,第一個參數是實體,第二則是主鍵的類型,也可以是Serializable。下面只給出UserDao的代碼,剩下兩個類似
/**
* @Description: 用戶表dao
* @Author along
* @Date 2019/1/9 14:07
*/
public interface UserDao extends JpaRepository<User, String> {
}
繼承了JpaRepository后會自動被spring注冊成為bean,這樣用戶表的dao層就編寫好了,可以進行基本的crud操作。
JpaRepository為我們做了什么?來看下JpaRepository的源碼
@NoRepositoryBean
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); // 根據id列表查詢
<S extends T> List<S> saveAll(Iterable<S> entities); // 批量保存
void flush(); // 立即寫入數據庫,正常情況下在事務提交的時候,JPA會自動執行flush()一次性保存所有數據。
<S extends T> S saveAndFlush(S entity); // 插入數據并且立即將更改寫入數據庫
void deleteInBatch(Iterable<T> entities); // 批量刪除
void deleteAllInBatch(); // 刪除批量調用中的所有實體(清空表)
T getOne(ID id); // 根據id得到一個對象
@Override
<S extends T> List<S> findAll(Example<S> example); // 實例查詢
@Override
<S extends T> List<S> findAll(Example<S> example, Sort sort); // 實例查詢,排序
}
可以看到JpaRepository實現了基本的crud操作,JpaRepository同時繼承了PagingAndSortingRepository和QueryByExampleExecutor接口
PagingAndSortingRepository接口包含了全表查詢時的分頁查詢和排序,源碼如下:
@NoRepositoryBean
public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> {
Iterable<T> findAll(Sort sort); // 查詢多有,帶排序
Page<T> findAll(Pageable pageable); // 分頁查詢多有
}
QueryByExampleExecutor接口是用來做復雜查詢的,十分好用,會在下文詳細介紹,下面是該接口的源碼:
public interface QueryByExampleExecutor<T> {
<S extends T> Optional<S> findOne(Example<S> example); // 根據實例查詢一個實體
<S extends T> Iterable<S> findAll(Example<S> example); // 查詢所有符合給定實例的實體
<S extends T> Iterable<S> findAll(Example<S> example, Sort sort); // 查詢所有符合給定實例的實體,帶排序
<S extends T> Page<S> findAll(Example<S> example, Pageable pageable); // 分頁查詢所有符合給定實例的實體
<S extends T> long count(Example<S> example); // 得到符合給定實例的數量
<S extends T> boolean exists(Example<S> example); // 判斷是否存在
}
現在我們對繼承了JpaRepository的UserDao可以做到多少數據庫操作已經有了大概的認識。
二、JPA的使用
1. 基本CRUD操作
下面我們編寫UserService來演示基本的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);
}
/**
* 分頁查詢所有,帶排序功能
*/
public Page<User> findAll() {
//分頁+排序查詢演示:
//Pageable pageable = new PageRequest(page, size);//2.0版本后,該方法以過時
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);
}
}
上面代碼舉例了簡單的幾個crud操作,這里要對分頁查詢和更新操作做特別的說明:
分頁查詢:
分頁查詢的關鍵在于創建Pageable對象,一般通過實現類PageRequest創建,早先的版本我們同伙new的方式創建Pageable對象,如下
Pageable pageable = new PageRequest(page, size)
但是在2.0版本后該方法已經過時,我們轉而使用PageRequest的of
方法創建Pageable實例,下面是源碼片段:
/**
* 不帶排序
*/
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));
}
下面是幾個模擬場景舉例:
- 第1頁每頁顯示20條
Pageable pageable = PageRequest.of(0, 20);
- 第1頁顯示20條,倒序排序,按創建時間字段排序,如果創建時間相同,按更新時間排序
方式一:構建Sort對象方式創建
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并沒有提供專門的update方法,而是將更新操作放在save中完成了,下面是save方法的源碼實現:
@Transactional
public <S extends T> S save(S entity) {
if (entityInformation.isNew(entity)) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
}
}
我們看到調用save方法傳入一個實例,首先會通過entityInformation.isNew(entity)
來判斷該實體是否是一個新的對象,具體的是先判斷有無id,如果有就通過id在數據庫中查找是否存在對應的數據,如果存在就是更新操作,會調用EntityManager
的merge()
方法執行更新,如果不存在就說明是插入操作,會調用EntityManager
的persist()
方法執行插入。
EntityManager管理器
通過源碼我們可以看到save方法實質上是調用的EntityManager的方法完成的數據庫操作,所以這里有必要介紹下EntityManager接口,在此之前得了解jpa中實體對象擁有的四種狀態:
- 瞬時狀態(new/transient):沒有主鍵,不與持久化上下文關聯,即 new 出的對象(但不能指定id的值,若指定則是游離態而非瞬時態)
- 托管狀態(persistent):使用EntityManager進行find或者persist操作返回的對象即處于托管狀態,此時該對象已經處于持久化上下文中(被EntityManager監控),任何對該實體的修改都會在提交事務時同步到數據庫中。
- 游離狀態(detached):有主鍵,但是沒有跟持久化上下文關聯的實體對象。
- 刪除狀態 (deleted):當調用EntityManger對實體進行remove后,該實體對象就處于刪除狀態。其本質也就是一個瞬時狀態的對象。
下面的圖清晰的表示了各個狀態間的轉化關系:
下面介紹下EntityManager接口的幾個常用方法:
- persist():將臨時狀態(無主鍵)的對象轉化為托管狀態。由于涉及數據庫增刪改,執行該語句前需啟用事務
entityManager.persist(modelObject);
- merge():將游離狀態(有主鍵)的對象轉化為托管托管狀態,不同于persist(),merger()對于操作的對象,如果對象存在于數據庫則對對象進行修改,如果對象在數據庫中不存在,則將該對象作為一條新記錄插入數據庫。
entityManager.merge(modelObject);
- find()與getReference():從數據庫中查找對象。不同點:當對象不存在時,find()會返回null,getReference()則會拋出javax.persistence.EntityNotFoundException異常。
// 參數一:實體類的class,參數二:實體主鍵值
entityManager.find(Class<T> ModelObject.class , int key);
- remove():將托管狀態的對象轉化為刪除狀態。由于涉及數據庫增刪改,執行該語句前需啟用事務
entityManager.remove(entityManager.getReference(ModelObject.class, key));
- refresh(Object obj):重新從數據庫中讀取數據。可以保證當前的實例與數據庫中的實例的內容一致。該方法用來操作托管狀態的對象。
- contains(Object obj):判斷對象在持久化上下文(不是數據庫)中是否存在,返回true/false。
-
flush():立即將對托管狀態對象所做的修改(包括刪除)寫入數據庫。
從上面內容我們發現通過EntityManager對實體對象所做的操作實質是讓對象在不同的狀態間轉換,而這些修改是在執行flush()后才會真正的寫入數據庫。正常情況下不需要手動執行flash(),在事務提交的時候,JPA會自動執行flush()一次性保存所有數據。
如果要立即保存修改,可以手動執行flush()。
同時我們可以通過setFlushModel()
方法修改EntityManager的刷新模式。默認為AUTO
,這種模式下,會在執行查詢(指使用JPQL語句查詢前,不包括find()和getReference()查詢)前或事務提交時自動執行flush()。通過entityManager.setFlushMode(FlushModeType.COMMIT)
設置為COMMIT
模式,該模式下只有在事務提交時才會執行flush()。 - clear():把實體管理器中所有的實體對象(托管狀態)變成游離狀態,clear()之后,對實體類所做的修改也會丟失。
現在我們再回到更新方法,為了方便查看,我們摘取出上面寫好的更新方法實現
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;
}
如果你已經理解了上文中的JPA的四種狀態,那你應該就能看出這段代碼存在的問題,oldUser是從數據庫中查出來的,是托管狀態對象,受EntityManager管理,我們后面對該對象所做的修改會在事務提交時自動調用flush()
將修改寫入數據庫完成更新,所以并不需要再調用save()方法執行更新,這樣顯得多此一舉。當然還要注意這樣實現更新需要在方法上加@Transactional
啟動事務。
下面是修改后的代碼實現:
@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查詢
上文中說到JpaRepository繼承了QueryByExampleExecutor接口,利用該接口可以實現相對復雜的實例查詢,下面再來看看QueryByExampleExecutor接口的源碼:
public interface QueryByExampleExecutor<T> {
<S extends T> Optional<S> findOne(Example<S> example); // 根據實例查詢一個實體
<S extends T> Iterable<S> findAll(Example<S> example); // 查詢所有符合給定實例的實體
<S extends T> Iterable<S> findAll(Example<S> example, Sort sort); // 查詢所有符合給定實例的實體,帶排序
<S extends T> Page<S> findAll(Example<S> example, Pageable pageable); // 分頁查詢所有符合給定實例的實體
<S extends T> long count(Example<S> example); // 得到符合給定實例的數量
<S extends T> boolean exists(Example<S> example); // 判斷是否存在
}
可以看到所有api都需要傳入Example對象,下面是Example api的組成:
- Probe:實體對象,比如我們要查詢User表,那User對象就是可以作為Probe。
- ExampleMatcher:匹配器,用來詳細描述實體對象中的內容的查詢方式,如規定某個屬性為模糊查詢。
- Example:實例,代表的是完整的查詢條件,由Probe和ExampleMatcher共同創建。
寫一個簡單的測試方法來感受下實例查詢
模擬需求:查詢姓名為along和性別為男的用戶
@Autowired
private UserDao userDao;
@Test
public void test() {
// 創建查詢條件數據對象
User user = new User();
user.setName("along");
user.setSex(1);
// 創建實例
Example<User> example = Example.of(user);
// 查詢
List<User> users = userDao.findAll(example);
//輸出結果
System.out.println("數量:" + users.size());
for (User u : users) {
System.out.println(u);
}
}
先創建實體對象user,因為我們是根據姓名和性別查詢,所以為user對象的姓名和性別屬性復制內容,然后同通過Example.of()
方法創建Example實例最后執行查詢。這里我們調用Example.of()
只傳入了一個實體對象user,這樣jpa會在處理時默認傳入一個默認的ExampleMatcher
。
下面是默認ExampleMatcher
規定的查詢方式:
- 忽略空值,只將實體對象中中不為空的字段作為查詢條件
- 所有查詢條件都采用精確匹配
- 查詢條件大小寫敏感
運行測試,控制臺輸出如下內容:
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
數量:4
along
along
along
along
成功查詢出了4條數據,我們接下來觀察打印出來的sql,查詢條件正是name、sex和status,而且都是精確查詢,這時候發現多了一個查詢條件status,因為實例查詢默認會將實例中不為空的內容作為查詢條件,而我們在定義User實體類的時候為status屬性設了默認值1。
如果我們需要查詢條件只有name和sex,這時候就要定義ExampleMatcher
來忽略status屬性。我們修改測試代碼如下
@Autowired
private UserDao userDao;
@Test
public void test() {
// 創建查詢條件數據對象
User user = new User();
user.setName("along");
user.setSex(1);
// 創建匹配器,即規定如何使用查詢條件
ExampleMatcher matcher = ExampleMatcher.matching() // 構建對象
.withIgnorePaths("status"); // 忽略status屬性
// 創建實例
Example<User> example = Example.of(user, matcher);
// 查詢
List<User> users = userDao.findAll(example);
//輸出結果
System.out.println("數量:" + users.size());
for (User u : users) {
System.out.println(u.getName());
}
}
運行測試,控制帶輸出sql如下,可以看到查詢條件已經沒有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
我們可以通過下面代碼創建一個默認的ExampleMatcher對象
ExampleMatcher matcher = ExampleMatcher.matching();
下面是ExampleMatcher
的實現類的部分源碼,展示了 ExampleMatcher
的六個配置項
class TypedExampleMatcher implements ExampleMatcher {
private final NullHandler nullHandler; // Null值處理方式
private final StringMatcher defaultStringMatcher; // 默認字符串匹配方式
private final PropertySpecifiers propertySpecifiers; // 各個屬性特定查詢方式
private final Set<String> ignoredPaths; // 忽略屬性列表
private final boolean defaultIgnoreCase; // 默認大小寫忽略方式
private final MatchMode mode; // 默認為all,目前沒看出有什么特別的作用
/**
* 空參構造,為上面的屬性設置默認值
* null處理方式為忽略
* 默認字符串匹配方式為default(精確匹配)
* 各屬性特定查詢方式默認為空
* 忽略屬性列表默認為空列表
* 默認大小寫忽略方式為false,不忽略
* MatchMode默認為all
*/
TypedExampleMatcher() {
this(NullHandler.IGNORE, StringMatcher.DEFAULT, new PropertySpecifiers(), Collections.emptySet(), false, MatchMode.ALL);
}
......
}
從源碼我們可以看出ExampleMatcher的默認配置如下:
- nullHandler:IGNORE。Null值處理方式:忽略
- defaultStringMatcher:DEFAULT。默認字符串匹配方式:默認(相等)
- defaultIgnoreCase:false。默認大小寫忽略方式:不忽略
- propertySpecifiers:空。各屬性特定查詢方式,空。
- ignoredPaths:空列表。忽略屬性列表,空列表。
但是只創建一個默認的ExampleMatcher沒有什么意義,上文說到就算你創建Example實例時不傳ExampleMatcher對象,jpa也會自動加上默認的ExampleMatcher。
下面對每個配置項進行詳細講解
-
nullHandler:null值處理方式,枚舉類型,兩個可選值:
INCLUDE(包括)
和IGNORE(忽略)
,默認為IGNORE
。
通過下面代碼改變默認的null值處理方式:
// 默認就是忽略,所以再設置為忽略就沒有意義了,下面設置為不忽略
ExampleMatcher matcher = ExampleMatcher.matching()
.withNullHandler(ExampleMatcher.NullHandler.INCLUDE) // 方式一
.withIncludeNullValues(); // 方式二
-
defaultStringMatcher:默認字符串匹配方式,枚舉類型,六個可選值:
DEFAULT(默認,效果同EXACT)
,EXACT(精確匹配,即 = )
,STARTING(開始匹配,即 ?% )
,ENDING(結束匹配,即 %? )
,CONTAINING(包含,模糊匹配,即 %?% )
,REGEX(正則表達式匹配)
下面是改變默認字符串匹配方式的代碼示例:
ExampleMatcher matcher = ExampleMatcher.matching()
//下面只用配置一個
.withStringMatcher(ExampleMatcher.StringMatcher.STARTING) // 開始匹配 %?
.withStringMatcher(ExampleMatcher.StringMatcher.ENDING) // 結束匹配 ?%
.withStringMatcher(ExampleMatcher.StringMatcher.CONTAINING) // 包含,模糊匹配 %?%
.withStringMatcher(ExampleMatcher.StringMatcher.REGEX); // 正則匹配
-
propertySpecifiers:各屬性特定查詢方式,描述了各個屬性單獨定義的查詢方式,每個查詢方式中包含4個元素:屬性名、字符串匹配方式、大小寫忽略方式、屬性轉換器。如果屬性未單獨定義查詢方式,或單獨查詢方式中,某個元素未定義(如:字符串匹配方式),則采用 ExampleMatcher 中定義的默認值,即上面介紹的 defaultStringMatcher 和 defaultIgnoreCase 的值。
一個屬性的特定查詢方式,包含了3個信息:字符串匹配方式、大小寫忽略方式、屬性轉換器,存儲在 propertySpecifiers 中,操作時用 GenericPropertyMatcher 類來傳遞配置信息。
下面是該屬性配置的代碼示例:
ExampleMatcher matcher = ExampleMatcher.matching()
//方式一:單獨設置name字段為模糊查詢方式
.withMatcher("name", ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.CONTAINING))
//方式二:設置name字段為模糊查詢,忽略大小寫
.withMatcher("name", ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.CONTAINING, true))
//方式三(推薦):鏈式設置
.withMatcher("name", ExampleMatcher.GenericPropertyMatchers.contains().ignoreCase());
-
ignoredPaths:忽略屬性列表,忽略的屬性不參與查詢過濾。
下面是添加忽略屬性的代碼示例:
ExampleMatcher matcher = ExampleMatcher.matching()
.withIgnorePaths("status", "sex");
-
defaultIgnoreCase:默認大小寫忽略方式,布爾型,當值為false時,即不忽略,大小不相等。該配置對所有字符串屬性過濾有效,除非該屬性在 propertySpecifiers 中單獨定義自己的忽略大小寫方式。
下面是改變默認大小寫忽略方式的代碼示例:
ExampleMatcher matcher = ExampleMatcher.matching()
// 下面兩個等價
.withIgnoreCase()
.withIgnoreCase(true)
// 單獨為name屬性設置忽略大小寫,可設置多個值
.withIgnoreCase("name")
實例查詢的限制
- 不支持過濾條件分組。即不支持過濾條件用 or(或) 來連接,所有的過濾查件,都是簡單一層的用 and(并且) 連接。
- 靈活匹配只支持字符串類型,其他類型只支持精確匹配
參考文章:https://blog.csdn.net/zhao_tuo/article/details/78604324
3. 接口規范方法名查詢(最讓我驚喜的查詢方式)
根據可讀性極強的方法名就能創建查詢,初次接觸時會讓你覺得很不可思議
說明:按照Spring data 定義的規則,查詢方法以find|read|get開頭,涉及條件查詢時,條件的屬性用條件關鍵字連接,整個查詢方法名按駝峰式命名。
我們來看下面代碼
/**
* @Description: 接口規范方法名查詢示例
* @Author along
* @Date 2019/1/9 14:07
*/
public interface UserDao extends JpaRepository<User, String>, JpaSpecificationExecutor<User> {
/**
* and條件查詢
* 對應sql:select u from User u where u.name = ?1 and u.email = ?2
* 參數名大寫,條件名首字母大寫,并且接口名中參數出現的順序必須和參數列表中的參數順序一致
*/
User findByNameAndEmail(String name, String email);
/**
* or條件查詢
* 對應sql:select u from User u where u.name = ?1 or u.password = ?2
*/
List<User> findByNameOrPassword(String name, String password);
/**
* between查詢
* 對應sql:select u from User u where u.id between ?1 and ?2
*/
List<User> findByCreateTimeBetween(Date startTime, Date endTime);
/**
* less查詢
* 對應sql:select u from User u where u.id < ?1
*/
List<User> findByCreateTimeLessThan(Date time);
/**
* greater查詢
* 對應sql:select u from User u where u.id > ?1
*/
List<User> findByCreateTimeGreaterThan(Date time);
/**
* is null查詢
* 對應sql:select u from User u where u.name is null
*/
List<User> findByNameIsNull();
/**
* Is Not Null查詢
* 對應sql:select u from User u where u.name is not null
*/
List<User> findByNameIsNotNull();
/**
* like模糊查詢
* 這里的模糊查詢并不會自動在name兩邊加"%",需要手動對參數加"%"
* 對應sql:select u from User u where u.name like ?1
*/
List<User> findByNameLike(String name);
/**
* Not Like模糊查詢
* 對應sql:select u from User u where u.name not like ?1
*/
List<User> findByNameNotLike(String name);
/**
* 倒序排序查詢
* 對應sql:select u from User u where u.password = ?1 order by u.id desc
*/
List<User> findByPasswordOrderByCreateTimeDesc(String password);
/**
* <>查詢
* 對應sql:select u from User u where u.name <> ?1
*/
List<User> findByNameNot(String name);
/**
* in 查詢,方法的參數可以是 Collection 類型,也可以是數組或者不定長參數
* 對應sql:select u from User u where u.id in ?1
*/
List<User> findByIdIn(List<String> ids);
/**
* not in 查詢,方法的參數可以是 Collection 類型,也可以是數組或者不定長參數
* 對應sql:select u from User u where u.id not in ?1
*/
List<User> findByIdNotIn(List<String> ids);
/**
* 分頁查詢,方法的參數可以是 Collection 類型,也可以是數組或者不定長參數
* 對應sql:select u from User u where u.id not in ?1 limit ?
*/
Page<User> findByIdNotIn(List<String> ids, Pageable pageable);
}
dao編寫完畢,只需在service中調用即可,是不是感覺比上面的Example要優雅很多!而且可以應付大多數的查詢需求。如果你用idea,idea還會在你編寫方法是提供智能提示,簡直不要太貼心。
這里簡單地介紹下原理:jpa框架在進行方法名解析時,如果遇到以 find、findBy、read、readBy、get、getBy為前綴的方法名,會忽略前綴,對剩下部分進行解析,再后面會識別如And、Or這樣的關鍵字,來判斷以何種方式連接查詢關鍵字。而且如果方法的最后一個參數是 Sort 或 Pageable 類型,就會提取相關的信息,以便按規則進行排序或者分頁查詢。
4. @Query創建查詢
比起接口規范方法名查詢,@Query顯得稍微麻煩一點,需要自己寫JPQL查詢語句,但是卻更加強大,對方法名沒有要求,可以準確控制JPQL語句,而且不局限于查詢,還可以和@Modifying一起使用實現跟新操作,你甚至可以使用@Query來指定本地查詢,寫真正的sql語句,只要設置nativeQuery=true(但是個人建議不要用本地查詢,這樣就失去了JPA的優勢,如果喜歡寫sql,為什么不直接用mybatis呢?)
下面是示例代碼
/**
* @Description: @Query示例
* @Author along
* @Date 2019/1/9 14:07
*/
public interface UserDao extends JpaRepository<User, String> {
/**
* 根據name查詢,支持命名參數
*/
@Query("select u from User u where u.name = :mame")
List<User> findUserByName(@Param("name")String name);
/**
* 根據sex查詢,縮影參數
*/
@Query("select u from User u where u.sex = ?1")
List<User> findUsersBySex(Integer sex);
/**
* 模糊查詢
*/
@Query("select u from User u where name like concat('%',?1,'%') ")
List<User> findByName(String name);
/**
* 本地查詢
*/
@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);
}
注意模糊查詢的 JPQL 寫法,不要寫成like '%?1%'
,這樣是查不出來東西的。
用命名參數需要在參數前面用@Param()
注解制定參數名,不然會查詢失敗。
跟新需要加上@Modifying
,而且Modifying queries的返回值只能為void或者是int/Integer,調用更新方法前需要開啟事務,否則會跟新失敗。
5. Criteria查詢
上文介紹的查詢方法面簡單的查詢需求已經足夠了,但是如果給定的查詢條件是不固定的,需要動態的創建查詢語句,那上文的方法都就都不適用了,這里可以用Criteria查詢解決。
Criteria API查詢是通過面向對象的方式構建查詢,相比于傳統的基于字符串的JPQL查詢,優勢是類型安全,更加的面向對象。
這里推薦一篇文章,對 Criteria API 講解的十分詳細:詳解JPA 2.0動態查詢機制:Criteria API
下面我們先用 Criteria API 寫一個service方法來實現一個簡單的查詢需求
@Autoware
private EntityManager entityManager;
public List<User> findUserByNameAndSex0(String name, Integer sex) {
// 1. 利用entityManager構建出CriteriaQuery類型的參數
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<User> query = builder.createQuery(User.class);
// 2. 獲取User的Root,也就是包裝對象
Root<User> root = query.from(User.class);
// 3. 構建查詢條件,這里相當于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); // 到這里一個完整的動態查詢就構建完成了
// 指定查詢結果集,相當于“select id,name...”,如果不設置,默認查詢root中所有字段
query.select(root);
// 4. 執行查詢,獲取查詢結果
TypedQuery<User> typeQuery = entityManager.createQuery(query);
List<User> resultList = typeQuery.getResultList();
return resultList;
}
上面代碼完整的實現了一次通過Criteria API查詢的過程,下面編寫的測試方法測試下查詢結果
@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());
}
}
下面是運行后控制臺打印出來的查詢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中結果集并不包括articleList和roles,原因是我們再User實體中做表關聯配置時將這兩個字段定義為了懶加載(fetch = FetchType.LAZY)
。
盡管這達到了我們的目的,但這未免太麻煩了,創建一個查詢需要這么多步驟,完全可以利用JPQL語句實現相同的需求,如下:
@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);
幸運的是JPA為我們考慮到了這一點,我們可以發現上面完成一次查詢一共有四個步驟,其中除了第3步構建查詢條件,其他的都是固定的代碼,JPA提供了JpaSpecificationExecutor
接口幫我們實現了步驟1、2、4,我們在編寫代碼時只需要實現第3步即可。
接下來我們重新實現下上面的需求。
首先是要讓UserDao繼承JpaSpecificationExecutor接口,如下:
public interface UserDao extends JpaRepository<User, String>, JpaSpecificationExecutor<User> {
}
JpaSpecificationExecutor接口并不在JpaRepository接口體系中,需要額外繼承,而且Spring data JPA不會自動掃描識別,所以要和任意一個Repository的子接口一起使用。
接下來我們在UserService中編寫實現方法,如下
@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) {
// 構建查詢條件并返回
return criteriaBuilder.and(
criteriaBuilder.equal(root.get("name"), name),
criteriaBuilder.equal(root.get("sex"), sex)
);
}
});
}
我們只需要重寫Sepcfication接口的toPredicate方法,而toPredicate方法自動攜帶了我們需要的三個參數,這都是JPA提前為我們創建好的,不需要我們手動創建,我們只需要在方法中構建一個Predicate即可,也就是我們自開始的實現的第3步的代碼。是不是簡潔了很多?
下面深入源碼看看JPA是怎么幫我們實現的,首先要看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構建的,每個方法的功能也一目了然,接下來看看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);
}
/**
* 簡單的靜態工廠方法,在{@link Specification}周圍添加一些語法糖。
*/
static <T> Specification<T> where(Specification<T> spec) {
return Specifications.where(spec);
}
/**
* 將給定的{@link Specification}與當前的一個進行對比。
*
* @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);
}
/**
* 將給定的規范與當前規范進行或運算。
*/
default Specification<T> or(Specification<T> other) {
return Specifications.composed(this, other, OR);
}
/**
* 為給定的{@link Predicate}形式的被引用實體的查詢創建WHERE子句
*/
@Nullable
Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder);
}
我們只需要把關注點放到最下面的toPredicate方法上,這也是唯一需要我們手動實現的方法。
我們再來看findAll()方法的具體實現:
public List<T> findAll(@Nullable Specification<T> spec) {
return getQuery(spec, Sort.unsorted()).getResultList();
}
因為我們沒有傳入排序對象Sort,這里生成了一個默認的空的排序對象。再進入getQuery方法
protected TypedQuery<T> getQuery(@Nullable Specification<T> spec, Sort sort) {
return getQuery(spec, getDomainClass(), sort);
}
這里通過getDomainClass()方法得到了當前要查詢的User實體的字節碼類型,下面再進入getQuery()方法
protected <S extends T> TypedQuery<S> getQuery(
@Nullable Specification<S> spec, Class<S> domainClass, Sort sort) {
// 1. 構建出CriteriaQuery類型的參數
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<S> query = builder.createQuery(domainClass);
// 2. 獲取User的Root,也就是包裝對象
Root<S> root = applySpecificationToCriteria(spec, domainClass, query);
// 指定查詢結果集
query.select(root);
if (sort.isSorted()) {
query.orderBy(toOrders(sort, root, builder));
}
// 4. 執行查詢,獲取查詢結果
return applyRepositoryMethodMetadata(em.createQuery(query));
}
注意我在源碼中加的1、2、4注釋,再進入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,也就是包裝對象
Root<U> root = query.from(domainClass);
if (spec == null) {
return root;
}
CriteriaBuilder builder = em.getCriteriaBuilder();
// 重點?。。?!在這里執行我們實現的toPredicate()方法
Predicate predicate = spec.toPredicate(root, query, builder);
if (predicate != null) {
// 到這里一個完整的動態查詢就構建完成了
query.where(predicate);
}
return root;
}
看到這你會發現我們最開始的原始實現中的1、2、4步JPA都做了實現,并且源碼中spec參數調用了toPredicate方法,也就是我們自己在service中做的實現,而且在調動toPredicate方法之前Root、CriteriaQuery和CriteriaBuilder都已經創建好了。
到這里我們已經知道JpaSpecificationExecutor底層是如何實現的封裝,下面我們來探究下具體的使用。
動態語句查詢
下面我們利用JpaSpecificationExecutor實現一個動態語句查詢,并實現分頁排序功能
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實現的動態查詢有些類似?
多表關聯查詢
User表關聯了兩張表,分別是Article表和Role表,和Article表是一對多關系,和Role表是多對多關系。下面是關聯查詢代碼示例
/**
* 表關聯查詢
* @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;
}
});
}
下面是運行結果生成的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_ // 關聯article
on
user0_.id=articlelis1_.user_id
left outer join
user_role roles2_ // 關聯中間表user_role
on
user0_.id=roles2_.user_id
left outer join
role role3_ // 關聯role
on
roles2_.role_id=role3_.id
where
articlelis1_.id=? or role3_.id=?
Criteria查詢到這里也就介紹完了,能力有限,介紹的比較粗略。
如果你熟練地掌握了 Criteria API 的使用,你幾乎可以使用該方式來應對所有的查詢需求,但是面對簡單的查詢需求,還是建議使用更加簡潔的規范方法名查詢,只有在其他方式不方便解決時再考慮用 Criteria API 來解決。具體實際開發中如何選擇,還要根據實際開發情況而定。
三、性能問題解決
上文為了單純地介紹jpa的用法,隱藏了很多問題,在這一節中集中介紹并處理。
循環引用問題
再來回顧下User、Role、Article的關系,User與Role是多對多關系,User與Article是一對多關系。因為文章較長,再往上翻回顧比較麻煩,這里再放上這三個類的源碼,如下
/**
* 用戶實體類
*/
@Entity
@Table(name = "user") //對應數據庫中的表名
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", // 定義中間表的名稱
joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")}, // 定義中間表中關聯User表的外鍵名
inverseJoinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")} // 定義中間表中關聯role表的外鍵名
)
private Set<Role> roles; // 角色外鍵
// getter and setter...
}
/**
* 角色實體類
*/
@Entity
@Table(name = "role")
public class Role extends BaseData {
private static final long serialVersionUID = 5012235295240129244L;
private String roleName; // 角色名
private Integer roleType; // 1: 超級管理員 2: 系統管理員 3:一般用戶
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; // 與用戶多對多
// getter and setter ...
}
/**
* 文章實體類
*/
@Entity
@Table(name = "article")
public class Article extends BaseData{
private static final long serialVersionUID = -4817984675096952797L;
@NotEmpty(message = "標題不能為空")
@Column(nullable = false, length = 50)
private String title;
@Lob // 大對象,映射 MySQL 的 Long Text 類型
@Basic(fetch = FetchType.LAZY) // 懶加載
@Column(nullable = false) // 映射為字段,值不能為空
private String content; // 文章全文內容
@ManyToOne(cascade = {CascadeType.MERGE, CascadeType.REFRESH}, optional = false)
@JoinColumn(name = "user_id") // 設置在article表中的關聯字段(外鍵)名
private User user; // 所屬用戶
// getter and setter ...
}
現在我們編寫查詢代碼來看看這樣存在什么問題,在userDao中編寫查詢方法
List<User> findByName(String name);
下面是UserService中的實現方法
public List<User> findByName(String name) {
List<User> users = userDao.findByName(name);
return users;
}
在編寫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 發起查詢請求。
雖然查詢出了結果,但是結果集非常巨大,并且控制臺會報兩個異常,分別是IllegalStateException
和StackOverflowError
,第一個是無效狀態異常,重點是第二個異常,棧溢出,數據庫中名字為along的用戶只有幾條,結果卻發生了棧溢出,為什么會這樣?
我們再看看打印的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=?
共打印了兩條sql,第一條sql正是我們所希望的,根據name查詢user,由于user關聯了article,jpa于是再發起sql,去查詢article,又由于article中又關聯著user,于是又會再查一遍user,如此循環反復,根本停不下來,很快就棧溢出了。
按這樣說,你可能還會有疑問,這樣的話不是應該打印3條sql嗎?應該該有一條sql去查詢role,為什么只有兩條呢?原因是第二條sql查詢article時就已經進入了死循環并報了異常,所以也就不會再發起第三條sql去查詢role了。
解決方案一:使用@JsonIgnore注解
@JsonIgnore是Jackson提供的注解,在實體類的屬性上加上該注解,這樣在json序列化時會將java bean中的對應的屬性忽略掉,同樣jpa在查詢時也會忽略對應的屬性,如此便可以解決循環查詢的問題。
@JsonIgnore的使用十分靈活,你可以只在User中使用,這樣在查詢User時結果集中就不會包含添加了改注解的屬性,你也可以只在Role和Article實體中與User關聯的屬性上加@JsonIgnore,這樣在查詢User時還是會關聯查詢Role和Article,但是不會發生循環查詢。
這里只在User中加上@JsonIgnore注解,改造后的User類代碼如下
/**
* 用戶實體類
*/
@Entity
@Table(name = "user") //對應數據庫中的表名
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", // 定義中間表的名稱
joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")}, // 定義中間表中關聯User表的外鍵名
inverseJoinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")} // 定義中間表中關聯role表的外鍵名
)
private Set<Role> roles; // 角色外鍵
// getter and setter...
}
再次查詢,成功查詢出結果,結果中不包含article和role,控制臺也只打印出了一條查詢user的sql。
解決方案二:使用@JsonIgnoreProperties注解
同樣是Jackson提供的注解,@JsonIgnoreProperties和@JsonIgnore用法差不多,@JsonIgnoreProperties可以更加細粒度的選擇忽略關聯實體中的屬性。也就是說如果你需要關聯查詢,但是又想控制關聯查詢的類的屬性,那么可以使用該注解。
改造后的User類如下
/**
* 用戶實體類
*/
@Entity
@Table(name = "user") //對應數據庫中的表名
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"}) //解決循環引用問題,content內容大,不加載
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Article> articleList; // 文章
@JsonIgnoreProperties(value = {"users"}) //解決循環引用問題
@ManyToMany(cascade = CascadeType.MERGE, fetch = FetchType.LAZY)
@JoinTable(
name = "user_role", // 定義中間表的名稱
joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")}, // 定義中間表中關聯User表的外鍵名
inverseJoinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")} // 定義中間表中關聯role表的外鍵名
)
private Set<Role> roles; // 角色外鍵
// getter and setter...
}
上面我在關聯屬性aritcleList和roles上加上@JsonIgnoreProperties注解忽略了會引發循環引用的屬性,article中的content是大文本,也將其忽略。
再次發起查詢,沒有發生循環查詢,控制臺打印的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,分別查詢對應的三個表。
解決方案三:返回自定義包裝類
上面我們的查詢接口都是直接用實體類來接收查詢結果并返回,但是更加規范的是創建一個Vo類來接收查詢結果,Vo中的屬性名跟實體類相同,這樣我們可以實現不改變實體類就能決定返回結果的內容。
下面我們定義一個Vo
/**
* @Description: 返回前端的類,vo可以控制返回的字段,也是解決循環引用的一種方案
* @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"}) //解決循環引用問題
//List<Article> getArticleList(); // 文章列表
//@JsonIgnoreProperties(value = {"users"}) //解決循環引用問題
//Set<Role> getRoles(); // 角色外鍵
}
注意該 UserVo 是一個接口,里面的屬性是實體類中對應屬性的 get 方法。使用Vo可以靈活決定返回結果的字段。
下面我們改造userDao中的查詢方法,改用UserVo來接收
List<UserVo> findByName(String name);
下面是UserService中的實現方法,同樣改用UserVo來接收
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);
}
調用查詢,可以實現與方案一和方案二相同的效果,使用也更加靈活簡單,個人最推薦該方案。
N+1查詢問題
上面雖然解決了循環引用的問題,但隨后又出現一個更加頭疼的問題,也是Jpa使用了表關聯屬性后出現的N+1查詢問題,具體這是個什么現象呢,項目中會有很多的實體類,實體類之間通常又會有一對多、多對多的關聯,通常我們會將多的一方設置成懶加載,這樣我們在查詢一個實體時只會查詢出該實體的基本屬性(不包括被設置為懶加載的屬性),然后當我們需要關聯對象的某些屬性時,ORM就會再次發出sql語句查詢關聯的屬性。這也就解釋了為什么上文中查詢一個user,結果卻發出了三條sql語句。
數據小時不會有明顯的問題,可當查詢的數據量變大時,查詢發出的sql數量也會非常大,會引發嚴重的性能問題。
如下面的例子,我們調用userDao.findAll()方法查詢所有用戶數據,并且設置了分頁,查詢20條,結果控制臺打印的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=?
雖然只用分頁控制了結果集,但是sql的量還是非常大的。
JPA2.1提供了新的特性來解決N+1查詢的問題,就是實體圖(EntityGraph),由于實體間的關系錯綜復雜,如果實體間存在關系那么就用線將實體進行連接,那么實體間將形成一個網狀的圖,而實體圖技術就是對這個關系圖的操作,它允許開發者指定某個實體及其發展出來的關系網中的某些節點,這些節點間形成一條路徑,當調用查詢方法時,查詢方法會按照實體圖中的路徑將路徑中的這些節點立即加載,而繞過延遲加載,從而更高效的實現數據的檢索。
下面是實體圖的應用,我們在User表上使用@NamedEntityGraphs注解定義實體圖,你可以定義多個實體圖,如下
/**
* 用戶實體類
*/
@Entity
@Table(name = "user") //對應數據庫中的表名
//EntityGraph(實體圖)使用演示,解決查詢N+1問題
@NamedEntityGraphs({
@NamedEntityGraph(name = "user.all",
attributeNodes = { // attributeNodes 用來定義需要立即加載的屬性
@NamedAttributeNode("articleList"), // 無延伸
@NamedAttributeNode("roles"), // 無延伸
}
),
})
public class User extends BaseData {
...
}
@NamedEntityGraph代表著一個實體圖,通過name屬性設置實體圖的名字,通過attributeNodes屬性來定義需要立即加載的屬性,如果role還關聯了另外一張表,并且設置為了懶加載,那如果想要立即加載改表,就通過subgraphs屬性來進行描述,下面是在Role實體中定義的實體圖
@Entity
@Table(name = "role")
// EntityGraph(實體圖)使用演示,解決查詢N+1問題
@NamedEntityGraphs({
@NamedEntityGraph(name = "role.all",
attributeNodes = { // attributeNodes 用來指定要立即加載的節點,節點用 @NamedAttributeNode 定義
@NamedAttributeNode(value = "users", subgraph = "userWithArticleList"), // 要立即加載users屬性中的articleList元素
},
subgraphs = { // subgraphs 用來定義關聯對象的屬性,也就是對上面的 userWithArticleList 進行描述
@NamedSubgraph(name = "userWithArticleList", attributeNodes = @NamedAttributeNode("articleList")), // 一層延伸
}
),
})
public class Role extends BaseData {
...
}
光是定義了實體圖還不夠,我們需要在查詢時指定使用哪一個實體圖進行查詢,如下,重寫findAll()方法,使用@EntityGraph注解指定通過名為user.all的實體圖查詢
public interface UserDao extends JpaRepository<User, String>, JpaSpecificationExecutor<User> {
//重寫findAll(Pageable pageable)方法,用實體圖查詢
@EntityGraph(value = "user.all", type = EntityGraph.EntityGraphType.FETCH)
@Override
Page<User> findAll(Pageable pageable);
}
執行查詢,打印出的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語句。
本文相關源碼地址
https://github.com/alonglong/spring-boot-all/tree/master/spring-boot-jpa