JPA(Java Persistence API)是一套 Java 持久化规范,用于将应用程序中的对象映射到关系型数据库。
应用程序的数据访问层通常为域对象提供创建、读取、更新和删除(CRUD)操作,Spring Data JPA 提供了这方面的通用接口以及持久化存储特定的实现,它选择目前最流行之一的 Hibernate 作为 JPA 实现的提供者,旨在简化数据访问层。作为应用程序的开发人员,你只需要编写数据库的存取接口,由 Spring 运行时自动生成这些接口的适当实现,开发人员不需要编写任何具体的实现代码。在 Spring Boot 中,通过使用spring-boot-starter-data-jpa启动器,就能快速开启和使用 Spring Data JPA。

# pom.xml


1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

1. 编程接口

1.1 Repository

这是 Spring Data Jpa 抽象的中心接口,它是一个标记接口。扩展此接口需要传递实体类型和实体的ID字段类型参数,你必须在接口里面声明你自己需要的方法,这些方法由 Spring 在运行时提供具体的实现。

1
2
3
4
5
public interface EmployeeRepository extends Repository<Employee, Long> {
Employee findOne(Long id);
}

1.2 CrudRepository

继承自 Repository 接口,它提供了一套 CRUD 操作的方法。扩展此接口需要传递实体类型和实体的ID字段类型参数,你可以不需要再定义基础的 CRUD 操作方法而直接可以使用它们。但在某些场景中你可能不希望接口对外暴露一套完整的增删查改的方法,比如你只希望提供查改的方法而不希望暴露增删的功能。基于这种情况,你可以使用 Repository 接口,并将需要的方法从 CrudRepository 拷贝到其中以选择性的公开 CRUD 方法。

1
2
3
public interface EmployeeRepository extends CrudRepository<Employee, Long> {
}

1.2.1 save

当你需要修改数据库的数据时,你可以调用此方法。当此方法被调用时,它首先判断参数的实体对象是否是新的。如果是新的,则调用 persist 将对象数据 insert 到数据库。如果不是新的,则调用 merge 将对象数据 update/insert 到数据库。源码:spring-data-jpa.jar!\org\springframework\data\jpa\repository\support\SimpleJpaRepository.java

# SimpleJpaRepository 源码片段


1
2
3
4
5
6
7
8
9
@Transactional
public <S extends T> S save(S entity) {
if (entityInformation.isNew(entity)) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
}
}

判断实体对象是否是新的,其依据是主键字段是否设置了有效的值。源码:spring-data-commons.jar!\org\springframework\data\repository\core\support\AbstractEntityInformation.java

# AbstractEntityInformation 源码片段


1
2
3
4
5
6
7
8
9
10
11
public boolean isNew(T entity) {
ID id = getId(entity);
Class<ID> idType = getIdType();
if (!idType.isPrimitive()) {
return id == null;
}
if (id instanceof Number) {
return ((Number) id).longValue() == 0L;
}
throw new IllegalArgumentException(String.format("Unsupported primitive id type %s!", idType));
}

因此,CrudRepository.save()既有保存又有更新数据的能力。保存一条数据:

1
2
3
4
5
6
7
@Test
public void testSave() {
Employee employee = new Employee();
employee.setName("张三丰");
employee.setAge(24);
employeeRepository.save(employee);
}

更新数据时,应该先从数据库将记录查询出来,对数据修改完成之后再调用save更新回数据库:

1
2
3
4
5
6
@Test
public void testUpdate() {
Employee employee = employeeRepository.findByName("张三丰");
employee.setAge(25);
employeeRepository.save(employee);
}

切勿脑洞大开异想通过设置主键字段的值来直接更新数据库的记录,以下做法是不可取的:

1
2
3
4
5
6
7
@Test
public void testUpdateById() {
Employee employee = new Employee();
employee.setId(1L); // 已知ID=1的记录是存在的
employee.setAge(26); // 期望根据ID更新年龄的值
employeeRepository.save(employee);
}

方法执行完之后悲剧就发生了,除了主键和年龄之外,其余字段的值全部被清空了。观众朋友切勿模仿。

1.2.2 delete

根据主键删除时,主键字段不能为空,并且在数据库中必须得有与主键对应的行记录(通过SELECT查询判断),然后将查询出的行记录删除。

1
2
3
4
@Test
public void testDeleteById() {
employeeRepository.delete(1L); // 产生 SELECT 和 DELETE 语句
}

根据实体删除时,实体对象不能为空,依据实体的主键标识判断数据库中是否有与之对应的行记录,如果有,则将此行删除;如果没有,则调用merge产生 INSERT 语句,然后再删除。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void testDeleteByEntity() {
Employee employee = new Employee();
employee.setId(2L); // 此 ID 在数据库中存在
employeeRepository.delete(employee); // 产生 SELECT 和 DELETE
}
@Test
public void testDeleteByNonExistentEntity() {
Employee employee = new Employee();
employee.setId(20L); // 此 ID 在数据库中不存在
employeeRepository.delete(employee); // 产生 SELECT 和 INSERT 及 DELETE
}

1.3 PagingAndSortingRepository

继承自 CrudRepository 接口,它提供了一个分页和排序的操作方法。扩展此接口需要传递实体类型和实体的ID字段类型参数,但是通常我们会比较少选择扩展该接口,而更多的是在接口里声明含有 Pageable 或 Sort 类型参数的方法来完成分页或排序的功能。

1
2
3
public interface EmployeeRepository extends PagingAndSortingRepository<Employee, Long> {
}

1.3.1 排序查询

1
2
3
4
5
6
7
8
9
@Test
public void testSelectAndOrder() {
List<Order> orders = new ArrayList<>();
orders.add(new Order(Direction.DESC, "salary")); // 薪资降序
orders.add(new Order("age")); // 薪资相同则按年龄升序
orders.add(new Order("hireDate").with(Direction.ASC)); // 薪资和年龄都相同则按入职时间升序
employeeRepository.findAll(new Sort(orders))
.forEach(System.out::println);
}

1
2
3
4
5
6
7
8
@Test
public void testSelectAndSort() {
Sort sort = new Sort(Direction.DESC, "salary") // 薪资降序
.and(new Sort("age")) // 薪资相同则按年龄升序
.and(new Sort(Direction.DESC, "hireDate")); // 薪资和年龄都相同则按入职时间升序
employeeRepository.findAll(sort)
.forEach(System.out::println);
}

1.3.2 分页查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
public void testSelectByPagination() {
// 分页索引从0开始, 表示第一页
Page<Employee> page = employeeRepository.findAll(new PageRequest(0, 2));
long totalElements = page.getTotalElements(); // 查询结果总的记录条数
int totalPages = page.getTotalPages(); // 分页的总页数
List<Employee> content = page.getContent(); // 当前页的数据内容
int number = page.getNumber(); // 当前页的页码, 从0开始, 表示第一页
int numberOfElements = page.getNumberOfElements(); // 每页的记录条数
int size = page.getSize(); // 每页的记录条数
Sort sort = page.getSort(); // 分页查询的排序对象
boolean isFirst = page.isFirst(); // 是否是第一页
boolean isLast = page.isLast(); // 是否是最后一页
boolean hasContent = page.hasContent(); // 当前页是否有数据
boolean hasNext = page.hasNext(); // 是否有下一页
boolean hasPrevious = page.hasPrevious(); // 是否有上一页
Pageable nextPageable = page.nextPageable(); // 下一页的分页对象
Pageable previousPageable = page.previousPageable(); // 上一页的分页对象
page.forEach(System.out::println);
}

1.3.3 分页并排序

1
2
3
4
5
6
7
@Test
public void testSelectByPaginationAndSort() {
Sort sort = new Sort(Direction.DESC, "salary") // 薪资降序
.and(new Sort("age")); // 薪资相同则按年龄升序
Page<Employee> page = employeeRepository.findAll(new PageRequest(0, 2, sort));
page.forEach(System.out::println);
}

1.4 JpaRepository

继承自 PagingAndSortingRepository 接口,它提供了一组实用的操作方法,如批量操作等。扩展此接口需要传递实体类型和实体的ID字段类型参数,该接口的一部分方法返回 List 类型的实体,与之不同的是,CrudRepository 返回的是 Iterable 类型的实体。

1
2
3
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
}

2. 定义查询方法

Spring Data JPA 在运行时会为接口创建代理对象并为接口声明的方法提供具体的实现。代理提供了两种方式来从方法名中提取查询,一种是从方法名中直接提取查询,另外一种是从方法中提取手工定义的查询语句。代理如何创建查询是由具体的策略来决定的。

策略 简述
CREATE 根据方法名构造出一个特定的查询。
具体的做法是从方法名中移除一组已知的前缀,然后解析剩余的部分。
USE_DECLARED_QUERY 使用查询注解定义的查询语句。如:
@Query、@NamedQuery、@NamedNativeQuery
CREATE_IF_NOT_FOUND 默认使用的策略。
它组合了 CREATE 和 USE_DECLARED_QUERY 两个策略。它首先使用 USE_DECLARED_QUERY 策略查找,如果找不到再使用 CREATE 策略。

2.1 创建查询

JPA 提供了一种可以根据方法名称直接构造出查询语句的方式,这种方式称为创建查询。在存储库接口中定义的方法,其名称只需按照约定命名,需满足以下的规则:

  • 方法名必须以:findBy find...By readBy read...By queryBy query...By
    countBy count...By getBy get...By前缀之一开始命名;
  • 在第一个By之后可以添加查询方法的检索条件,可以使用实体的属性名和支持的关键字来组合;
  • 在第一个By之前可以添加FirstTop关键字,表示返回查询结果的第一条数据。除此之外,关键字FirstTop的后面也可以携带数字表示返回前多少条的数据,如Top10
  • 在第一个By之前可以添加Distinct关键字,去掉查询结果中重复的数据;
  • 查询方法如果设定了X个检索条件,那么,查询方法的参数个数也必须是X个,并且参数必须按与检索条件相同的顺序给出;
  • 查询方法同时还可以使用特殊的PageableSort参数,用于分页或排序,该参数不算在X之内;

2.1.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 findByStartDateBetween … where x.startDate between ?1 and ?2
LessThan findByAgeLessThan … where x.age < ?1
LessThanEqual findByAgeLessThanEqual … where x.age <= ?1
GreaterThan findByAgeGreaterThan … where x.age > ?1
GreaterThanEqual findByAgeGreaterThanEqual … where x.age >= ?1
After findByStartDateAfter … where x.startDate > ?1
Before findByStartDateBefore … where x.startDate < ?1
IsNull findByAgeIsNull … where x.age is null
IsNotNull
NotNull
findByAgeNotNull
findByAgeIsNotNull
… where x.age not null
Like findByFirstnameLike … where x.firstname like ?1
NotLike findByFirstnameNotLike … where x.firstname not like ?1
StartingWith findByFirstnameStartingWith … where x.firstname like ?1 (parameter bound with appended %)
EndingWith findByFirstnameEndingWith … where x.firstname like ?1 (parameter bound with prepended %)
Containing findByFirstnameContaining … where x.firstname like ?1 (parameter bound wrapped in %)
OrderBy findByAgeOrderByLastnameDesc … where x.age = ?1 order by x.lastname desc
Not findByLastnameNot … where x.lastname <> ?1
In findByAgeIn(Collection<Age> ages) … where x.age in ?1
NotIn findByAgeNotIn(Collection<Age> age) … where x.age not in ?1
True findByActiveTrue() … where x.active = true
False findByActiveFalse() … where x.active = false
IgnoreCase findByFirstnameIgnoreCase … where UPPER(x.firstame) = UPPER(?1)

2.1.2 查询方法支持的返回值表

类型 简述
void 不需要返回值
Primitives Java 基本数据类型
Wrapper Java 基本数据类型对应的包装类型
T 期望返回的实体类型,查询方法至多只能返回一条数据结果,多于一条数据的结果将抛出异常,没有查询到数据结果,则返回 null
Iterator<T> 迭代器类型
Collection<T> 集合类型
List<T> List 集合类型
Optional<T> Java8 Optional 类型
Stream<T> Java8 Stream 类型
Future<T> Java8 Future 类型,使用@Async注解标注查询方法,并且需要启用 Spring 异步方法执行的功能
CompletableFuture<T> Java8 CompletableFuture 类型,使用@Async注解标注查询方法,并且需要启用 Spring 异步方法执行的功能
ListenableFuture Spring ListenableFuture 类型,使用@Async注解标注查询方法,并且需要启用 Spring 异步方法执行的功能
Slice 分页相关
Page<T> 分页相关

在存储库接口中定义的方法,只需要按照约定命名,就能快速实现查询的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public interface EmployeeRepository extends CrudRepository<Employee, Long> {
// 根据姓名查询
Employee findByName(String name);
// 根据姓名查询, 返回第一条记录
Employee findFirstByName(String name);
// 根据姓名和性别查询
Employee findByNameAndSex(String name, Sex sex);
// 根据性别查询, 返回前3条记录
List<Employee> findTop3BySex(Sex sex);
// 根据性别分页查询
Page<Employee> findBySex(Sex sex, Pageable pageable);
// 根据给定的年龄查找小于且未婚的记录并按年龄升序排序
List<Employee> findByAgeLessThanAndMarriedIsFalseOrderByAge(int age);
}

创建查询的优点是,不用编写查询语句,处理检索条件简单的查询非常方便,而且方法的可读性很高。但是对于检索条件过多的查询方法,很容易导致方法名称过长,可读性降低。

2.2 命名查询

JPA 提供了一种可以将查询语句从存储库接口中独立出来的方式,这种方式称为命名查询。它允许我们通过使用@NamedQuery@NamedNativeQuery注解将预定义好的静态查询语句直接绑定到目标方法。
命名查询的优点是,查询语句集中,便于维护,查询方法的名称不受约束,编写复杂的查询只要合理命名也不会导致产生过长的方法名称。但是由于命名查询的注解都是标注在实体类中,因此它不适合用于大量定义查询语句,这样会使得实体类变得过于臃肿。

2.2.1 @NamedQuery

参数 简述
name 用于定义查询的方法名称,该方法名称是全局范围的,为避免不同的实体定义了相同的方法名称而导致的查询冲突,JPA 明确规定自定义的方法名称的命名需要满足约定:
实体类的简单类名 + “.” + 自定义的查询方法名称
query 用于定义 JPQL 查询语句(附:查询参数语法

使用@NamedQuery注解需要在实体类中标注使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Entity(name = "Employee")
@NamedQueries({
@NamedQuery(
name = "Employee.selectBySex",
query = "SELECT E FROM Employee E WHERE E.sex = ?1"
),
@NamedQuery(
name = "Employee.selectByName",
query = "SELECT E FROM Employee E WHERE E.name = ?1"
)
})
public class Employee {
...
}

然后在存储库接口中声明与这些名称相同的方法即可:

1
2
3
4
5
6
7
public interface EmployeeRepository extends CrudRepository<Employee, Long> {
Employee selectByName(String name);
List<Employee> selectBySex(Sex sex);
}

2.2.2 @NamedNativeQuery

注解@NamedNativeQuery@NamedQuery的用法和作用相类似。不同的是,@NamedQuery使用的是 JPQL 查询语言,可以做到跨数据库平台。而@NamedNativeQuery使用的是 SQL 查询语言,与特定的数据库平台紧密相关。@NamedNativeQuery注解也是需要在实体类中标注使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Entity(name = "Employee")
@NamedNativeQueries({
@NamedNativeQuery(
name = "Employee.searchBySex",
query = "SELECT * FROM EMPLOYEE WHERE SEX = ?1",
resultClass = Employee.class
),
@NamedNativeQuery(
name = "Employee.searchByName",
query = "SELECT * FROM EMPLOYEE WHERE NAME = ?1",
resultClass = Employee.class
)
})
public class Employee {
...
}

相比较@NamedQuery注解而言,多了一个resultClass参数,它用于定义查询结果的返回值类型。

1
2
3
4
5
6
7
public interface EmployeeRepository extends CrudRepository<Employee, Long> {
Employee searchByName(String name);
List<Employee> searchBySex(String sex);
}

2.3 @Query 查询

使用@Query注解可以直接将查询语句绑定到存储库接口的方法上,它同时支持 JPQL 和 SQL 查询语言。另外,它对方法名称的命名没有约束,并且查询语句就编写在方法的上方,方便追踪查询方法的具体作用。

2.3.1 JPQL 查询

注解@Query默认使用的就是 JPQL 查询语言:

1
2
3
4
5
6
public interface EmployeeRepository extends CrudRepository<Employee, Long> {
@Query("SELECT E FROM Employee E WHERE E.name = ?1")
Employee queryOneByName(String name);
}

2.3.2 SQL 查询

@Query注解中,如果要使用 SQL 查询语言,nativeQuery参数需要标记为 true

1
2
3
4
5
6
public interface EmployeeRepository extends CrudRepository<Employee, Long> {
@Query(value = "SELECT * FROM EMPLOYEE WHERE NAME = ?1", nativeQuery = true)
Employee queryOneByName(String name);
}

2.3.3 LIKE 查询

@Query注解中,可以使用高级的LIKE表达式查询(命名查询不支持):

1
2
3
4
5
6
public interface EmployeeRepository extends CrudRepository<Employee, Long> {
@Query("SELECT E FROM Employee E WHERE E.name LIKE %?1")
List<Employee> queryNameLike(String suffixName);
}

2.3.4 分页查询

如果你使用的是@Query的 JPQL 查询语言,只需在查询方法中添加Pageable参数就能实现分页查询:

1
2
3
4
5
6
public interface EmployeeRepository extends CrudRepository<Employee, Long> {
@Query("SELECT E FROM Employee E WHERE E.sex = ?1")
Page<Employee> queryBySexPagination(Sex sex, Pageable pageable);
}

如果你使用的不是 JPQL 而是 SQL 查询语言,则还需提供countQuery参数用于查询结果的总条数:

1
2
3
4
5
6
7
8
public interface EmployeeRepository extends CrudRepository<Employee, Long> {
@Query(value = "SELECT * FROM EMPLOYEE WHERE SEX = ?1",
countQuery = "SELECT COUNT(*) FROM EMPLOYEE WHERE SEX = ?1 ",
nativeQuery = true)
Page<Employee> queryBySexPagination(String sex, Pageable pageable);
}

Spring Data JPA 官方文档给出了@Query注解使用本地查询分页的基础示例(Example 51),但是按照该示例编写出的代码运行时报错。

2.3.5 排序查询

如果你使用的是@Query的 JPQL 查询语言,只需在查询方法中添加Sort参数就能实现排序功能:

1
2
3
4
5
6
public interface EmployeeRepository extends CrudRepository<Employee, Long> {
@Query("SELECT E FROM Employee E WHERE E.sex = ?1")
List<Employee> queryBySexAndSort(Sex sex, Sort sort);
}

注:@Query的本地查询(SQL 查询)不支持这种动态排序的功能。

如果是分页查询需要排序支持,可以通过向PageRequest构造器传入Sort对象来完成:

1
2
3
4
5
6
@Test
public void testQueryBySexPagination() {
Page<Employee> page = employeeRepository.queryBySexPagination(Sex.FEMALE,
new PageRequest(0, 2, new Sort("age")));
page.forEach(System.out::println);
}

2.3.6 SpEL 表达式

Spring Data JPA 1.4 版本开始引入 SpEL 表达式,目前支持的 SpEL 表达式非常有限(目前仅有一个):

变量 描述
entityName 存储库接口关联的实体类的实体名称。如果实体类@Entity注解设置了name属性,那么将使用它。否则将使用实体类的简单类名。
1
2
3
4
5
6
public interface EmployeeRepository extends CrudRepository<Employee, Long> {
@Query("SELECT E FROM #{#entityName} E WHERE E.name = ?1")
Employee queryByNameSpEL(String name);
}

2.3.7 更新查询

@Query注解除了可以用来定义查询语句还可以用来定义更新语句(UPDATE/DELETE),在@Query标注的方法上只需要使用@Modifying注解就能实现更新的行为:

1
2
3
4
5
6
7
8
9
10
11
public interface EmployeeRepository extends CrudRepository<Employee, Long> {
@Modifying
@Query("UPDATE Employee E SET E.salary = ?2 WHERE E.name = ?1")
int updateSalaryForName(String name, Double salary);
@Modifying
@Query("DELETE FROM Employee E WHERE E.name = ?1")
int deleteByName(String name);
}

示例项目开发环境:Java-8、Maven-3、IntelliJ IDEA-2017、Spring Boot-1.5.2.RELEASE
完整示例项目链接:spring-boot-jpa-sample
参考文档文献链接:http://docs.spring.io/spring-data/jpa/docs/1.11.1.RELEASE/reference/html