JPA是Java Persistence API的简称,中文名为Java持久层API,是JDK 5.0注解或XML描述对象-关系表的映射关系,并将运行期的实体对象持久化到数据库中。
3.1 JPA简介
Sun引入新的JPA ORM规范出于两个原因:其一,简化现有Java EE和Java SE应用开发工作;其二,Sun希望整合ORM技术,实现天下归一。
JPA包括以下3方面的内容:
- 一套API标准。在javax.persistence的包下面,用来操作实体对象,执行CRUD操作,框架在后台替代我们完成所有的事情,开发者从烦琐的JDBC和SQL代码中解脱出来。
- 面向对象的查询语言:Java Persistence Query Language(JPQL)。这是持久化操作中很重要的一个方面,通过面向对象而非面向数据库的查询语言查询数据,避免程序的SQL语句紧密耦合。
- ORM(object/relational metadata)元数据的映射。JPA支持XML和JDK5.0注解两种元数据的形式,元数据描述对象和表之间的映射关系,框架据此将实体对象持久化到数据库表中。
JPA的宗旨是为POJO提供持久化标准规范,由此可见,经过这几年的实践探索,能够脱离容器独立运行,方便开发和测试的理念已经深入人心了。Hibernate3.2+、TopLink 10.1.3以及OpenJPA都提供了JPA的实现,以及最后的Spring的整合Spring Data JPA。目前互联网公司和传统公司大量使用了JPA的开发标准规范。
3.2 JPA和Mybatis
在前面的内容中,我们详细学习了Mybatis框架,那么Mybatis和JPA有什么区别呢?
3.2.1 Mybatis
MyBatis本是Apache的一个开源项目iBatis,2010年这个项目由Apache SoftwareFoundation迁移到了Google Code,并且改名为MyBatis。MyBatis着力于POJO与SQL之间的映射关系,可以进行更为细致的SQL,使用起来十分灵活,上手简单,容易掌握,所以深受开发者的喜欢,目前市场占有率最高,比较适合互联应用公司的API场景。
3.2.2 Spring Data JPA
可以理解为JPA规范的再次封装抽象,底层还是使用了Hibernate的JPA技术实现,引用JPQL(Java Persistence Query Language)查询语言,属于Spring整个生态体系的一部分。随着Spring Boot和Spring Cloud在市场上的流行,Spring Data JPA也逐渐进入大家的视野,它们组成有机的整体,使用起来比较方便,加快了开发的效率,使开发者不需要关心和配置更多的东西,完全可以沉浸在Spring的完整生态标准实现下。JPA上手简单,开发效率高,对对象的支持比较好,又有很大的灵活性,市场的认可度越来越高。
3.3 JPA的主要类和结构图
3.3.1 常用类及接口
7个Repository接口:
- Repository (org.springframework.data.repository)
- CrudRepository (org.springframework.data.repository)
- PagingAndSortingRepository (org.springframework.data.repository)
- QueryByExampleExecutor (org.springframework.data.repository.query)
- JpaRepository (org.springframework.data.jpa.repository)
- JpaSpecificationExecutor (org.springframework.data.jpa.repository)
- QueryDslPredicateExecutor (org.springframework.data.querydsl)
2个实现类:
- SimpleJpaRepository (org.springframework.data.jpa.repository.support)
- QueryDslJpaRepository (org.springframework.data.jpa.repository.support)
上述类和接口的关系图如下:
对于上述接口和类,在这里先有初步了解,在后面的小节中会详细讲解。
3.3.2 SpringBoot整合JPA
下面,我们使用SpringBoot整合JPA,选用Mysql数据库做一个实例:
第一步:创建数据库并插入数据:
CREATE DATABASE JPA_DEMO;
USE JPA_DEMO;
CREATE TABLE USER(USER_ID INT PRIMARY KEY AUTO_INCREMENT,USERNAME VARCHAR(20),MAILN VARCHAR(20));
INSERT INTO USER (USERNAME,MAIL) VALUES ('admin','admin@163.com')
第二步:新建SpringBoot项目并添加如下依赖:
org.springframework.boot
spring-boot-starter-thymeleaf
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-data-jpa
mysql
mysql-connector-java
8.0.24
org.projectlombok
lombok
1.18.20
provided
org.springframework.boot
spring-boot-devtools
runtime
true
第三步:配置application.yaml,在文件中配置数据库连接
#Thymeleaf配置
spring:
thymeleaf:
mode: HTML5
encoding: UTF-8
cache: false
devtools:
restart:
enabled: true
datasource:
url: jdbc:mysql://localhost:3306/jpa_demo
username: root
password: byte2020
jpa:
show-sql: true #打印SQL语句
第四步:在项目中新建cn.bytecollege.model包,并新建实体类,代码如下:
package cn.bytecollege.model;
import lombok.Data;
import javax.persistence.*;
@Data
@Entity
@Table(name = "user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private int userId;
@Column(name = "username")
private String username;
@Column(name = "mail")
private String mail;
}
在实体类中出现了@Data,@Entity,@Table,@Id,@GeneratedValue等注解,这些注解分别属于lombok和JPA,将会在后续的内容详细了解。此处不做赘述
第五步:在项目中新建cn.bytecollege.repository包,并新建UserRepository接口,代码如下:
package cn.bytecollege.repository;
import cn.bytecollege.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository {
}
第六步:在项目中新建cn.bytecollege.service包,并新建UserService类,代码如下:
package cn.bytecollege.service;
import cn.bytecollege.model.User;
import cn.bytecollege.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public List findAllUser(){
return userRepository.findAll();
}
}
第七步:在项目中新建cn.bytecollege.controller包,并新建IndexController类,代码如下:
package cn.bytecollege.controller;
import cn.bytecollege.model.User;
import cn.bytecollege.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.List;
@Controller
public class IndexController {
@Autowired
private UserService userService;
@RequestMapping(value = "/")
public String index(Model model){
List list = userService.findAllUser();
model.addAttribute("list",list);
return "index";
}
}
第八步:在项目下templates目录下新建名称为index.html的Thymeleaf模板文件,代码如下:
首页
序号
ID
姓名
邮箱
运行项目的启动类,并在浏览器窗口访问http://localhost:8080/即可看到效果。
3.4 CrudRepository方法详解
从源码可以看到CrudRepository提供了公共的通用的CRUD方法。
public interface CrudRepository extends Repository {
//保存单个对象
S save(S entity);
//保存多个对象
Iterable saveAll(Iterable entities);
//通过ID查询
Optional findById(ID id);
//通过ID判断是否存在
boolean existsById(ID id);
//查询所有数据
Iterable findAll();
//通过ID查询所有数据
Iterable findAllById(Iterable ids);
//获取总记录数
long count();
//根据ID删除数据
void deleteById(ID id);
//根据数据实体删除数据
void delete(T entity);
//通过ID删除多条数据
void deleteAllById(Iterable extends ID> ids);
//迭代删除多条数据
void deleteAll(Iterable extends T> entities);
//删除所有数据
void deleteAll();
}
该接口中定义了通用的增删改查方法,如果对对单表做简单的增删改查,在创建的Repository接口中继承该接口即可。
3.5 PagingAndSortingRepository接口
从该接口的字面意思就可以看出该接口跟分页和排序有关,查看该接口的源码,该接口只定义了2个方法:
- Iterable
findAll(Sort sort):该方法会对查询的数据进行排序,排序规则需要通过Sort对象声明。 - Page
findAll(Pageable pageable):对查询的数据进行分页,分页条数可在Pageable中声明。
public interface PagingAndSortingRepository extends CrudRepository {
Iterable findAll(Sort sort);
Page findAll(Pageable pageable);
}
3.5.1 Sort类
Sort是用于定于排序规则的工具类,Sort类的使用非常简介,Sort提供了以下 个方法用于构造排序条件,支持单字段排序,也支持多字段排序:
//该方法用于指定排序的字段,默认使用升序排序
public static Sort by(String... properties) {
Assert.notNull(properties, "Properties must not be null!");
return properties.length == 0 ? unsorted() : new Sort(DEFAULT_DIRECTION, Arrays.asList(properties));
}
//该方法用于指定排序规则和排序字段
public static Sort by(Sort.Direction direction, String... properties) {
Assert.notNull(direction, "Direction must not be null!");
Assert.notNull(properties, "Properties must not be null!");
Assert.isTrue(properties.length > 0, "At least one property must be given!");
return by((List)Arrays.stream(properties).map((it) -> {
return new Sort.Order(direction, it);
}).collect(Collectors.toList()));
}
下面通过示例来演示该方法的使用,首先新建UserRepository接口,该接口继承
PagingAndSortingRepository接口。
@Repository
public interface UserRepository extends PagingAndSortingRepository {
}
在test目录下的测试用例中编写如下代码:
package cn.bytecollege.chapter03;
import cn.bytecollege.chapter03.model.User;
import cn.bytecollege.chapter03.repository.UserRepository;
import cn.bytecollege.chapter03.service.UserService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Sort;
import java.util.List;
@SpringBootTest
class Chapter03ApplicationTests {
@Autowired
private UserRepository userRepository;
@Test
void contextLoads() {
//指定字段升序排列
Iterable users1 = userRepository.findAll(Sort.by("userId"));
users1.forEach(user -> {
System.out.println(user);
});
//指定字段以及排序方式
Iterable users2 = userRepository.findAll(Sort.by(Sort.Direction.DESC,"userId"));
users2.forEach(user -> {
System.out.println(user);
});
}
}
需要注意的是Sort类提供了排序的枚举类Direction,其中Sort.Direction.Desc表示降序排序,Sort.Direction.ASC表示升序排序,如果不指定排序规则,则默认使用升序。
3.5.2 Pageable接口
Pageable主要用于设置分页信息,如页面和每页显示的数据条数等。先查看该接口的主要方法。
public interface Pageable {
/**
* 设置页数,可以看出该方法内部直接调用了PageRequest.of()方法
* 指定了页码和页面条数
*/
static Pageable ofSize(int pageSize) {
return PageRequest.of(0, pageSize);
}
/**
* 获取返回数据的条目数
*/
int getPageSize();
/**
* 返回排序参数
*/
Sort getSort();
/**
* 获取下一页数据
*/
Pageable next();
}
3.5.3 Page接口
从
PagingAndSortingRepository接口的方法中可以看出分页查询的方法返回了Page对象。接下来,我们需要了解一下Page接口中的内容。源码如下:
public interface Page extends Slice {
//省略部分代码
}
从接口中可以看出Page接口继承了Slice接口,继续查看Slice接口。源码核心方法如下:
public interface Slice extends Streamable {
//获取当前页码
int getNumber();
//获取当前页面条数
int getSize();
//获取分页中的数据
List getContent();
//判断是否有内容
boolean hasContent();
//获取排序字段及规则
Sort getSort();
//是否第一页
boolean isFirst();
//是否最后一页
boolean isLast();
//是否有下一页
boolean hasNext();
//是否有前一页
boolean hasPrevious();
//获取下一页数据
Pageable nextPageable();
//获取前一页数据
Pageable previousPageable();
}
下面,通过示例来学习分页查询的方法:
@SpringBootTest
class Chapter03ApplicationTests {
@Autowired
private UserRepository userRepository;
@Test
void contextLoads() {
Page page = userRepository.findAll(PageRequest.of(1,5));
List list = page.getContent();
list.forEach(user -> System.out.println(user));
}
}
在该示例中使用PageRequest规定了查询第一页数据,页面中总共有5条数据。
3.6 JpaRepository方法详解
JpaRepository开始是对关系型数据库进行抽象封装。从类图可以看得出来它继承了
PagingAndSortingRepository类,也就继承了其所有方法,并且实现类也是SimpleJpaRepository。从类图上还可以看出JpaRepository继承和拥有了QueryByExampleExecutor的相关方法。
@NoRepositoryBean
public interface JpaRepository extends PagingAndSortingRepository, QueryByExampleExecutor {
//查询所有数据
List findAll();
//排序查询所有数据
List findAll(Sort sort);
//通过Id查询所有数据
List findAllById(Iterable ids);
//批量保存数据
List saveAll(Iterable entities);
//刷新数据库
void flush();
//保存并刷新数据库
S saveAndFlush(S entity);
//批量保存并刷新数据库
List saveAllAndFlush(Iterable entities);
//通过实体对象批量删除
void deleteAllInBatch(Iterable entities);
//通过ID批量删除
void deleteAllByIdInBatch(Iterable ids);
//批量删除
void deleteAllInBatch();
//根据ID查询数据
T getById(ID id);
//条件查询
List findAll(Example example);
//条件排序查询
List findAll(Example example, Sort sort);
}
通过源码和CrudRepository相比较,它支持Query By Example,批量删除,提高删除效率,手动刷新数据库的更改方法,并将默认实现的查询结果变成了List。
在日常开发中,不需要去明确区分该继承那个接口,只需要直接继承JpaRepository接口即可。
JpaRepository的使用方法也一样,只需要继承它即可,例如:
package cn.bytecollege.repository;
import cn.bytecollege.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository {
}
3.7 查询方法的创建
Jpa内部基础架构中有个根据方法名的查询生成器机制,对于在存储库的实体上构建约束查询很有用。该机制方法的前缀有find…By、read…By、query…By、count…By和get…By,从这些方法可以分析它的其余部分(实体里面的字段)。引入子句可以包含其他表达式,例如在Distinct要创建的查询上设置不同的标志。然而,第一个By作为分隔符来指示实际标准的开始。在一个非常基本的水平上,你可以定义实体性条件,并与它们串联(And和Or)。
用一句话概括,待查询功能的方法名由查询策略(关键字)、查询字段和一些限制性条件组成。
@Repository
public interface UserRepository extends JpaRepository {
//根据姓名和邮箱查询
List findByUsernameAndMail(String username,String mail);
//根据ID查询用户
User findByUserId(int userId);
//根据姓名查询并排序
List findByUsernameOrderByUsernameDesc(String username);
}
表达式通常是可以连接的运算符的属性遍历。你可以使用组合属性表达式AND和OR。你还可以将运算关键字Between、LessThan、GreaterThan、Like作为属性表达式。受支持的操作员可能因数据存储而异,因此请参阅官方参考文档的相应部分内容。该方法解析器支持设置一个IgnoreCase标志个别特性(例如,findByLastnameIgnoreCase(…))或支持忽略大小写(通常是一个类型的所有属性为String的情况下,例如,
findByLastnameAndFirstnameAllIgnoreCase(…))。是否支持忽略示例可能会因存储而异,因此请参阅参考文档中的相关章节,了解特定于场景的查询方法。可以通过OrderBy在引用属性和提供排序方向(Asc或Desc)的查询方法中附加一个子句来应用静态排序。要创建支持动态排序的查询方法来影响查询结果。
3.7.1 关键字列表
关键字 | 示例 | JPQL表达 |
And | findByLastnameAndFirstname | where x.lastname=?1 and x.firstname =?2 |
Or | findByLastnameOrFirstname | where x.lastname=?1 or x.firstname=?2 |
Is、Equals | findByFirstname findByFirstnameIs findByFirstnameEquals | where x.firstname=?1 |
Between | findByStarDateBetween | where x.startDate between ?1 and ?2 |
LessThan | findByAgeLessThan | where x.age < ?! |
LessThanEqual | findByAgeLessThanEqual | where x.age<=?1 |
GreaterThan | findByAgeGreaterThan | where x.age>?1 |
GreaterThanEqual | findByAgeGreaterThanEqual | where x.age>=?! |
After | findByStartDateAfter | where startDate>?1 |
Before | findByStartDateBefore | where x.startDate1 |
IsNull | findByAgeIsNull | where x.age is null |
IsNotNull NotNull | findByAage(Is)NotNull | where x.age not null |
Like | findByFirstnameLike | where x.firstname not like ? |
NotLike | findByFirstnameNotLike | where x.firstname like ? |
StartingWith | findByFirstnameStartingWith | where x.firstname like ?1 参数增加前缀% |
EndingWith | findByFirstnameEndingWith | where x.lastname like ?1 参数增加后缀% |
Containing | findByFirstnameContaining | where x.firstname like ?1 参数被%包裹 |
OrderBy | findByAgeOrderByLastnameDesc | where x.age = ?1 order by x.lastname desc |
Not | findByLastnameNot | where x.lastname <>?1 |
In | findByAgeIn(Collection | where x.age in ?1 |
NotIn | findByAgeNotIn(Collection | where x.age not int ?1 |
True | findByActiveTrue() | where x.active = true |
False | findByActiveFalse() | where x.active = false |
IgnoreCase | findByFirstnameIgnoreCase | where upper(x.firstname) = upper(?1) |
注意,除了find的前缀之外,我们查看PartTree的源码,还有如下几种前缀:
private static final String QUERY_PATTERN = "find|read|get|query|search|stream";
private static final String COUNT_PATTERN = "count";
private static final String EXISTS_PATTERN = "exists";
private static final String DELETE_PATTERN = "delete|remove";
3.8 查询结果处理
3.8.1 参数选择分页和排序
Page findByUsername(String username,Pageable pageable);
Slice findByUsername(String username,Slice Slice);
List findByUsername(String username,Sort sort);
List findByUsername(String username,Pageable pageable);
第一种方法允许将
org.springframework.data.domain.Pageable实例传递给查询方法,以便动态地将分页添加到静态定义的查询中。Page知道可用的元素和页面的总数。它通过基础框架里面触发计数查询来计算总数。由于这可能是昂贵的,具体取决于所使用的场景,说白了,当用到Pageable的时候会默认执行一条cout语句。而Slice的作用是,只知道是否有下一个Slice可用,不会执行count,所以当查询较大的结果集时,只知道数据是足够的就可以了,而且相关的业务场景也不用关心一共有多少页。
排序选项也通过Pageable实例处理。如果只需要排序,那么在
org.springframework.data.domain.Sort参数中添加一个参数即可。正如你可以看到的,只返回一个List也是可能的。在这种情况下,Page将不会创建构建实际实例所需的附加元数据(这反过来意味着必须不被发布的附加计数查询),而仅仅是限制查询仅查找给定范围的实体。
3.8.3 限制结果查询
在查询方法上加限制查询结果的关键字first和top。
User findFirstOrderByUsernameAsc();
User findTopByOrderByAgeDesc();
Page queryFirst10ByUsername(String username,Pageable pageable);
Slice findTop3ByLastname(String lastname,Pageable pageable);
List findFirst10ByLastname(String lastname,Sort sort);
List findTop10ByLastname(Stirng lastname,Pageable pageable);
查询方法的结果可以通过关键字来限制first或top,其可以被互换地使用。可选的数值可以追加到顶部/第一个以指定要返回的最大结果大小。如果数字被省略,则假设结果大小为1。限制表达式也支持Distinct关键字。此外,对于将结果集限制为一个实例的查询,支持将结果包装到一个实例中的Optional中。如果将分页或切片应用于限制查询分页(以及可用页数的计算),则在限制结果中应用。
3.9 注解式查询
3.9.1 @Query的使用
使用命名查询为实体声明查询是一种有效的方法,对于少量查询很有效。一般只需要关心@Query里面的value和nativeQuery的值。使用声明式JPQL查询有一个好处,就是启动的时候就知道语法正确与否。
@Query注解源码如下:
public @interface Query {
//指定的JPQL语句。(nativeQuery=true的时候,是原生的SQL语句)
String value() default "";
//指定count的JPQL语句,如果不指定将根据query自动生成
//nativeQuery=true的时候,是原生的SQL语句
String countQuery() default "";
//根据哪个字段count,一般默认即可
String countProjection() default "";
//默认false,表示value里面不是原生的sql语句
boolean nativeQuery() default false;
//可以指定一个query的名字,必须唯一,如果不指定,则自动生成,
//生成规则:{$domainClass}.${queryMethodName}
String name() default "";
//可以指定一个count的query名字,必须唯一,如果不指定,则自动生成
//生成规则:{$domainClass}.${queryMethodName}count
String countName() default "";
}
下面,我们来演示该注解的使用。
@Repository
public interface UserRepository extends JpaRepository {
@Query(value = "select u from User u where u.username=:username")
List findByUsername(@Param("username")String username);
}
在上面的代码中使用了@Query注解指定了该方法的查询语句,该语句就是JPQL语句。
从该语句中可以看出,虽然JPQL语句和SQL语句有相似之处,但是在JPQL语句中,操作的都是和表建立了映射关系的实体,以及和表字段有映射关系的实体属性。而不是直接对表进行操作,这就屏蔽了SQL语句操作的复杂性,直接操作对应的对象即可。需要注意的是,当在做查询时,要为实体对象起别名,在select后也是对象别名,不能直接写实体名,否则会抛出异常。
此外,不管是JPQL语句也好,还是SQL语句也好,通常需要向JPQL或者SQL语句传递参数,那么如何接收呢,在上面的代码中参数位置处使用了":方法参数名"的方式(注意:@Parma注解的作用是将方法参数名称和sql参数名称绑定,如果方法参数名称和sql语句参数名称一致,可以忽略该注解),JPA还提供了一种方式:“?x”,这里的x是指参数的索引,从1开始,也就是说,上面@Query中的语句接收参数也可使用以如下方式:
@Query(value = "select u from User u where u.username=?1")
@Query中同样支持原生sql语句,但是需要nativeQuery=true,示例如下:
@Query(value="select * from User",nativeQuery=true)
在SpringBoot测试类中进行测试:
@SpringBootTest
class Chapter03ApplicationTests {
@Autowired
private UserRepository userRepository;
@Test
void contextLoads() {
List list = userRepository.findByUsername("admin");
list.forEach(user -> {
System.out.println(user);
});
}
}
3.9.2 @Query排序
@Query在JPQL下想实现排序,直接用PageRequest或者直接用Sort参数都可以。在排序实例中实际使用的属性需要与实体模型里面的字段相匹配,这意味着它们需要解析为查询中使用的属性或别名。示例如下:
public interface UserRepository extends JpaRepository {
@Query("select u from User u where u.username like ?1%")
List findByUsernameAndSort(String username,Sort sort);
}
//调用上述方法
userRepository.findByAndSort("admin",Sort.by("username"));
3.9.3 @Query分页
如果要使用@Query分页,直接使用Page对象接收参数,参数直接用Pageable的实现类即可。
public interface UserRepository extends JpaRepository {
@Query("select u from User u where u.username like ?1%")
Page findByAndSort(String username,Pageable pageable);
}
//调用上述方法
userRepository.findByAndSort("admin",PageRequest.of(1,10));
3.9.4 @Modifying注解
@Modifying通常是用于标明该操作属于增加、删除、修改。
首先查看@Modifying注解源码:
public @interface Modifying {
boolean flushAutomatically() default false;
//如果配置了一级缓存,这是时候配置该属性为true,会刷新一级缓存,否则在同一接口中
//更新了对象,接着查询这个对象,查出来的对象时未更新之前的状态。
boolean clearAutomatically() default false;
}
下面示例@Modifying注解的用法:
@Repository
public interface UserRepository extends JpaRepository {
@Modifying
@Query(value = "DELETE FROM User u where u.userId=?1")
int deleteById(int userId);
}
@SpringBootTest
class Chapter03ApplicationTests {
@Autowired
private UserRepository userRepository;
@Test
void contextLoads() {
int k = userRepository.deleteById(1);
System.out.println(k);
}
}
3.10 @Entity实例中常用注解
实体中的基本注解包括:@Entity、@Table、@Id、@IdClass、@GeneratedValue、@Basic、@Transient、@Column、@Temporal、@Enumerated、@Lob。
在本小节内将详细讲解上述注解:
3.10.1 @Entity
@Entity定义对象将会成为被JPA管理的实体,将映射到指定的数据库表。这个注解是必须的。
@Entity
@Table(name = "user")
public class User {
//省略部分代码
}
3.10.2 @Table
该注解的作用是将对象实体和数据库表进行关联,该注解通常和@Entity搭配使用。查看该注解源码:
public @interface Table {
//用于指定该对象实体对应的数据库表名
String name() default "";
//通常用于设置表所属的数据库,可不用设置
String catalog() default "";
//作用类似于catelog,可不用设置
String schema() default "";
//用于设置唯一约束的列,可不设置
UniqueConstraint[] uniqueConstraints() default {};
//用于指定索引列,可不设置
Index[] indexes() default {};
}
3.10.3 @Id
@Id定义属性为数据库的主键,一个实体中必须要添加该属性。
3.10.4 @GeneratedValue
该注解用于定义主键的生成策略,注解源码如下:
public @interface GeneratedValue {
//主键的生成策略
GenerationType strategy() default GenerationType.AUTO;
//通过Sequence生成的ID,常见于Oracle数据库ID生成规则,需要配合@SequenceGenerator使用
String generator() default "";
}
GenerationType是一个枚举类,共有4个值
public enum GenerationType {
//通过表产生主键,框架有表模拟序列产生主键,该策略可以更方便的迁移数据库
TABLE,
//通过序列产生主键,通过@SequenceGenerator注解指定序列化名,MySQL不支持
SEQUENCE,
//采用数据库ID自增长,一般用于MySQL数据库
IDENTITY,
//JPA自动选择合适的策略,默认选项
AUTO;
}
3.10.4 @Column
@Column用于定义对象实体中属性对应的数据库列名。首先查看该注解源码:
public @interface Column {
//数据库中表的列名,可选,如果不填写则认为字段名和实体属性名一样。如果属性名和字段名不一致
//可以使用该属性指定
String name() default "";
//是否唯一,可选,默认false
boolean unique() default false;
//是否允许为空,可选,默认false
boolean nullable() default true;
//执行insert操作时是否包含此字段,默认为true,可选
boolean insertable() default true;
//执行update操作时是否包含此字段,默认为true,可选
boolean updatable() default true;
//表示该字段在数据库中的实例类型
String columnDefinition() default "";
//用于指定表名
String table() default "";
//字段默认长度
int length() default 255;
//指定字段精度
int precision() default 0;
//指定字段精度
int scale() default 0;
}
3.11 关联关系注解
关联关系注解包括@JoinColumn、@OneToOne、@OneToMany、@ManyToOne、@ManyToMany、@JoinTable、@OrderBy。
3.11.1 @JoinColumn
该注解用于指定外键关联的字段名称,该注解源码如下:
public @interface JoinColumn {
//目标表的字段名,必填属性
String name() default "";
//本实体的字段名,非必填,默认是本表的ID
String referencedColumnName() default "";
//外键字段是否唯一
boolean unique() default false;
//外键字段是否可以为空
boolean nullable() default true;
//是否跟随一起新增
boolean insertable() default true;
//是否跟随一起更新
boolean updatable() default true;
//表示该字段在数据库中的实例类型
String columnDefinition() default "";
//指定表名
String table() default "";
}
@JoinColumn主要配合@OneToOne、@ManyToOne、@OneToMany一起使用,单独使用没有意义。
3.11.2 @OneToOne
该注解用于表名表与表中的数据存在一对一关系,其源码如下:
public @interface OneToOne {
//关系目标实体,非必填
Class targetEntity() default void.class;
//级联操作策略
CascadeType[] cascade() default {};
//数据获取方式,EAGER(立即加载)/LAZY(懒加载)
FetchType fetch() default FetchType.EAGER;
//是允许为空
boolean optional() default true;
//关联关系被谁维护,非必填,一般不需要特别指定
String mappedBy() default "";
//是否级联删除,和CascadeType.REMOVE的效果一样,只要配置了两种中的一种就会自动级联删除
boolean orphanRemoval() default false;
}
级联操作的枚举源码如下:
public enum CascadeType {
//包括以下所有选项
ALL,
//级联新建
PERSIST,
//级联更新
MERGE,
//级联删除
REMOVE,
//级联刷洗
REFRESH,
}
可以看出该注解的配置比较简单,需要注意的是mappedBy属性,只有关系维护方才能操作两者的关系,被维护方及时设置了维护方属性进行存储也不会更新外键关联:
- mappedBy不能与@JoinColum或者@JoinTable同时使用。
- mappedBy的值是指另一方的实体里面属性的字段,而不是数据库的字段,也不是实体的对象的名字,即另一方配置了@JoinColumn或者@JoinTable注解的属性的字段名称。
@OneToOne需要配合@JoinColumn一起使用。注意:可以双向关联,也可以只配置一方,需要视实际需求而定。
下面我们通过一个示例来学习该注解的用法:
首先创建数据表,分别为部门表和雇员表,雇员和部门表之间存在一对一的关系。
#创建部门表
CREATE TABLE DEPARTMENT(
DEPARTMENT_ID INT PRIMARY KEY AUTO_INCREMENT,
DEPARTMENT_NAME VARCHAR(20),
CREATE_TIME DATETIME);
#创建雇员表
CREATE TABLE EMPLOYEE(
EMPLOYEE_ID INT PRIMARY KEY AUTO_INCREMENT,
NAME VARCHAR(20),
AGE INT,
DEPARTMENT_ID INT);
#新增数据
INSERT INTO DEPARTMENT (DEPARTMENT_NAME,CREATE_TIME) VALUES ("技术部",now());
INSERT INTO EMPLOYEE (NAME,AGE,DEPARTMENT_ID) VALUES ("Jack",28,1);
新建实体类
@Data
@Entity
@Table(name = "department")
public class Department {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int departmentId;
@Column(name = "department_name")
private String departmentName;
@Column(name = "create_time")
@UpdateTimestamp
private Date createTime;
}
@Data
@Entity
@Table(name = "employee")
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int employeeId;
@Column(name = "name")
private String name;
@Column(name = "age")
private int age;
private int departmentId;
@OneToOne
@JoinColumn(name = "departmentId",insertable = false,updatable = false)
private Department department;
}
departmentId是指引用对象的主键名称。
新建EmployeeRepository.java
public interface EmployeeRepository extends JpaRepository {
}
在测试类中进行测试:
@SpringBootTest
class Chapter03ApplicationTests {
@Autowired
private EmployeeRepository employeeRepository;
@Test
void contextLoads() {
Employee employee = employeeRepository.findById(1).get();
System.out.println(employee);
}
}
3.11.3 @OneToMany与@ManyToOne关联关系
@OnToMany与@ManyToOne可以相对存在,也可以只存在一方。
public @interface OneToMany {
//同OneToOne
Class targetEntity() default void.class;
//级联操作,如果不填,默认关系不会产生任何影响
CascadeType[] cascade() default {};
//获取数据方式,默认懒加载
FetchType fetch() default LAZY;
//关系被谁维护
String mappedBy() default "";
//是否级联删除
boolean orphanRemoval() default false;
}
继续以上一小节中的两张表为例,员工对于部门属于一对一的关系,而部门对于员工则是一对多的关系,对实体类进行修改如下:
@Data
@Entity
@Table(name = "employee")
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int employeeId;
@Column(name = "name")
private String name;
@Column(name = "age")
private int age;
@Column(name = "departmentId")
private int departmentId;
}
@Data
@Entity
@Table(name = "department")
public class Department {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int departmentId;
@Column(name = "department_name")
private String departmentName;
@Column(name = "create_time")
@UpdateTimestamp
private Date createTime;
@OneToMany(fetch = FetchType.EAGER)
@JoinColumn(name = "departmentId")
private List list;
}
新建DepartmentRepository.java
@Repository
public interface DepartmentRepository extends JpaRepository {
}
测试类中进行测试:
@SpringBootTest
class Chapter02ApplicationTests {
@Autowired
private UserRepository userRepository;
@Autowired
private EmployeeRepository employeeRepository;
@Autowired
private DepartmentRepository departmentRepository;
@Test
void contextLoads() {
List list = departmentRepository.findAll();
list.forEach(e-> System.out.println(e));
}
}
3.11.4 @OrderBy关联查询时排序
该注解通常和@OneToMany一起使用。源码如下:
public @interface OrderBy {
/**
* 排序的字段格式如下:
* orderby_list::= orderby_item [,orderby_item]*
* orderby_item::= [property_or_field_name] [ASC | DESC]
* 字段也可以是实体属性,也可以使数据库字段,默认ASC
*/
String value() default "";
}
以上一小节的Department对象为例,用法如下:
public class Department {
//省略部分字段
@OneToMany(fetch = FetchType.EAGER)
@JoinColumn(name = "departmentId")
@OrderBy("age DESC")
private List list;
}
3.11.5 @JoinTable关联表
如果对象与对象之间有一个关联关系表的时候,就需要用到@JoinTable,一般和@ManyToMany一起使用。
首先查看@ManyToMany源码:
public @interface JoinTable {
/**
* 中间关联表表名
*/
String name() default "";
/**
* 表catalog
*/
String catalog() default "";
/**
* 表schema
*/
String schema() default "";
/**
* 主连接表的字段
*/
JoinColumn[] joinColumns() default {};
}
@ManyToMany源码如下:
public @interface ManyToMany {
Class targetEntity() default void.class;
CascadeType[] cascade() default {};
FetchType fetch() default LAZY;
String mappedBy() default "";
}
@ManyToMany表示多对多,和@OneToOne、@ManyToOne一样也有单向、双向之分。单向双向和注解没有关系,只看实体类之间是否相互引用。
以学生选课为例,一个学生可以选择多门课程,一门课程也可以被多个学生选择,因此学生和课程之间是多对多的关系,新建数据表,代码如下:
CREATE TABLE STUDENT (
STUDENT_ID INT PRIMARY KEY AUTO_INCREMENT,
STUDENT_NAME VARCHAR(20),
STUDENT_AGE INT,
STUDENT_GENDER CHAR(2)
);
CREATE TABLE COURSE(
COURSE_ID INT PRIMARY KEY AUTO_INCREMENT,
COURSE_NAME VARCHAR(20)
);
CREATE TABLE COURSE_RECORD(
RECORD_ID INT PRIMARY KEY AUTO_INCREMENT,
STUDENT_ID INT,
COURSE_ID INT
);
INSERT INTO STUDENT VALUES (1,'张三',18,'男');
INSERT INTO STUDENT VALUES (2,'李四',19,'男');
INSERT INTO STUDENT VALUES (3,'王五',17,'男');
INSERT INTO STUDENT VALUES (4,'赵六',18,'男');
INSERT INTO COURSE VALUES (1,'语文');
INSERT INTO COURSE VALUES (2,'数学');
INSERT INTO COURSE VALUES (3,'英语');
INSERT INTO COURSE_RECORD VALUES (1,1,1);
INSERT INTO COURSE_RECORD VALUES (2,1,2);
INSERT INTO COURSE_RECORD VALUES (3,1,3);
INSERT INTO COURSE_RECORD VALUES (4,2,1);
INSERT INTO COURSE_RECORD VALUES (5,2,2);
INSERT INTO COURSE_RECORD VALUES (6,2,3);
INSERT INTO COURSE_RECORD VALUES (7,3,1);
INSERT INTO COURSE_RECORD VALUES (8,3,2);
INSERT INTO COURSE_RECORD VALUES (9,3,3);
新建实体类,代码如下:
package cn.bytecollege.chapter03.model;
import lombok.Data;
import javax.persistence.*;
import java.util.List;
@Data
@Entity
@Table(name = "student")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer studentId;
private String studentName;
private Integer studentAge;
private String studentGender;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "course_record",
joinColumns = @JoinColumn(name = "studentId"),
inverseJoinColumns = @JoinColumn(name = "courseId"))
private List courses;
}
package cn.bytecollege.chapter03.model;
import lombok.Data;
import javax.persistence.*;
@Data
@Entity
@Table(name = "course")
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer courseId;
private String courseName;
}
package cn.bytecollege.chapter03.model;
import lombok.Data;
import javax.persistence.*;
@Data
@Entity
@Table(name = "course_record")
public class CourseRecord {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer recordId;
private Integer studentId;
private Integer courseId;
}
新建StudentRepository接口,代码如下:
public interface StudentRepository extends JpaRepository {
}
测试类中进行测试:
@SpringBootTest
class Chapter03ApplicationTests {
@Autowired
private StudentRepository studentRepository;
@Test
void contextLoads() {
List list = studentRepository.findAll();
list.forEach(System.out::println);
}
}
3.12 连接查询和@EntityGraph
当使用@ManyToMany、@ManyToOne、@OneToMany、@OneToOne关联关系的时候,FetchType怎么配置LAZY或者EAGER。SQL真正执行的时候是由一条主表查询和N条子表查询组成的。这种查询效率一般比较低下,比如子对象有N个就会执行N+1条SQL。例如在上一小节中的示例会发现打印了N+1条SQL语句。
有时候我们需要用到Left Join或者Inner Join来提高效率,只能通过@Query的JQPL语法实现,后面我们将讲到的Criteria API也可以做到。Spring Data JPA为了简单地提高查询率,引入了EntityGraph的概念,可以解决N+1条SQL的问题。
3.12.1 @EntityGraph
JPA 2.1推出来的@EntityGraph、@NamedEntityGraph用来提高查询效率,很好地解决了N+1条SQL的问题。两者需要配合起来使用,缺一不可。@NamedEntityGraph配置在@Entity上面,而@EntityGraph配置在Repository的查询方法上面。我们来看一下实例。
- 先在Entity里面定义@NamedEntityGraph,其他都不变。其中,@NamedAttributeNode可以有多个,也可以有一个。
@Data
@NamedEntityGraph(name = "Student.courses",attributeNodes = @NamedAttributeNode("courses"))
@Entity
@Table(name = "student")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer studentId;
private String studentName;
private Integer studentAge;
private String studentGender;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "course_record",
joinColumns = @JoinColumn(name = "studentId"),
inverseJoinColumns = @JoinColumn(name = "courseId"))
private List courses;
}
- 只需要在查询方法上加@EntityGraph注解即可,其中value就是@NamedEntityGraph中的Name。实例配置如下:
public interface StudentRepository extends JpaRepository {
@EntityGraph(value = "Student.courses")
@Override
List findAll();
}
再次运行测试类会发现控制台只打印了1条SQL语句。
3.13 QueryByExampleExecutor的使用
按示例查询(QBE)是一种用户友好的查询技术,具有简单的接口。它允许动态查询创建,并且不需要编写包含字段名称的查询。从UML图中,可以看出继承JpaRepository接口后,自动拥有了按“实例”进行查询的诸多方法。可见SpringData的团队已经认为了QBE是Spring JPA的基本功能了,继承QueryByExampleExecutor和继承JpaRepository都会有这些基本方法。
3.13.1 QueryByExampleExecutor详细配置
QueryByExampleExecutor的源码如下:
public interface QueryByExampleExecutor {
//根据example查找一个对象
Optional findOne(Example example);
//根据example查找一批对象
Iterable findAll(Example example);
//根据example查找一批对象,且排序
Iterable findAll(Example example, Sort sort);
//根据example查找一批对象,并且分页
Page findAll(Example example, Pageable pageable);
//根据example查找,返回符合条件的对象个数
long count(Example example);
//根据example查询,判断是否有符合条件的对象
boolean exists(Example example);
}
接下来查看Example的源码,了解Example的用法:
public interface Example {
static Example of(T probe) {
return new TypedExample(probe, ExampleMatcher.matching());
}
static Example of(T probe, ExampleMatcher matcher) {
return new TypedExample(probe, matcher);
}
T getProbe();
ExampleMatcher getMatcher();
default Class getProbeType() {
return ProxyUtils.getUserClass(this.getProbe().getClass());
}
}
从源码中可以看出Example主要包含三部分内容:
- Probe:这是具有填充字段的域对象的实际实体类,即查询条的封装类。必填。
- ExampleMatcher:ExampleMatcher有关于如何匹配特定字段的匹配规则,它可以重复使用在多个示例。必填。如果不填,用默认的。
Example:Example由探针和ExampleMatcher组成。它用于创建查询。它提供了两个of()方法来创建Example对象:
- of(T probe):以probe对象创建最简单的Example对象
- of(T probe,ExampleMatcher matcher):以probe对象创建Example对象,并使用matcher指定匹配规则。
3.13.1 QueryByExampleExecutor使用示例
以上面章节中的数据表employee为例。
@SpringBootTest
class Chapter03ApplicationTests {
@Autowired
private EmployeeRepository employeeRepository;
@Test
void contextLoads() {
//创建查询条件数据对象
Employee employee = new Employee();
employee.setAge(28);
//创建匹配器,忽略空属性,如果不忽略会将空属性拼接到SQL语句中。
ExampleMatcher matcher = ExampleMatcher.matching();
Example employeeExample = Example.of(employee,matcher);
List list = employeeRepository.findAll(employeeExample);
list.forEach(e->{
System.out.println(e);
});
}
}
上面例子中,是这样创建“实例”的:Example
- Probe:实体对象,在持久化框架中与Table对应的域对象,一个对象代表数据库表中的一条记录,如上例中Employee对象。在构建查询条件时,一个实体对象代表的是查询条件中的“数值”部分。如:要查询年龄“28”的雇员,实体对象只能存储条件值“28”。
- ExampleMatcher:匹配器,它是匹配“实体对象”的,表示了如何使用“实体对象”中的“值”进行查询,它代表的是“查询方式”,解释了如何去查的问题。如上例中ExampleMatcher.matching().取了基本的匹配器,即等值判断,注意:实体类中的属性应该使用包装类或者引用类型,尽量避免使用基本类型。
- Example:实例对象,代表的是完整的查询条件。由实体对象(查询条件值)和匹配器(查询方式)共同创建。
再来理解“实例查询”,顾名思义,就是通过一个例子来查询。要查询的是Employee对象,查询条件也是一个Employee对象,通过一个现有的雇员对象作为例子,查询和这个例子相匹配的对象。
3.13.2 QueryByExampleExecutor的特点及约束
- 支持动态查询。即支持查询条件个数不固定的情况,如:客户列表中有多个过滤条件,用户使用时在“地址”查询框中输入了值,就需要按地址进行过滤,如果没有输入值,就忽略这个过滤条件。对应的实现是,在构建查询条件Customer对象时,将address属性值设置为具体的条件值或设置为null。
- 不支持过滤条件分组。即不支持过滤条件用or(或)来连接,所有的过滤查件,都是简单一层的用and(并且)连接。如firstname = ?0 or (firstname = ?1and lastname = ?2)。
- 仅支持字符串的开始/包含/结束/正则表达式匹配和其他属性类型的精确匹配。查询时,对一个要进行匹配的属性(如:姓名name),只能传入一个过滤条件值,如以Customer为例,要查询姓“刘”的客户,“刘”这个条件值就存储在表示条件对象的Customer对象的name属性中,针对于“姓名”的过滤也只有这么一个存储过滤值的位置,没办法同时传入两个过滤值。正是由于这个限制,有些查询是没办法支持的,例如要查询某个时间段内添加的客户,对应的属性是addTime,需要传入“开始时间”和“结束时间”两个条件值,而这种查询方式没有存两个值的位置,所以就没办法完成这样的查询。
3.13.3 ExampleMatcher详解
ExampleMatcher 提供了如下静态方法来创建实例。
- static ExampleMatcher matching():创建一个需要所有属性都匹配的匹配器。
- static ExampleMatcher matchingAny():创建一个只要任意一个属性匹配的匹配器。
- static ExampleMatcher matchingAlI():它完全等同于 matchin()方法。
假如传入的样本 Student 包含 3 个非空属性:name、gender、address,如果使用 matching()或matchingAl()方法创建的 ExampleMatcher,那么就查询 name、gender、address 属性全都匹配的记录。
但如果使用 matchingAny()方法创建的 ExampleMatcher,那么就查询只要 name、gender、address任意一个属性能匹配的记录。
简单来说,matching()或 matchingAII()方法创建的 ExampleMatcher使用 AND 作为查询条件的连接符,而 matchingAny()方法创建的 ExampleMatcher 使用 OR 作为查询条件的连接符。
此外,ExampleMatcher 还可通过如下方法来指定对特定属性的匹配规则。
- withIgnoreCase():指定属性匹配时默认不区分大小写。
- withlgnoreCase(String...propertyPaths):指定 propertyPaths 参数列出的属性匹配时不区分大小写。
- withlgnoreNullValues():指定不比较 Example 对象中属性值为 null 的属性。
- withlgnorePaths(String...ignoredPaths):指定忽略 ignoredPaths 参数列出的属性,也就是这些属性不参与匹配。
- withncludeNullValues():强行指定要比较 Example 对象中属性值为 null 的属性。
- withMatcher(String propertyPath,比较器):对 propertyPath 参数指定的属性使用专门的匹配规则。
最后一个方法允许通过“比较器”参数来指定匹配规则,该参数既可使用 GenericPropertyMatcher对象,也可使用 Lambda 表达式。
ExampleMatcher接口的实现类只有TypedExampleMatcher核心源码如下:
class TypedExampleMatcher implements ExampleMatcher {
private final NullHandler nullHandler;
private final StringMatcher defaultStringMatcher;
private final PropertySpecifiers propertySpecifiers;
private final Set ignoredPaths;
private final boolean defaultIgnoreCase;
private final MatchMode mode;
TypedExampleMatcher() {
this(NullHandler.IGNORE, StringMatcher.DEFAULT, new PropertySpecifiers(), Collections.emptySet(), false,
MatchMode.ALL);
}
}
关键属性分析:
- nullHandler:Null值处理方式,枚举类型,有2个可选值:INCLUDE(包括)IGNORE(忽略)标识作为条件的实体对象中,一个属性值(条件值)为Null时,表示是否参与过滤。当该选项值是INCLUDE时,表示仍参与过滤,会匹配数据库表中该字段值是Null的记录;若为IGNORE值,表示不参与过滤。
- defaultStringMatcher:默认字符串匹配方式,枚举类型,有6个可选值:DEFAULT(默认,效果同EXACT)EXACT(相等)STARTING(开始匹配)ENDING(结束匹配)CONTAINING(包含,模糊匹配)REGEX(正则表达式)该配置对所有字符串属性过滤有效,除非该属性在propertySpecifiers中单独定义自己的匹配方式。
- defaultIgnoreCase:默认大小写忽略方式,布尔型,当值为false时,即不忽略,大小不相等。该配置对所有字符串属性过滤有效,除非该属性在propertySpecifiers中单独定义自己的忽略大小写方式。
- propertySpecifiers:各属性特定查询方式,描述了各个属性单独定义的查询方式,每个查询方式中包含4个元素:属性名、字符串匹配方式、大小写忽略方式、属性转换器。如果属性未单独定义查询方式,或单独查询方式中,某个元素未定义(如:字符串匹配方式),则采用ExampleMatcher中定义的默认值,即上面介绍的defaultStringMatcher和defaultIgnoreCase的值。
- ignoredPaths:忽略属性列表,忽略的属性不参与查询过滤。
字符串匹配举例:
字符串匹配方式 | 对应的JPQL写法 |
Default&不忽略大小写 | firstname=?1 |
Exact&忽略大小写 | LOWER(firstname)=LOWER(?1) |
Staring&忽略大小写 | Lower(firstname) like lower(?1)+'%' |
Ending&不忽略大小写 | firstname like '%'+?1 |
Containing不忽略大小写 | firstname like '%'+?1+'%' |
3.14 JpaSpecificationExecutor的详细使用
JpaSpecificationExecutor是JPA 2.0提供的Criteria API,可以用于动态生成query。Spring Data JPA支持Criteria查询,可以很方便地使用,足以应付工作中的所有复杂查询的情况了,可以对JPA实现最大限度的扩展。
3.14.1 JpaSpecificationExecutor的方法
public interface JpaSpecificationExecutor {
//根据Specification条件查询单个对象
Optional findOne(@Nullable Specification spec);
//根据Specification条件查询多个对象
List findAll(@Nullable Specification spec);
//根据Specification条件分页查询多个对象
Page findAll(@Nullable Specification spec, Pageable pageable);
//根据Specification条件查询多个对象并排序
List findAll(@Nullable Specification spec, Sort sort);
////根据Specification条件分页查询对象数量
long count(@Nullable Specification spec);
}
这个接口基本是围绕着Specification接口来定义的,Specification接口中只定义了如下一个方法:
public interface Specification extends Serializable {
@Nullable
Predicate toPredicate(Root root, CriteriaQuery> query, CriteriaBuilder criteriaBuilder);
}
所以可看出,JpaSpecificationExecutor是针对Criteria API进行了predicate标准封装,帮我们封装了通过EntityManager的查询和使用细节,操作Criteria更加便利了一些。
3.14.2 Criteria简介
- Root
root:代表了可以查询和操作的实体对象的根。如果将实体对象比喻成表名,那root里面就是这张表里面的字段。这不过是JPQL的实体字段而已。通过里面的Path get(String attributeName)来获得我们操作的字段。 - CriteriaQuery>query:代表一个specific的顶层查询对象,它包含着查询的各个部分,比如:select、from、where、group by、order by等。CriteriaQuery对象只对实体类型或嵌入式类型的Criteria查询起作用,简单理解,它提供了查询ROOT的方法。常用的方法有:
public interface CriteriaQuery extends AbstractQuery {
CriteriaQuery select(Selection extends T> selection);
CriteriaQuery where(Expression restriction);
CriteriaQuery where(Predicate... restrictions);
CriteriaQuery groupBy(Expression>... grouping);
CriteriaQuery groupBy(List> grouping);
CriteriaQuery having(Expression restriction);
CriteriaQuery having(Predicate... restrictions);
CriteriaQuery orderBy(Order... o);
CriteriaQuery orderBy(List o);
CriteriaQuery distinct(boolean distinct);
}
- CriteriaBuilder cb:用来构建CritiaQuery的构建器对象,其实就相当于条件或者是条件组合,以谓语即Predicate的形式返回。构建简单的Predicate示例:
Predicate predicate1 = cb.equal(join.get("id"),1);
Predicate predicate2 = cb.like(join.get("departmentName"),"技术部");
构建组合的Predicate示例
cb.and(predicate1,predicate2);
cb.or(predicate1,predicate2);
其实JpaSpecificationExecutor帮我们提供了一个高级的入口和结构,通过这个入口,可以使用底层JPA的Criteria的所有方法,其实就可以满足了所有业务场景。但实际工作中,需要注意的是,如果一旦我们写的实现逻辑太复杂,第二个人一般看不懂的时候,那一定是有问题的,我们要寻找更简单的,更易懂的,更优雅的方式。
3.14.3 JpaSpecificationExecutor示例
以3.11.3小节中部门表和员工表为例,演示JpaSpecificationExecutor使用。
@Data
@Entity
@Table(name = "employee")
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer employeeId;
@Column(name = "name")
private String name;
@Column(name = "age")
private Integer age;
private Integer departmentId;
}
@Data
@Entity
@Table(name = "department")
public class Department {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(name = "department_name")
private String departmentName;
@Column(name = "create_time")
@UpdateTimestamp
private Date createTime;
@OneToMany(fetch = FetchType.EAGER)
@JoinColumn(name = "departmentId")
private List employees;
}
DepartmentRepository接口需要继承JpaSpecificationExecutor
@Repository
public interface DepartmentRepository extends JpaRepository, JpaSpecificationExecutor {
}
测试类中添加如下代码:
@SpringBootTest
class Chapter03ApplicationTests {
@Autowired
private DepartmentRepository departmentRepository;
@Test
void contextLoads() {
Specification specification = (root,query,cb)->{
//cb用来构造查询条件,如where,like,and,or等
Root employeeRoot = query.from(Employee.class);
//连接表,第一个参数是封装到实体中的实体,即连接表对应的实体对象名
//第二个参数是连接类型,如果不写则默认使用内链接
Join join = root.join("employees", JoinType.LEFT);
return join.getOn();
};
List list = departmentRepository.findAll(specification);
list.forEach(System.out::println);
}
}