02 Spring Data Common 之 Repoitory 如何全面掌握?

通过上一课时,我们知道了 Spring Data 对整个数据操作做了很好的封装,其中 Spring Data Common 定义了很多公用的接口和一些相对数据操作的公共实现(如分页排序、结果映射、Autiting 信息、事务等),而 Spring Data JPA 就是 Spring Data Common 的关系数据库的查询实现。

所以本课时我们来了解一下 Spring Data Common 的核心内容——Repository。我将从 Repository 的所有子类着手,带领你逐步掌握 CrudRepository、PageingAndSortingRepository、JpaRepository的使用。

在讲解 Repository 之前,我们先来看看 Spring Data JPA 所依赖的 jar 包关系是什么样的,看下 Spring Data Common 的 jar 依赖关系。

Spring Data Common 的依赖关系

我们通过 Gradle 看一下项目依赖,了解一下 Spring Data Common 的依赖关系。

数据库类型为money在java里面调用是什么数据类型_java

通过上图的项目依赖,不难发现,数据库连接用的是 JDBC,连接池用的是 HikariCP,强依赖 Hibernate;Spring Boot Starter Data JPA 依赖 Spring Data JPA;而 Spring Data JPA 依赖 Spring Data Commons。

在这些 jar 依赖关系中,Spring Data Commons 是我们要重点介绍的,因为 Spring Data Commons 是终极依赖。下面我们学习 DB 操作的入口 Repository,为你一一介绍 Repository 的子类。

Repository 接口

Repository 是 Spring Data Common 里面的顶级父类接口,操作 DB 的入口类。首先介绍 Repository 接口的源码、类层次关系和使用实例。

查看 Resposiory 源码

我们查看 Common 里面的 Resposiory 源码,了解一下里面实现了什么。如下所示:

package org.springframework.data.repository;
import org.springframework.stereotype.Indexed;
@Indexed
public interface Repository<T, ID> {
}

package org.springframework.data.repository;
import org.springframework.stereotype.Indexed;
@Indexed
public interface Repository<T, ID> {
}

Resposiory 是 Spring Data 里面进行数据库操作顶级的抽象接口,里面什么方法都没有,但是如果任何接口继承它,就能得到一个 Repository,还可以实现 JPA 的一些默认实现方法。Spring 利用 Respository 作为 DAO 操作的 Type,以及利用 Java 动态代理机制就可以实现很多功能,比如为什么接口就能实现 DB 的相关操作?这就是 Spring 框架的高明之处。

Spring 在做动态代理的时候,只要是它的子类或者实现类,再利用 T 类以及 T 类的 主键 ID 类型作为泛型的类型参数,就可以来标记出来、并捕获到要使用的实体类型,就能帮助使用者进行数据库操作。

Repository 类层次关系

下面我们来根据存这个基类 Repository 接口,顺藤摸瓜看看 Spring Data JPA 里面都有什么。

首先,我们用工具 Intellij Idea,打开类 Repository.class,然后依次导航 → Hierchy 类型,会得到如下图所示的结果:

数据库类型为money在java里面调用是什么数据类型_spring boot_02

通过该层次结构视图,你就会明白基类 Repository 的用意,由此可知,存储库分为以下 4 个大类。

  • ReactiveCrudRepository 这条线是响应式编程,主要支持当前 NoSQL 方面的操作,因为这方面大部分操作都是分布式的,所以由此我们可以看出 Spring Data 想统一数据操作的“野心”,即想提供关于所有 Data 方面的操作。目前 Reactive 主要有 Cassandra、MongoDB、Redis 的实现。
  • RxJava2CrudRepository 这条线是为了支持 RxJava 2 做的标准响应式编程的接口。
  • CoroutineCrudRepository 这条继承关系链是为了支持 Kotlin 语法而实现的。
  • CrudRepository 这条继承关系链正是本课时我要详细介绍的 JPA 相关的操作接口,你也可以把我的这种方法应用到另外 3 种继承关系链里面学习。

然后,通过 Intellij Idea,我们也可以打开类 UserRepository.java(第一课时“Spring Data JPA 初识”里面的案例),在此类里面,鼠标右键点击 Show Diagram 显示层次结构图,用图表的方式查看类的关系层次,打开后如下图(Repository 继承关系图)所示:

数据库类型为money在java里面调用是什么数据类型_Data_03

在这里简单介绍一下,我们需要掌握和使用到的类如下所示。

7 个大 Repository 接口:

  • Repository(org.springframework.data.repository),没有暴露任何方法;
  • CrudRepository(org.springframework.data.repository),简单的 Curd 方法;
  • PagingAndSortingRepository(org.springframework.data.repository),带分页和排序的方法;
  • QueryByExampleExecutor(org.springframework.data.repository.query),简单 Example 查询;
  • JpaRepository(org.springframework.data.jpa.repository),JPA 的扩展方法;
  • JpaSpecificationExecutor(org.springframework.data.jpa.repository),JpaSpecification 扩展查询;
  • QueryDslPredicateExecutor(org.springframework.data.querydsl),QueryDsl 的封装。

两大 Repository 实现类:

  • SimpleJpaRepository(org.springframework.data.jpa.repository.support),JPA 所有接口的默认实现类;
  • QueryDslJpaRepository(org.springframework.data.jpa.repository.support),QueryDsl 的实现类。

关于其他的类,后面我也会通过不同方式的讲解,让你一一认识。下面我们再来看一个 Repository 实例。

一个 Repository 的实例

我们通过一个例子,利用 UserRepository 继承 Repository 来实现对 User 的两个查询方法,如下:

import org.springframework.data.repository.Repository;
import java.util.List;
public interface UserRepository extends Repository<User,Integer> {
	//根据名称进行查询用户列表
	List<User> findByName(String name);
	// 根据用户的邮箱和名称查询
	List<User> findByEmailAndName(String email, String name);
}

import org.springframework.data.repository.Repository;
import java.util.List;
public interface UserRepository extends Repository<User,Integer> {
	//根据名称进行查询用户列表
	List<User> findByName(String name);
	// 根据用户的邮箱和名称查询
	List<User> findByEmailAndName(String email, String name);
}

由于 Repository 接口里面没有任何方法,所以此 UserRepository 对外只有两个可用方法,如上面的代码一样。Service 里面只能调用到 findByName 和 findByEmailAndName 两个方法,我们通过 IDEA 的 Structure 也可以看到对外只有两个方法可用,如下所示:

数据库类型为money在java里面调用是什么数据类型_spring_04

这时,我在第 01 课时中“Spring Boot 和 Spring Data JPA 的 Demo 演示”的例子里,提到过的 Controller 中引用 userRepository 的 save 和 findAll 方法就会报错。

数据库类型为money在java里面调用是什么数据类型_spring boot_05

上面这个实例通过继承 Repository,使 Spring 容器知道 UserRepository 是 DB 操作的类,是我们可以对 User 对象进行 CURD 的操作。这时我们对 Repository 有了一定的掌握,接下来再来看看它的直接子类 CurdRepository 接口都为我们提供了哪些方法。

CrudRepository 接口

下面我们通过 IDEA 工具,看下 CrudRepository 为我们提供的方法有哪些。

数据库类型为money在java里面调用是什么数据类型_Data_06

通过上图,你可以看到其中展示的一些方法,在这里一一说明一下:

  • count(): long 查询总数返回 long 类型;
  • void delete(T entity) 根据 entity 进行删除;
  • void deleteAll(Iterable<? extends T> entities) 批量删除;
  • void deleteAll() 删除所有;原理可以通过刚才的类关系查看,CrudRepository 的实现方法如下:
//SimpleJpaRepository里面的deleteALL方法
public void deleteAll() {
   for (T element : findAll()) {
      delete(element);
   }
}

//SimpleJpaRepository里面的deleteALL方法
public void deleteAll() {
   for (T element : findAll()) {
      delete(element);
   }
}

通过源码我们可以看出 SimpleJpaRepository 里面的 deleteAll 是利用 for 循环调用 delete 方法进行删除操作。我们接着看 CrudRepository 提供的方法。

  • void deleteById(ID id); 根据主键删除,查看源码会发现,其是先查询出来再进行删除;
  • boolean existsById(ID id) 根据主键判断实体是否存在;
  • Iterable<T> findAllById(Iterable ids); 根据主键列表查询实体列表;
  • Iterable<T> findAll(); 查询实体的所有列表;
  • Optional<T> findById(ID id); 根据主键查询实体,返回 JDK 1.8 的 Optional,这可以避免 null exception;
  • <S extends T> S save(S entity); 保存实体方法,参数和返回结果可以是实体的子类;
  • saveAll(Iterable<S> entities) : 批量保存,原理和 save方法相同,我们去看实现的话,就是 for 循环调用上面的 save 方法。

上面这些方法是 CrudRepository 对外暴露的常见的 Crud 接口,我们在对数据库进行 Crud 的时候就会运用到,如我们打算对 User 实体进行 Curd 操作,来看一下应该怎么写,如下所示:

public interface UserRepository extends CrudRepository<User,Long> {
}

public interface UserRepository extends CrudRepository<User,Long> {
}

我们通过 UserRepository 继承 CrudRepository,这个时候我们的 UserRepository 就会有 CrudRepository 里面的所有方法,如下图所示:

数据库类型为money在java里面调用是什么数据类型_spring boot_07

这里我们需要注意一下 save 和 deleteById 的实现逻辑,分别看看一下这两种方法是怎么实现的:

//新增或者保存
public <S extends T> S save(S entity) {
   if (entityInformation.isNew(entity)) {
      em.persist(entity);
      return entity;
   } else {
      return em.merge(entity);
   }
}
//删除
public void deleteById(ID id) {
   Assert.notNull(id, ID_MUST_NOT_BE_NULL);
   delete(findById(id).orElseThrow(() -> new EmptyResultDataAccessException(
         String.format("No %s entity with id %s exists!", entityInformation.getJavaType(), id), 1)));
}

//新增或者保存
public <S extends T> S save(S entity) {
   if (entityInformation.isNew(entity)) {
      em.persist(entity);
      return entity;
   } else {
      return em.merge(entity);
   }
}
//删除
public void deleteById(ID id) {
   Assert.notNull(id, ID_MUST_NOT_BE_NULL);
   delete(findById(id).orElseThrow(() -> new EmptyResultDataAccessException(
         String.format("No %s entity with id %s exists!", entityInformation.getJavaType(), id), 1)));
}

你会发现在进行 Update、Delete、Insert 等操作之前,我们看上面的源码,会通过 findById 先查询一下实体对象的 ID,然后再去对查询出来的实体对象进行保存操作。而如果在 Delete 的时候,查询到的对象不存在,则直接抛异常。

我在这里特别强调了一下 Delete 和 Save 方法,是因为在实际工作中,看到有的同事画蛇添足:自己在做 Save 的时候先去 Find 一下,其实是没有必要的,Spring JPA 底层都考虑到了。这里其实是想告诉你,当我们用任何第三方方法的时候,最好先查一下其源码和逻辑或者 API,然后再写出优雅的代码。

关于 entityInformation.isNew(entity),在这里简单说一下,如果当传递的参数里面没有 ID,则直接 insert;若当传递的参数里面有 ID,则会触发 select 查询。此方法会去看一下数据库里面是否存在此记录,若存在,则 update,否则 insert。后面第 14 课时讲乐观锁实现机制的时候会有详细介绍。

PagingAndSortingRepository 接口

上面我们介绍完了 Crud 的基本操作,发现没有分页和排序方法,那么接下来讲讲 PagingAndSortingRepository 接口,该接口也是 Repository 接口的子类,主要用于分页查询和排序查询。我们先来看看 PagingAndSortingRepository 的源码,了解一下都有哪些方法。

PagingAndSortingRepository 的源码

PagingAndSortingRepository 源码发现有两个方法,分别是用于分页和排序的时候使用的,如下所示:

package org.springframework.data.repository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
@NoRepositoryBean
public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> {
	Iterable<T> findAll(Sort sort); (1)
	Page<T> findAll(Pageable pageable); (2)
}

package org.springframework.data.repository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
@NoRepositoryBean
public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> {
	Iterable<T> findAll(Sort sort); (1)
	Page<T> findAll(Pageable pageable); (2)
}

其中,第一个方法 findAll 参数是 Sort,是根据排序参数,实现不同的排序规则获取所有的对象的集合;第二个方法 findAll 参数是 Pageable,是根据分页和排序进行查询,并用 Page 对返回结果进行封装。而 Pageable 对象包含 Page 和 Sort 对象。

通过开篇讲到的【Repository 继承关系图】和上面介绍的一大堆源码可以看到,PagingAndSortingRepository 继承了 CrudRepository,进而拥有了父类的方法,并且增加了分页和排序等对查询结果进行限制的通用的方法。

PagingAndSortingRepository 和 CrudRepository 都是 Spring Data Common 的标准接口,那么实现类是什么呢?如果我们采用 JPA,那对应的实现类就是 Spring Data JPA 的 jar 包里面的 SimpleJpaRepository。如果是其他 NoSQL的 实现如 MongoDB,那实现就在 Spring Data MongoDB 的 jar 里面的 MongoRepositoryImpl。

关于 PagingAndSortingRepository 源码的介绍到这里,下面我们看看怎么使用这两个方法。

PagingAndSortingRepository 使用案例

第一步:我们定一个 UserRepository 类来继承 PagingAndSortingRepository 接口,实现对 User 的分页和排序操作,实现源码如下:

package com.example.jpa.example1;
import org.springframework.data.repository.PagingAndSortingRepository;
public interface UserRepository extends PagingAndSortingRepository<User,Long> {
}

package com.example.jpa.example1;
import org.springframework.data.repository.PagingAndSortingRepository;
public interface UserRepository extends PagingAndSortingRepository<User,Long> {
}

第二步:我们利用 UserRepository 直接继承 PagingAndSortingRepository 即可,而 Controller 里面就可以有如下用法了:

/**
 * 验证排序和分页查询方法,Pageable的默认实现类:PageRequest
 * @return
 */
@GetMapping(path = "/page")
@ResponseBody
public Page<User> getAllUserByPage() {
   return userRepository.findAll(
         PageRequest.of(1, 20,Sort.by(new Sort.Order(Sort.Direction.ASC,"name"))));
}
/**
 * 排序查询方法,使用Sort对象
 * @return
 */
@GetMapping(path = "/sort")
@ResponseBody
public Iterable<User> getAllUsersWithSort() {
   return userRepository.findAll(Sort.by(new Sort.Order(Sort.Direction.ASC,"name")));
}

/**
 * 验证排序和分页查询方法,Pageable的默认实现类:PageRequest
 * @return
 */
@GetMapping(path = "/page")
@ResponseBody
public Page<User> getAllUserByPage() {
   return userRepository.findAll(
         PageRequest.of(1, 20,Sort.by(new Sort.Order(Sort.Direction.ASC,"name"))));
}
/**
 * 排序查询方法,使用Sort对象
 * @return
 */
@GetMapping(path = "/sort")
@ResponseBody
public Iterable<User> getAllUsersWithSort() {
   return userRepository.findAll(Sort.by(new Sort.Order(Sort.Direction.ASC,"name")));
}

到这里,你已经实现了对实体 User 的 DB 操作,那么以上内容我们学习了 CURD 和分页排序的基本操作,下面看看 JpaRepsitory 的接口为我们提供了哪些方法。

JpaRepository 接口

到这里可以进入到分水岭了,上面的那些都是 Spring Data 为了兼容 NoSQL 而进行的一些抽象封装,而从 JpaRepository 开始是对关系型数据库进行抽象封装。从类图可以看出来它继承 PagingAndSortingRepository 类,也就继承了其所有方法,并且其实现类也是 SimpleJpaRepository。从类图上还可以看出 JpaRepository 继承和拥有了 QueryByExampleExecutor 的相关方法,我们先来看一下 JpaRepository 有哪些方法。一样的道理,我们直接看它的源码,看 Structure 即可,如下图所示:

数据库类型为money在java里面调用是什么数据类型_Data_08

涉及 QueryByExample 的部分我们在 11 课时“JpaRepository 如何自定义”再详细介绍,而 JpaRepository 里面重点新增了批量删除,优化了批量删除的性能,类似于之前 SQL 的 batch 操作,并不是像上面的 deleteAll 来 for 循环删除。其中 flush() 和 saveAndFlush() 提供了手动刷新 session,把对象的值立即更新到数据库里面的机制。

我们都知道 JPA 是 由 Hibernate 实现的,所以有 session 一级缓存的机制,当调用 save() 方法的时候,数据库里面是不会立即变化的,其原理我将在 21 课时“Persistence Context 所表达的核心概念是什么”再详细讲解。JpaRepository 的使用方式也一样,直接继承 JpaRepository 即可。

我们看一个 Demo,用 UserRepository 直接继承 JpaRepository,来实现 JPA 的相关方法,如下所示:

public interface UserRepository extends JpaRepository<User,Long> {
}

public interface UserRepository extends JpaRepository<User,Long> {
}

这样 controller 里面就可以直接调用 JpaRepository 及其父接口里面的所有方法了。

那么以上就是我们对 Repository 及其他子接口的使用案例,在应用时,你需要注意不同的接口有不同的方法,根据业务场景继承不同的接口即可。下面我们接着学习 Repository 的实现类 SimpleJpaRepository。

Repository 的实现类 SimpleJpaRepository

关系数据库的所有 Repository 接口的实现类就是 SimpleJpaRepository,如果有些业务场景需要进行扩展了,可以继续继承此类,如 QueryDsl 的扩展(虽然不推荐使用了,但我们可以参考它的做法,自定义自己的 SimpleJpaRepository),如果能将此类里面的实现方法看透了,基本上 JPA 中的 API 就能掌握大部分内容。

我们可以通过 Debug 视图看一下动态代理过程,如下面【类的继承关系图】所示:

数据库类型为money在java里面调用是什么数据类型_Data_09

你会发现 UserRepository 的实现类是 Spring 启动的时候,利用 Java 动态代理机制帮我们生成的实现类,而真正的实现类就是 SimpleJpaRepository。

通过上面【类的继承关系图】也可以知道 SimpleJpaRepository 是 Repository 接口、CrudRepository 接口、PagingAndSortingRepository 接口、JpaRepository 接口的实现。其中,SimpleJpaRepository 的部分源码如下:

@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID> implements JpaRepository<T, ID>, JpaSpecificationExecutor<T> {
	private static final String ID_MUST_NOT_BE_NULL = "The given id must not be null!";
	private final JpaEntityInformation<T, ?> entityInformation;
	private final EntityManager em;
	private final PersistenceProvider provider;
	private @Nullable CrudMethodMetadata metadata;
	......
	@Transactional
	public void deleteAllInBatch() {
		em.createQuery(getDeleteAllQueryString()).executeUpdate();
	}
	......

@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID> implements JpaRepository<T, ID>, JpaSpecificationExecutor<T> {
	private static final String ID_MUST_NOT_BE_NULL = "The given id must not be null!";
	private final JpaEntityInformation<T, ?> entityInformation;
	private final EntityManager em;
	private final PersistenceProvider provider;
	private @Nullable CrudMethodMetadata metadata;
	......
	@Transactional
	public void deleteAllInBatch() {
		em.createQuery(getDeleteAllQueryString()).executeUpdate();
	}
	......

通过此类的源码,我们可以挺清晰地看出 SimpleJpaRepository 的实现机制,是通过 EntityManger 进行实体的操作,而 JpaEntityInforMation 里面存在实体的相关信息和 Crud 方法的元数据等。

上面我们讲到利用 Java 动态代理机制帮我们生成的实现类,那么关于动态代理的实现,我们可以在 RepositoryFactorySupport 设置一个断点,启动的时候,在我们的断点处就会发现 UserRepository 的接口会被动态代理成 SimpleJapRepository 的实现,如下图所示:

数据库类型为money在java里面调用是什么数据类型_java_10

这里需要注意的是每一个 Repository 的子类,都会通过这里的动态代理生成实现类,在实际工作中 debug 看源码的时候,希望上面介绍的内容可以帮助到你。

Repository 接口给我的启发

在接触了 Repository 的源码之后,我在工作中遇到过一些类似需要抽象接口和写动态代理的情况,所以对于 Repository 的源码,我受到了一些启发:

第一,上面的 7 个大 Repository 接口,我们在使用的时候可以根据实际场景,来继承不同的接口,从而选择暴露不同的 Spring Data Common 给我们提供的已有接口。这其实利用了 Java 语言的 interface 特性,在这里可以好好理解一下 interface 的妙用。

第二,利用源码也可以很好地理解一下 Spring 中动态代理的作用,可以利用这种思想,在改善 MyBatis 的时候使用。

总结

本课时到这里就结束了,这一课时我讲解了 Repository 接口、CrudRepository 接口、PagingAndSortingRepository 接口、JpaRepository 接口的用法,通过源码我们知道了接口里面的方法有哪些、怎么实现的,也知道了 Spring 的动态代理机制是怎么运用到 UserRepository 接口的。

通过这一课时,相信你对 Repository 的基本用法,以及接口暴露的方法和使用方法都有了一定的了解,下节课我会讲解除了 Repository 的接口里面定义的方法之外,还可以在我们的 UserRepository 里面实现哪些方法,又会有哪些动态实现机制呢?我们到时见。

补充一个TIPS:课程中的案例是依赖 lombok 插件的,如下图所示:

数据库类型为money在java里面调用是什么数据类型_JPA_11

并开启 annotation processing。

数据库类型为money在java里面调用是什么数据类型_Data_12

点击下方链接查看源码(不定时更新)
https://github.com/zhangzhenhuajack/spring-boot-guide/tree/master/spring-data/spring-data-jpa


03 Defining Query Method 的命名语法与参数

Spring Data JPA 的最大特色是利用方法名定义查询方法(Defining Query Methods)来做 CRUD 操作,这一课时我将围绕这个内容来详细讲解。

在工作中,你是否经常为方法名的语义、命名规范而发愁?是否要为不同的查询条件写各种的 SQL 语句?是否为同一个实体的查询,写一个超级通用的查询方法或者 SQL?如果其他开发同事不查看你写的 SQL 语句,而直接看方法名的话,却不知道你想查什么而郁闷?

Spring Data JPA 的 Defining Query Methods(DQM)通过方法名和参数,可以很好地解决上面的问题,也能让我们的方法名的语义更加清晰,开发效率也会提升很多。DQM 语法共有 2 种,可以实现上面的那些问题,具体如下:

  • 一种是直接通过方法名就可以实现,这也是本课时会详细介绍的重点内容;
  • 另一种是 @Query 手动在方法上定义,这将在第 05 课时“@Query 帮我们解决了什么问题?什么时候应该选择 @Query?”中详细介绍。

下面我将从 6 个方面来详细讲解 Defining Query Methods。先来分析一下“定义查询方法的配置和使用方法”,这个是 Defining Query Methods 中必须要掌握的语法。

定义查询方法的配置和使用方法

若想要实现 CRUD 的操作,常规做法是写一大堆 SQL 语句。但在 JPA 里面,只需要继承 Spring Data Common 里面的任意 Repository 接口或者子接口,然后直接通过方法名就可以实现,神不神奇?来看下面具体的使用步骤。

第 1 步,User 实体的 UserRepository 继承 Spring Data Common 里面的 Repository 接口:

interface UserRepository extends CrudRepository<User, Long> {
     User findByEmailAddress(String emailAddress);
}

interface UserRepository extends CrudRepository<User, Long> {
     User findByEmailAddress(String emailAddress);
}

第 2 步,对于 Service 层就可以直接使用 UserRepository 接口:

@Service
public class UserServiceImpl{
    @Autowired
    UserRepository userRepository;
    public void testJpa() {
        userRepository.deleteAll();
        userRepository.findAll();
        userRepository.findByEmailAddress("zjk@126.com");
    }

@Service
public class UserServiceImpl{
    @Autowired
    UserRepository userRepository;
    public void testJpa() {
        userRepository.deleteAll();
        userRepository.findAll();
        userRepository.findByEmailAddress("zjk@126.com");
    }

这个时候就可以直接调用 CrudRepository 里面暴露的所有接口方法,以及 UserRepository 里面定义的方法,不需要写任何 SQL 语句,也不需要写任何实现方法。通过上面的两步我们完成了 Defining Query Methods(DQM)的基本使用,下面来看另外一种情况:选择性暴露方法

然而,有时如果不想暴露 CrudRepository 里面的所有方法,那么可以直接继承我们认为需要暴露的那些方法的接口。假如 UserRepository 只想暴露 findOne 和 save,除了这两个方法之外不允许任何的 User 操作,其做法如下。

我们选择性地暴露 CRUD 方法,直接继承Repository(因为这里面没有任何方法),把CrudRepository 里面的 save 和 findOne 方法复制到我们自己的 MyBaseRepository 接口即可,代码如下:

@NoRepositoryBean
interface MyBaseRepository<T, ID extends Serializable> extends Repository<T, ID> {
    T findOne(ID id); 
    T save(T entity);
}
interface UserRepository extends MyBaseRepository<User, Long> {
     User findByEmailAddress(String emailAddress);
}

@NoRepositoryBean
interface MyBaseRepository<T, ID extends Serializable> extends Repository<T, ID> {
    T findOne(ID id); 
    T save(T entity);
}
interface UserRepository extends MyBaseRepository<User, Long> {
     User findByEmailAddress(String emailAddress);
}

这样在 Service 层就只有 findOne、save、findByEmailAddress 这 3 个方法可以调用,不会有更多方法了,我们可以对 SimpleJpaRepository 里面任意已经实现的方法做选择性暴露。

综上所述,得出以下 2 点结论:

  • MyRepository Extends Repository 接口可以实现 Defining Query Methods 的功能;
  • 继承其他 Repository 的子接口,或者自定义子接口,可以选择性地暴露 SimpleJpaRepository 里面已经实现的基础公用方法。

在平时的工作中,你可以通过方法名,或者定义方法名上面添加 @Query 注解两种方式来实现 CRUD 的目的,而 Spring 给我们提供了两种切换方式。接下来我们就讲讲“方法的查询策略设置”。

方法的查询策略设置

目前在实际生产中还没有遇到要修改默认策略的情况,但我们必须要知道有这样的配置方法,做到心中有数,这样我们才能知道为什么方法名可以,@Query 也可以。通过 @EnableJpaRepositories 注解来配置方法的查询策略,详细配置方法如下:

@EnableJpaRepositories(queryLookupStrategy= QueryLookupStrategy.Key.CREATE_IF_NOT_FOUND)

@EnableJpaRepositories(queryLookupStrategy= QueryLookupStrategy.Key.CREATE_IF_NOT_FOUND)

其中,QueryLookupStrategy.Key 的值共 3 个,具体如下:

  • Create:直接根据方法名进行创建,规则是根据方法名称的构造进行尝试,一般的方法是从方法名中删除给定的一组已知前缀,并解析该方法的其余部分。如果方法名不符合规则,启动的时候会报异常,这种情况可以理解为,即使配置了 @Query 也是没有用的。
  • USE_DECLARED_QUERY:声明方式创建,启动的时候会尝试找到一个声明的查询,如果没有找到将抛出一个异常,可以理解为必须配置 @Query。
  • CREATE_IF_NOT_FOUND:这个是默认的,除非有特殊需求,可以理解为这是以上 2 种方式的兼容版。先用声明方式(@Query)进行查找,如果没有找到与方法相匹配的查询,那用 Create 的方法名创建规则创建一个查询;这两者都不满足的情况下,启动就会报错。

以 Spring Boot 项目为例,更改其配置方法如下:

@EnableJpaRepositories(queryLookupStrategy= QueryLookupStrategy.Key.CREATE_IF_NOT_FOUND)
public class Example1Application {
   public static void main(String[] args) {
      SpringApplication.run(Example1Application.class, args);
   }
}

@EnableJpaRepositories(queryLookupStrategy= QueryLookupStrategy.Key.CREATE_IF_NOT_FOUND)
public class Example1Application {
   public static void main(String[] args) {
      SpringApplication.run(Example1Application.class, args);
   }
}

以上就是方法的查询策略设置,很简单。接下来我们再讲讲“Defining Query Method(DQM)语法”,这是可以让方法生效的详细语法。

Defining Query Method(DQM)语法

该语法是:带查询功能的方法名由查询策略(关键字)+ 查询字段 + 一些限制性条件组成,具有语义清晰、功能完整的特性,我们实际工作中 80% 的 API 查询都可以简单实现。

我们来看一个复杂点的例子,这是一个 and 条件更多、distinct or 排序、忽略大小写的例子。下面代码定义了 PersonRepository,我们可以在 service 层直接使用,如下所示:

interface PersonRepository extends Repository<User, Long> {
   // and 的查询关系
   List<User> findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname);
   // 包含 distinct 去重,or 的 sql 语法
   List<User> findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname);
   // 根据 lastname 字段查询忽略大小写
   List<User> findByLastnameIgnoreCase(String lastname);
   // 根据 lastname 和 firstname 查询 equal 并且忽略大小写
   List<User> findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname); 
  // 对查询结果根据 lastname 排序,正序
   List<User> findByLastnameOrderByFirstnameAsc(String lastname);
  // 对查询结果根据 lastname 排序,倒序
   List<User> findByLastnameOrderByFirstnameDesc(String lastname);
}

interface PersonRepository extends Repository<User, Long> {
   // and 的查询关系
   List<User> findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname);
   // 包含 distinct 去重,or 的 sql 语法
   List<User> findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname);
   // 根据 lastname 字段查询忽略大小写
   List<User> findByLastnameIgnoreCase(String lastname);
   // 根据 lastname 和 firstname 查询 equal 并且忽略大小写
   List<User> findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname); 
  // 对查询结果根据 lastname 排序,正序
   List<User> findByLastnameOrderByFirstnameAsc(String lastname);
  // 对查询结果根据 lastname 排序,倒序
   List<User> findByLastnameOrderByFirstnameDesc(String lastname);
}

下面表格是一个我们在上面 DQM 方法语法里常用的关键字列表,方便你快速查阅,并满足在实际代码中更加复杂的场景:

数据库类型为money在java里面调用是什么数据类型_java_13

综上,总结 3 点经验:

  • 方法名的表达式通常是实体属性连接运算符的组合,如 And、or、Between、LessThan、GreaterThan、Like 等属性连接运算表达式,不同的数据库(NoSQL、MySQL)可能产生的效果不一样,如果遇到问题,我们可以打开 SQL 日志观察。
  • IgnoreCase 可以针对单个属性(如 findByLastnameIgnoreCase(…)),也可以针对查询条件里面所有的实体属性忽略大小写(所有属性必须在 String 情况下,如 findByLastnameAndFirstnameAllIgnoreCase(…))。
  • OrderBy 可以在某些属性的排序上提供方向(Asc 或 Desc),称为静态排序,也可以通过一个方便的参数 Sort 实现指定字段的动态排序的查询方法(如 repository.findAll(Sort.by(Sort.Direction.ASC, "myField")))。

我们看到上面的表格虽然大多是 find 开头的方法,除此之外,JPA 还支持read、get、query、stream、count、exists、delete、remove等前缀,如字面意思一样。我们来看看 count、delete、remove 的例子,其他前缀可以举一反三。实例代码如下:

interface UserRepository extends CrudRepository<User, Long> {
     long countByLastname(String lastname);//查询总数
     long deleteByLastname(String lastname);//根据一个字段进行删除操作,并返回删除行数
     List<User> removeByLastname(String lastname);//根据Lastname删除一堆User,并返回删除的User
}

interface UserRepository extends CrudRepository<User, Long> {
     long countByLastname(String lastname);//查询总数
     long deleteByLastname(String lastname);//根据一个字段进行删除操作,并返回删除行数
     List<User> removeByLastname(String lastname);//根据Lastname删除一堆User,并返回删除的User
}

有的时候随着版本的更新,也会有更多的语法支持,或者不同的版本语法可能也不一样,我们通过源码来看一下上面说的几种语法。感兴趣的同学可以到类 org.springframework.data.repository.query.parser.PartTree 查看相关源码的逻辑和处理方法,关键源码如下:

数据库类型为money在java里面调用是什么数据类型_JPA_14

根据源码我们也可以分析出来,query method 包含其他的表达式,比如 find、count、delete、exist 等关键字在 by 之前通过正则表达式匹配。

数据库类型为money在java里面调用是什么数据类型_java_15

由此可知,我们方法中的关键字不是乱填的,是枚举帮我们定义好的。接下来打开枚举类 Type 源码看下,比什么都清楚。

public static enum Type {
    BETWEEN(2, new String[]{"IsBetween", "Between"}),
    IS_NOT_NULL(0, new String[]{"IsNotNull", "NotNull"}),
    IS_NULL(0, new String[]{"IsNull", "Null"}),
    LESS_THAN(new String[]{"IsLessThan", "LessThan"}),
    LESS_THAN_EQUAL(new String[]{"IsLessThanEqual", "LessThanEqual"}),
    GREATER_THAN(new String[]{"IsGreaterThan", "GreaterThan"}),
    GREATER_THAN_EQUAL(new String[]{"IsGreaterThanEqual", "GreaterThanEqual"}),
    BEFORE(new String[]{"IsBefore", "Before"}),
    AFTER(new String[]{"IsAfter", "After"}),
    NOT_LIKE(new String[]{"IsNotLike", "NotLike"}),
    LIKE(new String[]{"IsLike", "Like"}),
    STARTING_WITH(new String[]{"IsStartingWith", "StartingWith", "StartsWith"}),
    ENDING_WITH(new String[]{"IsEndingWith", "EndingWith", "EndsWith"}),
    IS_NOT_EMPTY(0, new String[]{"IsNotEmpty", "NotEmpty"}),
    IS_EMPTY(0, new String[]{"IsEmpty", "Empty"}),
    NOT_CONTAINING(new String[]{"IsNotContaining", "NotContaining", "NotContains"}),
    CONTAINING(new String[]{"IsContaining", "Containing", "Contains"}),
    NOT_IN(new String[]{"IsNotIn", "NotIn"}),
    IN(new String[]{"IsIn", "In"}),
    NEAR(new String[]{"IsNear", "Near"}),
    WITHIN(new String[]{"IsWithin", "Within"}),
    REGEX(new String[]{"MatchesRegex", "Matches", "Regex"}),
    EXISTS(0, new String[]{"Exists"}),
    TRUE(0, new String[]{"IsTrue", "True"}),
    FALSE(0, new String[]{"IsFalse", "False"}),
    NEGATING_SIMPLE_PROPERTY(new String[]{"IsNot", "Not"}),
    SIMPLE_PROPERTY(new String[]{"Is", "Equals"});
....}

public static enum Type {
    BETWEEN(2, new String[]{"IsBetween", "Between"}),
    IS_NOT_NULL(0, new String[]{"IsNotNull", "NotNull"}),
    IS_NULL(0, new String[]{"IsNull", "Null"}),
    LESS_THAN(new String[]{"IsLessThan", "LessThan"}),
    LESS_THAN_EQUAL(new String[]{"IsLessThanEqual", "LessThanEqual"}),
    GREATER_THAN(new String[]{"IsGreaterThan", "GreaterThan"}),
    GREATER_THAN_EQUAL(new String[]{"IsGreaterThanEqual", "GreaterThanEqual"}),
    BEFORE(new String[]{"IsBefore", "Before"}),
    AFTER(new String[]{"IsAfter", "After"}),
    NOT_LIKE(new String[]{"IsNotLike", "NotLike"}),
    LIKE(new String[]{"IsLike", "Like"}),
    STARTING_WITH(new String[]{"IsStartingWith", "StartingWith", "StartsWith"}),
    ENDING_WITH(new String[]{"IsEndingWith", "EndingWith", "EndsWith"}),
    IS_NOT_EMPTY(0, new String[]{"IsNotEmpty", "NotEmpty"}),
    IS_EMPTY(0, new String[]{"IsEmpty", "Empty"}),
    NOT_CONTAINING(new String[]{"IsNotContaining", "NotContaining", "NotContains"}),
    CONTAINING(new String[]{"IsContaining", "Containing", "Contains"}),
    NOT_IN(new String[]{"IsNotIn", "NotIn"}),
    IN(new String[]{"IsIn", "In"}),
    NEAR(new String[]{"IsNear", "Near"}),
    WITHIN(new String[]{"IsWithin", "Within"}),
    REGEX(new String[]{"MatchesRegex", "Matches", "Regex"}),
    EXISTS(0, new String[]{"Exists"}),
    TRUE(0, new String[]{"IsTrue", "True"}),
    FALSE(0, new String[]{"IsFalse", "False"}),
    NEGATING_SIMPLE_PROPERTY(new String[]{"IsNot", "Not"}),
    SIMPLE_PROPERTY(new String[]{"Is", "Equals"});
....}

看源码就可以知道框架支持了哪些逻辑关键字,比如 NotIn、Like、In、Exists 等,有的时候比查文档和任何人写的博客都准确、还快。好了,上面介绍了方面名的基本表达方式,希望你可以在工作中灵活运用,举一反三。接下来我们讲讲特定类型的参数:Sort 排序和 Pageable 分页,这是分页和排序必备技能。

特定类型的参数:Sort 排序和 Pageable 分页

Spring Data JPA 为了方便我们排序和分页,支持了两个特殊类型的参数:Sort 和 Pageable。

Sort 在查询的时候可以实现动态排序,我们看下其源码:

public Sort(Direction direction, String... properties) {
   this(direction, properties == null ? new ArrayList<>() : Arrays.asList(properties));
}

public Sort(Direction direction, String... properties) {
   this(direction, properties == null ? new ArrayList<>() : Arrays.asList(properties));
}

Sort 里面决定了我们哪些字段的排序方向(ASC 正序、DESC 倒序)。
Pageable 在查询的时候可以实现分页效果和动态排序双重效果,我们看下 Pageable 的 Structure,如下图所示:

数据库类型为money在java里面调用是什么数据类型_JPA_16

我们发现 Pageable 是一个接口,里面有常见的分页方法排序、当前页、下一行、当前指针、一共多少页、页码、pageSize 等。

在查询方法中如何使用 Pageable 和 Sort 呢?下面代码定义了根据 Lastname 查询 User 的分页和排序的实例,此段代码是在 UserRepository 接口里面定义的方法:

Page<User> findByLastname(String lastname, Pageable pageable);//根据分页参数查询User,返回一个带分页结果的Page(下一课时详解)对象(方法一)
Slice<User> findByLastname(String lastname, Pageable pageable);//我们根据分页参数返回一个Slice的user结果(方法二)
List<User> findByLastname(String lastname, Sort sort);//根据排序结果返回一个List(方法三)
List<User> findByLastname(String lastname, Pageable pageable);//根据分页参数返回一个List对象(方法四)

Page<User> findByLastname(String lastname, Pageable pageable);//根据分页参数查询User,返回一个带分页结果的Page(下一课时详解)对象(方法一)
Slice<User> findByLastname(String lastname, Pageable pageable);//我们根据分页参数返回一个Slice的user结果(方法二)
List<User> findByLastname(String lastname, Sort sort);//根据排序结果返回一个List(方法三)
List<User> findByLastname(String lastname, Pageable pageable);//根据分页参数返回一个List对象(方法四)

方法一:允许将 org.springframework.data.domain.Pageable 实例传递给查询方法,将分页参数添加到静态定义的查询中,通过 Page 返回的结果得知可用的元素和页面的总数。这种分页查询方法可能是昂贵的(会默认执行一条 count 的 SQL 语句),所以用的时候要考虑一下使用场景。

方法二:返回结果是 Slice,因为只知道是否有下一个 Slice 可用,而不知道 count,所以当查询较大的结果集时,只知道数据是足够的,也就是说用在业务场景中时不用关心一共有多少页。

方法三:如果只需要排序,需在 org.springframework.data.domain.Sort 参数中添加一个参数,正如上面看到的,只需返回一个 List 也是有可能的。

方法四:排序选项也通过 Pageable 实例处理,在这种情况下,Page 将不会创建构建实际实例所需的附加元数据(即不需要计算和查询分页相关数据),而仅仅用来做限制查询给定范围的实体。

那么如何使用呢?我们再来看一下源码,也就是 Pageable 的实现类,如下图所示:

数据库类型为money在java里面调用是什么数据类型_java_17

由此可知,我们可以通过 PageRequest 里面提供的几个 of 静态方法(多态),分别构建页码、页面大小、排序等。我们来看下,在使用中的写法,如下所示:

//查询user里面的lastname=jk的第一页,每页大小是20条;并会返回一共有多少页的信息
Page<User> users = userRepository.findByLastname("jk",PageRequest.of(1, 20));
//查询user里面的lastname=jk的第一页的20条数据,不知道一共多少条
Slice<User> users = userRepository.findByLastname("jk",PageRequest.of(1, 20));
//查询出来所有的user里面的lastname=jk的User数据,并按照name正序返回List
List<User> users = userRepository.findByLastname("jk",new Sort(Sort.Direction.ASC, "name"))
//按照createdAt倒序,查询前一百条User数据
List<User> users = userRepository.findByLastname("jk",PageRequest.of(0, 100, Sort.Direction.DESC, "createdAt"));

//查询user里面的lastname=jk的第一页,每页大小是20条;并会返回一共有多少页的信息
Page<User> users = userRepository.findByLastname("jk",PageRequest.of(1, 20));
//查询user里面的lastname=jk的第一页的20条数据,不知道一共多少条
Slice<User> users = userRepository.findByLastname("jk",PageRequest.of(1, 20));
//查询出来所有的user里面的lastname=jk的User数据,并按照name正序返回List
List<User> users = userRepository.findByLastname("jk",new Sort(Sort.Direction.ASC, "name"))
//按照createdAt倒序,查询前一百条User数据
List<User> users = userRepository.findByLastname("jk",PageRequest.of(0, 100, Sort.Direction.DESC, "createdAt"));

上面讲解了分页和排序的应用场景,在实际工作中,如果遇到不知道参数怎么传递的情况,可以看一下源码,因为 Java 是类型安全的。接下来讲解“限制查询结果 First 和 Top”,这是分页的另一种表达方式。

限制查询结果 First 和 Top

有的时候我们想直接查询前几条数据,也不需要动态排序,那么就可以简单地在方法名字中使用 First 和 Top 关键字,来限制返回条数。

我们来看看 userRepository 里面可以定义的一些限制返回结果的使用。在查询方法上加限制查询结果的关键字 First 和 Top。

User findFirstByOrderByLastnameAsc();
User findTopByOrderByAgeDesc();
List<User> findDistinctUserTop3ByLastname(String lastname, Pageable pageable);
List<User> findFirst10ByLastname(String lastname, Sort sort);
List<User> findTop10ByLastname(String lastname, Pageable pageable);

User findFirstByOrderByLastnameAsc();
User findTopByOrderByAgeDesc();
List<User> findDistinctUserTop3ByLastname(String lastname, Pageable pageable);
List<User> findFirst10ByLastname(String lastname, Sort sort);
List<User> findTop10ByLastname(String lastname, Pageable pageable);

其中:

  • 查询方法在使用 First 或 Top 时,数值可以追加到 First 或 Top 后面,指定返回最大结果的大小;
  • 如果数字被省略,则假设结果大小为 1;
  • 限制表达式也支持 Distinct 关键字;
  • 支持将结果包装到 Optional 中(下一课时详解)。
  • 如果将 Pageable 作为参数,以 Top 和 First 后面的数字为准,即分页将在限制结果中应用。

First 和 Top 关键字的使用非常简单,可以让我们的方法名语义更加清晰。接下来讲讲 NULL 的情况作了哪些支持。

@NonNull@NonNullApi@Nullable

从 Spring Data 2.0 开始,JPA 新增了@NonNull @NonNullApi @Nullable,是对 null 的参数和返回结果做的支持。

  • @NonNullApi:在包级别用于声明参数,以及返回值的默认行为是不接受或产生空值的。
  • @NonNull:用于不能为空的参数或返回值(在 @NonNullApi 适用的参数和返回值上不需要)。
  • @Nullable:用于可以为空的参数或返回值。

我在自己的 Repository 所在 package 的 package-info.java 类里面做如下声明:

@org.springframework.lang.NonNullApi
package com.myrespository;

@org.springframework.lang.NonNullApi
package com.myrespository;

myrespository 下面的 UserRepository 实现如下:

package com.myrespository;
import org.springframework.lang.Nullable;
interface UserRepository extends Repository<User, Long> {
  User getByEmailAddress(EmailAddress emailAddress); 
}

package com.myrespository;
import org.springframework.lang.Nullable;
interface UserRepository extends Repository<User, Long> {
  User getByEmailAddress(EmailAddress emailAddress); 
}

这个时候当 emailAddress 参数为 null 的时候就会抛异常,当返回结果为 null 的时候也会抛异常。因为我们在package 的 package-info.java里面指定了NonNullApi,所有返回结果和参数不能为 Null。

@Nullable
  User findByEmailAddress(@Nullable EmailAddress emailAdress);//当我们添加@Nullable 注解之后,参数和返回结果这个时候就都会允许为 null 了;
  Optional<User> findOptionalByEmailAddress(EmailAddress emailAddress); //返回结果允许为 null,参数不允许为 null 的情况

  @Nullable
  User findByEmailAddress(@Nullable EmailAddress emailAdress);//当我们添加@Nullable 注解之后,参数和返回结果这个时候就都会允许为 null 了;
  Optional<User> findOptionalByEmailAddress(EmailAddress emailAddress); //返回结果允许为 null,参数不允许为 null 的情况

以上就是对 Defining Query Methods 的方法名和分页参数整体学习了。

给我们的一些思考

我们学习了 Defining Query Methods 的语法和其所表达的命名规范,在实际工作中,也可以将方法名(非常语义化的 respository 里面所定义方法命名规范)的强制约定规范运用到 controller 和 service 层,这样全部统一后,可以减少很多的沟通成本。

Spring Data Common 里面的 repository 基类,我们是否可以应用推广到 service 层呢?能否也建立一个自己的 baseService?我们来看下面的实战例子:

public interface BaseService<T, ID> {
    Class<T> getDomainClass();
    <S extends T> S save(S entity);
    <S extends T> List<S> saveAll(Iterable<S> entities);
    void delete(T entity);
    void deleteById(ID id);
    void deleteAll();
    void deleteAll(Iterable<? extends T> entities);
    void deleteInBatch(Iterable<T> entities);
    void deleteAllInBatch();
    T getOne(ID id);
    <S extends T> Optional<S> findOne(Example<S> example);
    Optional<T> findById(ID id);
    List<T> findAll();
    List<T> findAll(Sort sort);
    Page<T> findAll(Pageable pageable);
    <S extends T> List<S> findAll(Example<S> example);
    <S extends T> List<S> findAll(Example<S> example, Sort sort);
    <S extends T> Page<S> findAll(Example<S> example, Pageable pageable);
    List<T> findAllById(Iterable<ID> ids);
    long count();
    <S extends T> long count(Example<S> example);
    <S extends T> boolean exists(Example<S> example);
    boolean existsById(ID id);
    void flush();
    <S extends T> S saveAndFlush(S entity);
}

public interface BaseService<T, ID> {
    Class<T> getDomainClass();
    <S extends T> S save(S entity);
    <S extends T> List<S> saveAll(Iterable<S> entities);
    void delete(T entity);
    void deleteById(ID id);
    void deleteAll();
    void deleteAll(Iterable<? extends T> entities);
    void deleteInBatch(Iterable<T> entities);
    void deleteAllInBatch();
    T getOne(ID id);
    <S extends T> Optional<S> findOne(Example<S> example);
    Optional<T> findById(ID id);
    List<T> findAll();
    List<T> findAll(Sort sort);
    Page<T> findAll(Pageable pageable);
    <S extends T> List<S> findAll(Example<S> example);
    <S extends T> List<S> findAll(Example<S> example, Sort sort);
    <S extends T> Page<S> findAll(Example<S> example, Pageable pageable);
    List<T> findAllById(Iterable<ID> ids);
    long count();
    <S extends T> long count(Example<S> example);
    <S extends T> boolean exists(Example<S> example);
    boolean existsById(ID id);
    void flush();
    <S extends T> S saveAndFlush(S entity);
}

我们模仿JpaRepository接口也自定义了一个自己的BaseService,声明了常用的CRUD操作,上面的代码是生产代码,可以作为参考。当然了我们也可以建立自己的 PagingAndSortingService、ComplexityService、SampleService 等来划分不同的 service接口,供不同目的 Service 子类继承。

我们再来模仿一个 SimpleJpaRepository,来实现自己的 BaseService 的实现类。

public class BaseServiceImpl<T, ID, R extends JpaRepository<T, ID>> implements BaseService<T, ID> {
    private static final Map<Class, Class> DOMAIN_CLASS_CACHE = new ConcurrentHashMap<>();
    private final R repository;
    public BaseServiceImpl(R repository) {
        this.repository = repository;
    }
    @Override
    public Class<T> getDomainClass() {
        Class thisClass = getClass();
        Class<T> domainClass = DOMAIN_CLASS_CACHE.get(thisClass);
        if (Objects.isNull(domainClass)) {
            domainClass = GenericsUtils.getGenericClass(thisClass, 0);
            DOMAIN_CLASS_CACHE.putIfAbsent(thisClass, domainClass);
        }
        return domainClass;
    }
    protected R getRepository() {
        return repository;
    }
    @Override
    public <S extends T> S save(S entity) {
        return repository.save(entity);
    }
    @Override
    public <S extends T> List<S> saveAll(Iterable<S> entities) {
        return repository.saveAll(entities);
    }
    @Override
    public void delete(T entity) {
        repository.delete(entity);
    }
    @Override
    public void deleteById(ID id) {
        repository.deleteById(id);
    }
    @Override
    public void deleteAll() {
        repository.deleteAll();
    }
    @Override
    public void deleteAll(Iterable<? extends T> entities) {
        repository.deleteAll(entities);
    }
    @Override
    public void deleteInBatch(Iterable<T> entities) {
        repository.deleteInBatch(entities);
    }
    @Override
    public void deleteAllInBatch() {
        repository.deleteAllInBatch();
    }
    @Override
    public T getOne(ID id) {
        return repository.getOne(id);
    }
    @Override
    public <S extends T> Optional<S> findOne(Example<S> example) {
        return repository.findOne(example);
    }
    @Override
    public Optional<T> findById(ID id) {
        return repository.findById(id);
    }
    @Override
    public List<T> findAll() {
        return repository.findAll();
    }
    @Override
    public List<T> findAll(Sort sort) {
        return repository.findAll(sort);
    }
    @Override
    public Page<T> findAll(Pageable pageable) {
        return repository.findAll(pageable);
    }
    @Override
    public <S extends T> List<S> findAll(Example<S> example) {
        return repository.findAll(example);
    }
    @Override
    public <S extends T> List<S> findAll(Example<S> example, Sort sort) {
        return repository.findAll(example, sort);
    }
    @Override
    public <S extends T> Page<S> findAll(Example<S> example, Pageable pageable) {
        return repository.findAll(example, pageable);
    }
    @Override
    public List<T> findAllById(Iterable<ID> ids) {
        return repository.findAllById(ids);
    }
    @Override
    public long count() {
        return repository.count();
    }
    @Override
    public <S extends T> long count(Example<S> example) {
        return repository.count(example);
    }
    @Override
    public <S extends T> boolean exists(Example<S> example) {
        return repository.exists(example);
    }
    @Override
    public boolean existsById(ID id) {
        return repository.existsById(id);
    }
    @Override
    public void flush() {
        repository.flush();
    }
    @Override
    public <S extends T> S saveAndFlush(S entity) {
        return repository.saveAndFlush(entity);
    }
}

public class BaseServiceImpl<T, ID, R extends JpaRepository<T, ID>> implements BaseService<T, ID> {
    private static final Map<Class, Class> DOMAIN_CLASS_CACHE = new ConcurrentHashMap<>();
    private final R repository;
    public BaseServiceImpl(R repository) {
        this.repository = repository;
    }
    @Override
    public Class<T> getDomainClass() {
        Class thisClass = getClass();
        Class<T> domainClass = DOMAIN_CLASS_CACHE.get(thisClass);
        if (Objects.isNull(domainClass)) {
            domainClass = GenericsUtils.getGenericClass(thisClass, 0);
            DOMAIN_CLASS_CACHE.putIfAbsent(thisClass, domainClass);
        }
        return domainClass;
    }
    protected R getRepository() {
        return repository;
    }
    @Override
    public <S extends T> S save(S entity) {
        return repository.save(entity);
    }
    @Override
    public <S extends T> List<S> saveAll(Iterable<S> entities) {
        return repository.saveAll(entities);
    }
    @Override
    public void delete(T entity) {
        repository.delete(entity);
    }
    @Override
    public void deleteById(ID id) {
        repository.deleteById(id);
    }
    @Override
    public void deleteAll() {
        repository.deleteAll();
    }
    @Override
    public void deleteAll(Iterable<? extends T> entities) {
        repository.deleteAll(entities);
    }
    @Override
    public void deleteInBatch(Iterable<T> entities) {
        repository.deleteInBatch(entities);
    }
    @Override
    public void deleteAllInBatch() {
        repository.deleteAllInBatch();
    }
    @Override
    public T getOne(ID id) {
        return repository.getOne(id);
    }
    @Override
    public <S extends T> Optional<S> findOne(Example<S> example) {
        return repository.findOne(example);
    }
    @Override
    public Optional<T> findById(ID id) {
        return repository.findById(id);
    }
    @Override
    public List<T> findAll() {
        return repository.findAll();
    }
    @Override
    public List<T> findAll(Sort sort) {
        return repository.findAll(sort);
    }
    @Override
    public Page<T> findAll(Pageable pageable) {
        return repository.findAll(pageable);
    }
    @Override
    public <S extends T> List<S> findAll(Example<S> example) {
        return repository.findAll(example);
    }
    @Override
    public <S extends T> List<S> findAll(Example<S> example, Sort sort) {
        return repository.findAll(example, sort);
    }
    @Override
    public <S extends T> Page<S> findAll(Example<S> example, Pageable pageable) {
        return repository.findAll(example, pageable);
    }
    @Override
    public List<T> findAllById(Iterable<ID> ids) {
        return repository.findAllById(ids);
    }
    @Override
    public long count() {
        return repository.count();
    }
    @Override
    public <S extends T> long count(Example<S> example) {
        return repository.count(example);
    }
    @Override
    public <S extends T> boolean exists(Example<S> example) {
        return repository.exists(example);
    }
    @Override
    public boolean existsById(ID id) {
        return repository.existsById(id);
    }
    @Override
    public void flush() {
        repository.flush();
    }
    @Override
    public <S extends T> S saveAndFlush(S entity) {
        return repository.saveAndFlush(entity);
    }
}

以上代码就是 BaseService 常用的 CURD 实现代码,我们这里面大部分也是直接调用 Repository 提供的方法。需要注意的是,当继承 BaseServiceImpl 的时候需要传递自己的 Repository,如下面实例代码:

@Service
public class UserServiceImpl extends BaseServiceImpl<User, Long, UserRepository> implements UserService {
    public UserServiceImpl(UserRepository repository) {
        super(repository);
    }
    .....
}

@Service
public class UserServiceImpl extends BaseServiceImpl<User, Long, UserRepository> implements UserService {
    public UserServiceImpl(UserRepository repository) {
        super(repository);
    }
    .....
}

实战思考只是提供一种常见的实现思路,你也可以根据实际情况进行扩展和扩充。

总结

本课时主要讲解了 Defining Query Methods 的语法和参数部分的内容。首先介绍了配置方法,其次讲解了 DQM 语法结构所支持的关键字和特殊参数类型,最后对分页和 Null 做了特殊说明。通过本课时的学习,希望你可以轻松掌握 DQM 的方法名和参数的精髓所在,下一课时将会重点介绍 DQM 的返回结果有哪些支持,及其实现原理和实战应用场景,到时见~

这里留个思考题:如何返回自定义 DTO 而不是 Entity?

有思想,有方法,有技巧,有源码。如果觉得有帮助,就动动手指分享吧!同时也欢迎你在留言区发表学习感悟,大家一起更好地成长。

学会看源码,逐步从入门到精通,提高学习效率。此种学习方法,可以应用在任何需要学习的框架里面。

点击下方链接查看源码(不定时更新)
https://github.com/zhangzhenhuajack/spring-boot-guide/tree/master/spring-data/spring-data-jpa


04 如何利用 Repoitory 中的方法返回值解决实际问题?

上一课时,我们着重讲了方法名和参数的使用方法,这一课时我们来看下Repository 支持的返回结果有哪些,以及 DTO 类型的返回结果如何自定义,及其在实际工作场景中我们如何做。通过本课时的学习,你将了解到 Repository 的几种返回结果,以及如何返回 DTO。我们先看一下返回结果有哪些。

Repository 的返回结果有哪些?

我们之前已经介绍过了 Repository 的接口,那么现在来看一下这些接口支持的返回结果有哪些,如下图所示:

数据库类型为money在java里面调用是什么数据类型_java_18

打开 SimpleJpaRepository 直接看它的 Structure 就可以知道,它实现的方法,以及父类接口的方法和返回类型包括:Optional、Iterable、List、Page、Long、Boolean、Entity 对象等,而实际上支持的返回类型还要多一些。

由于 Repository 里面支持 Iterable,所以其实 java 标准的 List、Set 都可以作为返回结果,并且也会支持其子类,Spring Data 里面定义了一个特殊的子类 Steamable,Streamable 可以替代 Iterable 或任何集合类型。它还提供了方便的方法来访问 Stream,可以直接在元素上进行 ….filter(…) 和 ….map(…) 操作,并将 Streamable 连接到其他元素。我们看个关于 UserRepository 直接继承 JpaRepository 的例子。

public interface UserRepository extends JpaRepository<User,Long> {
}

public interface UserRepository extends JpaRepository<User,Long> {
}

还用之前的 UserRepository 类,在测试类里面做如下调用:

User user = userRepository.save(User.builder().name("jackxx").email("123456@126.com").sex("man").address("shanghai").build());
Assert.assertNotNull(user);
Streamable<User> userStreamable = userRepository.findAll(PageRequest.of(0,10)).and(User.builder().name("jack222").build());
userStreamable.forEach(System.out::println);

User user = userRepository.save(User.builder().name("jackxx").email("123456@126.com").sex("man").address("shanghai").build());
Assert.assertNotNull(user);
Streamable<User> userStreamable = userRepository.findAll(PageRequest.of(0,10)).and(User.builder().name("jack222").build());
userStreamable.forEach(System.out::println);

然后我们就会得到如下输出:

User(id=1, name=jackxx, email=123456@126.com, sex=man, address=shanghai)
User(id=null, name=jack222, email=null, sex=null, address=null)

User(id=1, name=jackxx, email=123456@126.com, sex=man, address=shanghai)
User(id=null, name=jack222, email=null, sex=null, address=null)

这个例子 Streamable<User> userStreamable,实现了 Streamable 的返回结果,如果想自定义方法,可以进行如下操作。

自定义 Streamable

官方给我们提供了自定义 Streamable 的方法,不过在实际工作中很少出现要自定义保证结果类的情况,在这里我简单介绍一下方法,看如下例子:

class Product { (1)
  MonetaryAmount getPrice() { … }
}
@RequiredArgConstructor(staticName = "of")
class Products implements Streamable<Product> { (2)
  private Streamable<Product> streamable;
  public MonetaryAmount getTotal() { (3)
    return streamable.stream() //
      .map(Priced::getPrice)
      .reduce(Money.of(0), MonetaryAmount::add);
  }
}
interface ProductRepository implements Repository<Product, Long> {
  Products findAllByDescriptionContaining(String text); (4)
}

class Product { (1)
  MonetaryAmount getPrice() { … }
}
@RequiredArgConstructor(staticName = "of")
class Products implements Streamable<Product> { (2)
  private Streamable<Product> streamable;
  public MonetaryAmount getTotal() { (3)
    return streamable.stream() //
      .map(Priced::getPrice)
      .reduce(Money.of(0), MonetaryAmount::add);
  }
}
interface ProductRepository implements Repository<Product, Long> {
  Products findAllByDescriptionContaining(String text); (4)
}

以上四个步骤介绍了自定义 Streamable 的方法,分别为:

(1)Product 实体,公开 API 以访问产品价格。

(2)Streamable<Product> 的包装类型可以通过 Products.of(…) 构造(通过 Lombok 注解创建的工厂方法)。

(3)包装器类型在 Streamable<Product> 上公开了计算新值的其他 API。

(4)可以将包装器类型直接用作查询方法返回类型。无须返回 Stremable<Product> 并将其手动包装在存储库 Client 端中。

通过以上例子你就可以做到自定义 Streamable,其原理很简单,就是实现Streamable接口,自己定义自己的实现类即可。我们也可以看下源码 QueryExecutionResultHandler 里面是否有 Streamable 子类的判断,来支持自定义 Streamable,关键源码如下:

数据库类型为money在java里面调用是什么数据类型_java_19

通过源码你会发现 Streamable 为什么生效,下面来看看常见的集合类的返回实现。

返回结果类型 List/Stream/Page/Slice

在实际开发中,我们如何返回 List/Stream/Page/Slice 呢?

首先,新建我们的 UserRepository:

package com.example.jpa.example1;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import java.util.stream.Stream;
public interface UserRepository extends JpaRepository<User,Long> {
   //自定义一个查询方法,返回Stream对象,并且有分页属性
    @Query("select u from User u")
    Stream<User> findAllByCustomQueryAndStream(Pageable pageable);
    //测试Slice的返回结果
    @Query("select u from User u")
    Slice<User> findAllByCustomQueryAndSlice(Pageable pageable);
}

package com.example.jpa.example1;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import java.util.stream.Stream;
public interface UserRepository extends JpaRepository<User,Long> {
   //自定义一个查询方法,返回Stream对象,并且有分页属性
    @Query("select u from User u")
    Stream<User> findAllByCustomQueryAndStream(Pageable pageable);
    //测试Slice的返回结果
    @Query("select u from User u")
    Slice<User> findAllByCustomQueryAndSlice(Pageable pageable);
}

然后,修改一下我们的测试用例类,如下,验证一下结果:

package com.example.jpa.example1;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.assertj.core.util.Lists;
import org.junit.Assert;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Slice;
import org.springframework.data.util.Streamable;
import java.util.List;
import java.util.stream.Stream;
@DataJpaTest
public class UserRepositoryTest {
    @Autowired
    private UserRepository userRepository;
    @Test
    public void testSaveUser() throws JsonProcessingException {
        //我们新增7条数据方便测试分页结果
        userRepository.save(User.builder().name("jack1").email("123456@126.com").sex("man").address("shanghai").build());
        userRepository.save(User.builder().name("jack2").email("123456@126.com").sex("man").address("shanghai").build());
        userRepository.save(User.builder().name("jack3").email("123456@126.com").sex("man").address("shanghai").build());
        userRepository.save(User.builder().name("jack4").email("123456@126.com").sex("man").address("shanghai").build());
        userRepository.save(User.builder().name("jack5").email("123456@126.com").sex("man").address("shanghai").build());
        userRepository.save(User.builder().name("jack6").email("123456@126.com").sex("man").address("shanghai").build());
        userRepository.save(User.builder().name("jack7").email("123456@126.com").sex("man").address("shanghai").build());
        //我们利用ObjectMapper将我们的返回结果Json to String
        ObjectMapper objectMapper = new ObjectMapper();
        //返回Stream类型结果(1)
        Stream<User> userStream = userRepository.findAllByCustomQueryAndStream(PageRequest.of(1,3));
        userStream.forEach(System.out::println);
        //返回分页数据(2)
        Page<User> userPage = userRepository.findAll(PageRequest.of(0,3));
        System.out.println(objectMapper.writeValueAsString(userPage));
        //返回Slice结果(3)
        Slice<User> userSlice = userRepository.findAllByCustomQueryAndSlice(PageRequest.of(0,3));
        System.out.println(objectMapper.writeValueAsString(userSlice));
        //返回List结果(4)
        List<User> userList = userRepository.findAllById(Lists.newArrayList(1L,2L));
        System.out.println(objectMapper.writeValueAsString(userList));
    }
}

package com.example.jpa.example1;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.assertj.core.util.Lists;
import org.junit.Assert;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Slice;
import org.springframework.data.util.Streamable;
import java.util.List;
import java.util.stream.Stream;
@DataJpaTest
public class UserRepositoryTest {
    @Autowired
    private UserRepository userRepository;
    @Test
    public void testSaveUser() throws JsonProcessingException {
        //我们新增7条数据方便测试分页结果
        userRepository.save(User.builder().name("jack1").email("123456@126.com").sex("man").address("shanghai").build());
        userRepository.save(User.builder().name("jack2").email("123456@126.com").sex("man").address("shanghai").build());
        userRepository.save(User.builder().name("jack3").email("123456@126.com").sex("man").address("shanghai").build());
        userRepository.save(User.builder().name("jack4").email("123456@126.com").sex("man").address("shanghai").build());
        userRepository.save(User.builder().name("jack5").email("123456@126.com").sex("man").address("shanghai").build());
        userRepository.save(User.builder().name("jack6").email("123456@126.com").sex("man").address("shanghai").build());
        userRepository.save(User.builder().name("jack7").email("123456@126.com").sex("man").address("shanghai").build());
        //我们利用ObjectMapper将我们的返回结果Json to String
        ObjectMapper objectMapper = new ObjectMapper();
        //返回Stream类型结果(1)
        Stream<User> userStream = userRepository.findAllByCustomQueryAndStream(PageRequest.of(1,3));
        userStream.forEach(System.out::println);
        //返回分页数据(2)
        Page<User> userPage = userRepository.findAll(PageRequest.of(0,3));
        System.out.println(objectMapper.writeValueAsString(userPage));
        //返回Slice结果(3)
        Slice<User> userSlice = userRepository.findAllByCustomQueryAndSlice(PageRequest.of(0,3));
        System.out.println(objectMapper.writeValueAsString(userSlice));
        //返回List结果(4)
        List<User> userList = userRepository.findAllById(Lists.newArrayList(1L,2L));
        System.out.println(objectMapper.writeValueAsString(userList));
    }
}

这个时候我们分别看下四种测试结果:
第一种:通过Stream<User>取第二页的数据,得到结果如下:

User(id=4, name=jack4, email=123456@126.com, sex=man, address=shanghai)

User(id=5, name=jack5, email=123456@126.com, sex=man, address=shanghai)

User(id=6, name=jack6, email=123456@126.com, sex=man, address=shanghai)

Spring Data 的支持可以通过使用 Java 8 Stream 作为返回类型来逐步处理查询方法的结果。需要注意的是:流的关闭问题,try catch 是一种常用的关闭方法,如下所示:

Stream<User> stream;
try {
   stream = repository.findAllByCustomQueryAndStream()
   stream.forEach(…);
} catch (Exception e) {
   e.printStackTrace();
} finally {
   if (stream!=null){
      stream.close();
   }
}

Stream<User> stream;
try {
   stream = repository.findAllByCustomQueryAndStream()
   stream.forEach(…);
} catch (Exception e) {
   e.printStackTrace();
} finally {
   if (stream!=null){
      stream.close();
   }
}

第二种:返回 Page<User> 的分页数据结果,如下所示:

{
   "content":[
      {
         "id":1,
         "name":"jack1",
         "email":"123456@126.com",
         "sex":"man",
         "address":"shanghai"
      },
      {
         "id":2,
         "name":"jack2",
         "email":"123456@126.com",
         "sex":"man",
         "address":"shanghai"
      },
      {
         "id":3,
         "name":"jack3",
         "email":"123456@126.com",
         "sex":"man",
         "address":"shanghai"
      }
   ],
   "pageable":{
      "sort":{
         "sorted":false,
         "unsorted":true,
         "empty":true
      },
      "pageNumber":0,当前页码
      "pageSize":3,页码大小
      "offset":0,偏移量
      "paged":true,是否分页了
      "unpaged":false
   },
   "totalPages":3,一共有多少页
   "last":false,是否是到最后
   "totalElements":7,一共多少调数
   "numberOfElements":3,当前数据下标
   "sort":{
      "sorted":false,
      "unsorted":true,
      "empty":true
   },
   "size":3,当前content大小
   "number":0,当前页面码的索引
   "first":true,是否是第一页
   "empty":false是否有数据
}

{
   "content":[
      {
         "id":1,
         "name":"jack1",
         "email":"123456@126.com",
         "sex":"man",
         "address":"shanghai"
      },
      {
         "id":2,
         "name":"jack2",
         "email":"123456@126.com",
         "sex":"man",
         "address":"shanghai"
      },
      {
         "id":3,
         "name":"jack3",
         "email":"123456@126.com",
         "sex":"man",
         "address":"shanghai"
      }
   ],
   "pageable":{
      "sort":{
         "sorted":false,
         "unsorted":true,
         "empty":true
      },
      "pageNumber":0,当前页码
      "pageSize":3,页码大小
      "offset":0,偏移量
      "paged":true,是否分页了
      "unpaged":false
   },
   "totalPages":3,一共有多少页
   "last":false,是否是到最后
   "totalElements":7,一共多少调数
   "numberOfElements":3,当前数据下标
   "sort":{
      "sorted":false,
      "unsorted":true,
      "empty":true
   },
   "size":3,当前content大小
   "number":0,当前页面码的索引
   "first":true,是否是第一页
   "empty":false是否有数据
}

这里我们可以看到 Page<User> 返回了第一个页的数据,并且告诉我们一共有三个部分的数据:

  • content:数据的内容,现在指 User 的 List 3 条。
  • pageable:分页数据,包括排序字段是什么及其方向、当前是第几页、一共多少页、是否是最后一条等。
  • 当前数据的描述:“size”:3,当前 content 大小;“number”:0,当前页面码的索引;  “first”:true,是否是第一页;“empty”:false,是否没有数据。

通过这三部分数据我们可以知道要查数的分页信息。我们接着看第三种测试结果。

第三种:返回 Slice<User> 结果,如下所示:

{
   "content":[
      {
         "id":4,
         "name":"jack4",
         "email":"123456@126.com",
         "sex":"man",
         "address":"shanghai"
      },
      {
         "id":5,
         "name":"jack5",
         "email":"123456@126.com",
         "sex":"man",
         "address":"shanghai"
      },
      {
         "id":6,
         "name":"jack6",
         "email":"123456@126.com",
         "sex":"man",
         "address":"shanghai"
      }
   ],
   "pageable":{
      "sort":{
         "sorted":false,
         "unsorted":true,
         "empty":true
      },
      "pageNumber":1,
      "pageSize":3,
      "offset":3,
      "paged":true,
      "unpaged":false
   },
   "numberOfElements":3,
   "sort":{
      "sorted":false,
      "unsorted":true,
      "empty":true
   },
   "size":3,
   "number":1,
   "first":false,
   "last":false,
   "empty":false
}

{
   "content":[
      {
         "id":4,
         "name":"jack4",
         "email":"123456@126.com",
         "sex":"man",
         "address":"shanghai"
      },
      {
         "id":5,
         "name":"jack5",
         "email":"123456@126.com",
         "sex":"man",
         "address":"shanghai"
      },
      {
         "id":6,
         "name":"jack6",
         "email":"123456@126.com",
         "sex":"man",
         "address":"shanghai"
      }
   ],
   "pageable":{
      "sort":{
         "sorted":false,
         "unsorted":true,
         "empty":true
      },
      "pageNumber":1,
      "pageSize":3,
      "offset":3,
      "paged":true,
      "unpaged":false
   },
   "numberOfElements":3,
   "sort":{
      "sorted":false,
      "unsorted":true,
      "empty":true
   },
   "size":3,
   "number":1,
   "first":false,
   "last":false,
   "empty":false
}

这时我们发现上面的 Page 返回结果少了,那么一共有多少条结果、多少页的数据呢?我们再比较一下第二种和第三种测试结果的执行 SQL:

第二种执行的是普通的分页查询 SQL:

查询分页数据
Hibernate: select user0_.id as id1_0_, user0_.address as address2_0_, user0_.email as email3_0_, user0_.name as name4_0_, user0_.sex as sex5_0_ from user user0_ limit ?
计算分页数据
Hibernate: select count(user0_.id) as col_0_0_ from user user0_

查询分页数据
Hibernate: select user0_.id as id1_0_, user0_.address as address2_0_, user0_.email as email3_0_, user0_.name as name4_0_, user0_.sex as sex5_0_ from user user0_ limit ?
计算分页数据
Hibernate: select count(user0_.id) as col_0_0_ from user user0_

第三种执行的 SQL 如下:

Hibernate: select user0_.id as id1_0_, user0_.address as address2_0_, user0_.email as email3_0_, user0_.name as name4_0_, user0_.sex as sex5_0_ from user user0_ limit ? offset ?

Hibernate: select user0_.id as id1_0_, user0_.address as address2_0_, user0_.email as email3_0_, user0_.name as name4_0_, user0_.sex as sex5_0_ from user user0_ limit ? offset ?

通过对比可以看出,只查询偏移量,不计算分页数据,这就是 Page 和 Slice 的主要区别。我们接着看第四种测试结果。

第四种:返回 List<User> 结果如下:

[
   {
      "id":1,
      "name":"jack1",
      "email":"123456@126.com",
      "sex":"man",
      "address":"shanghai"
   },
   {
      "id":2,
      "name":"jack2",
      "email":"123456@126.com",
      "sex":"man",
      "address":"shanghai"
   }
]

[
   {
      "id":1,
      "name":"jack1",
      "email":"123456@126.com",
      "sex":"man",
      "address":"shanghai"
   },
   {
      "id":2,
      "name":"jack2",
      "email":"123456@126.com",
      "sex":"man",
      "address":"shanghai"
   }
]

到这里,我们可以很简单地查询出来 ID=1 和 ID=2 的数据,没有分页信息。

上面四种方法介绍了常见的多条数据返回结果的形式,单条的我就不多介绍了,相信你一看就懂,无非就是对 JDK8 的 Optional 的支持。比如支持了 Null 的优雅判断,再一个就是支持直接返回 Entity,或者一些存在 / 不存在的 Boolean 的结果和一些 count 条数的返回结果而已。

我们接下来看下 Repository 的方法是如何对异步进行支持的?

Repository 对 Feature/CompletableFuture 异步返回结果的支持:

我们可以使用 Spring 的异步方法执行Repository查询,这意味着方法将在调用时立即返回,并且实际的查询执行将发生在已提交给 Spring TaskExecutor 的任务中,比较适合定时任务的实际场景。异步使用起来比较简单,直接加@Async 注解即可,如下所示:

@Async
Future<User> findByFirstname(String firstname); (1)
@Async
CompletableFuture<User> findOneByFirstname(String firstname); (2)
@Async
ListenableFuture<User> findOneByLastname(String lastname);(3)

@Async
Future<User> findByFirstname(String firstname); (1)
@Async
CompletableFuture<User> findOneByFirstname(String firstname); (2)
@Async
ListenableFuture<User> findOneByLastname(String lastname);(3)

上述三个异步方法的返回结果,分别做如下解释:

  • 第一处:使用 java.util.concurrent.Future 的返回类型;
  • 第二处:使用 java.util.concurrent.CompletableFuture 作为返回类型;
  • 第三处:使用 org.springframework.util.concurrent.ListenableFuture 作为返回类型。

以上是对 @Async 的支持,关于实际使用需要注意以下三点内容:

  • 在实际工作中,直接在 Repository 这一层使用异步方法的场景不多,一般都是把异步注解放在 Service 的方法上面,这样的话,可以有一些额外逻辑,如发短信、发邮件、发消息等配合使用;
  • 使用异步的时候一定要配置线程池,这点切记,否则“死”得会很难看;
  • 万一失败我们会怎么处理?关于事务是怎么处理的呢?这种需要重点考虑的,我将会在 14 课时(乐观锁机制和重试机制在实战中应该怎么用?)中详细介绍。

接下来看看 Repository 对Reactive 是如何支持的。

对 Reactive 支持 flux 与 Mono

可能有同学会问,看到Spring Data Common里面对React还是有支持的,那为什么在JpaRespository里面没看到有响应的返回结果支持呢?其实Common里面提供的只是接口,而JPA里面没有做相关的Reactive 的实现,但是本身Spring Data Common里面对 Reactive 是支持的。

下面我们在 gradle 里面引用一个Spring Data Common的子模块implementation 'org.springframework.boot:spring-boot-starter-data-mongodb' 来加载依赖,这时候我们打开 Repository 看 Hierarchy 就可以看到,这里多了一个 Mongo 的 Repsitory 的实现,天然地支持着 Reactive 这条线。

数据库类型为money在java里面调用是什么数据类型_spring boot_20

相信到这里你能感受到 Spring Data Common 的强大支持,对 Repository 接口的不同实现也有了一定的认识。对于以上讲述的返回结果,你可以自己测试一下加以理解并运用,那么接下来我们进行一个总结。

返回结果支持总结

下面打开 ResultProcessor 类的源码看一下支持的类型有哪些。

数据库类型为money在java里面调用是什么数据类型_Data_21

从上图可以看出 processResult 的时候分别对 PageQuery、Stream、Reactiv 有了各自的判断,我们 debug 到这里的时候来看一下 convert,进入到类里面。

数据库类型为money在java里面调用是什么数据类型_spring_22

可以看到 QueryExecutorConverters 里面对 JDK8、Guava、vavr 也做了各种支持,如果你有兴趣可以课后去仔细看看源码。

这里我们先用表格总结一下返回值,下表列出了 Spring Data JPA Query Method 机制支持的方法的返回值类型:

数据库类型为money在java里面调用是什么数据类型_Data_23

以上是对返回的类型做的总结,接下来进入本课时的第二部分,来看看工作中最常见的、同一个 Entity 的不同字段的返回形式有哪些。

最常见的 DTO 返回结果的支持方法有哪些?

上面我们讲解了 Repository 不同的返回类型,下面我们着重说一下除了 Entity,还能返回哪些 POJO 呢?我们先了解一个概念:Projections。

Projections 的概念

Spring JPA 对 Projections 扩展的支持,我个人觉得这是个非常好的东西,从字面意思上理解就是映射,指的是和 DB 的查询结果的字段映射关系。一般情况下,返回的字段和 DB 的查询结果的字段是一一对应的;但有的时候,需要返回一些指定的字段,或者返回一些复合型的字段,而不需要全部返回。

原来我们的做法是自己写各种 entity 到 view 的各种 convert 的转化逻辑,而 Spring Data 正是考虑到了这一点,允许对专用返回类型进行建模,有选择地返回同一个实体的不同视图对象。

下面还以我们的 User 查询对象为例,看看怎么自定义返回 DTO:

@Entity
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class User {
   @Id
   @GeneratedValue(strategy= GenerationType.AUTO)
   private Long id;
   private String name;
   private String email;
   private String sex;
   private String address;
}

@Entity
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class User {
   @Id
   @GeneratedValue(strategy= GenerationType.AUTO)
   private Long id;
   private String name;
   private String email;
   private String sex;
   private String address;
}

看上面的原始 User 实体代码,如果我们只想返回 User 对象里面的 name 和 email,应该怎么做?下面我们介绍三种方法。

第一种方法:新建一张表的不同 Entity

首先,我们新增一个Entity类:通过 @Table 指向同一张表,这张表和 User 实例里面的表一样都是 user,完整内容如下:

@Entity
@Table(name = "user")
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class UserOnlyNameEmailEntity {
   @Id
   @GeneratedValue(strategy= GenerationType.AUTO)
   private Long id;
   private String name;
   private String email;
}

@Entity
@Table(name = "user")
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class UserOnlyNameEmailEntity {
   @Id
   @GeneratedValue(strategy= GenerationType.AUTO)
   private Long id;
   private String name;
   private String email;
}

然后,新增一个 UserOnlyNameEmailEntityRepository,做单独的查询:

package com.example.jpa.example1;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserOnlyNameEmailEntityRepository extends JpaRepository<UserOnlyNameEmailEntity,Long> {
}

package com.example.jpa.example1;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserOnlyNameEmailEntityRepository extends JpaRepository<UserOnlyNameEmailEntity,Long> {
}

最后,我们的测试用例里面的写法如下:

@Test
public void testProjections() {
  userRepository.save(User.builder().id(1L).name("jack12").email("123456@126.com").sex("man").address("shanghai").build());
    List<User> users= userRepository.findAll();
    System.out.println(users);
    UserOnlyNameEmailEntity uName = userOnlyNameEmailEntityRepository.getOne(1L);
    System.out.println(uName);
}

@Test
public void testProjections() {
  userRepository.save(User.builder().id(1L).name("jack12").email("123456@126.com").sex("man").address("shanghai").build());
    List<User> users= userRepository.findAll();
    System.out.println(users);
    UserOnlyNameEmailEntity uName = userOnlyNameEmailEntityRepository.getOne(1L);
    System.out.println(uName);
}

我们看一下输出结果:

Hibernate: insert into user (address, email, name, sex, id) values (?, ?, ?, ?, ?)
Hibernate: select user0_.id as id1_0_, user0_.address as address2_0_, user0_.email as email3_0_, user0_.name as name4_0_, user0_.sex as sex5_0_ from user user0_
[User(id=1, name=jack12, email=123456@126.com, sex=man, address=shanghai)]
Hibernate: select useronlyna0_.id as id1_0_0_, useronlyna0_.email as email3_0_0_, useronlyna0_.name as name4_0_0_ from user useronlyna0_ where useronlyna0_.id=?
UserOnlyNameEmailEntity(id=1, name=jack12, email=123456@126.com)

Hibernate: insert into user (address, email, name, sex, id) values (?, ?, ?, ?, ?)
Hibernate: select user0_.id as id1_0_, user0_.address as address2_0_, user0_.email as email3_0_, user0_.name as name4_0_, user0_.sex as sex5_0_ from user user0_
[User(id=1, name=jack12, email=123456@126.com, sex=man, address=shanghai)]
Hibernate: select useronlyna0_.id as id1_0_0_, useronlyna0_.email as email3_0_0_, useronlyna0_.name as name4_0_0_ from user useronlyna0_ where useronlyna0_.id=?
UserOnlyNameEmailEntity(id=1, name=jack12, email=123456@126.com)

上述结果可以看到,当在 user 表里面插入了一条数据,而 userRepository 和 userOnlyNameEmailEntityRepository 查询的都是同一张表 user,这种方式的好处是简单、方便,很容易可以想到;缺点就是通过两个实体都可以进行 update 操作,如果同一个项目里面这种实体比较多,到时候就容易不知道是谁更新的,从而导致出 bug 不好查询,实体职责划分不明确。我们来看第二种返回 DTO 的做法。

第二种方法:直接定义一个 UserOnlyNameEmailDto

首先,我们新建一个 DTO 类来返回我们想要的字段,它是 UserOnlyNameEmailDto,用来接收 name、email 两个字段的值,具体如下:

@Data
@Builder
@AllArgsConstructor
public class UserOnlyNameEmailDto {
    private String name,email;
}

@Data
@Builder
@AllArgsConstructor
public class UserOnlyNameEmailDto {
    private String name,email;
}

其次,在 UserRepository 里面做如下用法:

public interface UserRepository extends JpaRepository<User,Long> {
    //测试只返回name和email的DTO
    UserOnlyNameEmailDto findByEmail(String email);
}

public interface UserRepository extends JpaRepository<User,Long> {
    //测试只返回name和email的DTO
    UserOnlyNameEmailDto findByEmail(String email);
}

然后,测试用例里面写法如下:

@Test
    public void testProjections() {
userRepository.save(User.builder().id(1L).name("jack12").email("123456@126.com").sex("man").address("shanghai").build());
        UserOnlyNameEmailDto userOnlyNameEmailDto =  userRepository.findByEmail("123456@126.com");
        System.out.println(userOnlyNameEmailDto);
    }

    @Test
    public void testProjections() {
userRepository.save(User.builder().id(1L).name("jack12").email("123456@126.com").sex("man").address("shanghai").build());
        UserOnlyNameEmailDto userOnlyNameEmailDto =  userRepository.findByEmail("123456@126.com");
        System.out.println(userOnlyNameEmailDto);
    }

最后,输出结果如下:

Hibernate: select user0_.name as col_0_0_, user0_.email as col_1_0_ from user user0_ where user0_.email=?
UserOnlyNameEmailDto(name=jack12, email=123456@126.com)

Hibernate: select user0_.name as col_0_0_, user0_.email as col_1_0_ from user user0_ where user0_.email=?
UserOnlyNameEmailDto(name=jack12, email=123456@126.com)

这里需要注意的是,如果我们去看源码的话,看关键的 PreferredConstructorDiscoverer 类时会发现,UserDTO 里面只能有一个全参数构造方法,如下所示:

数据库类型为money在java里面调用是什么数据类型_java_24

如上图所示,Constructor 选择的时候会帮我们做构造参数的选择,如果 DTO 里面有多个构造方法,就会报转化错误的异常,这一点需要注意,异常是这样的:

No converter found capable of converting from type [com.example.jpa.example1.User] to type [com.example.jpa.example1.UserOnlyNameEmailDto

No converter found capable of converting from type [com.example.jpa.example1.User] to type [com.example.jpa.example1.UserOnlyNameEmailDto

所以这种方式的优点就是返回的结果不需要是个实体对象,对 DB 不能进行除了查询之外的任何操作;缺点就是有 set 方法还可以改变里面的值,构造方法不能更改,必须全参数,这样如果是不熟悉 JPA 的新人操作的时候很容易引发 Bug。

第三种方法:返回结果是一个 POJO 的接口

我们再来学习一种返回不同字段的方式,这种方式与上面两种的区别是只需要定义接口,它的好处是只读,不需要添加构造方法,我们使用起来非常灵活,一般很难产生 Bug,那么它怎么实现呢?

首先,定义一个 UserOnlyName 的接口:

package com.example.jpa.example1;
public interface UserOnlyName {
    String getName();
    String getEmail();
}

package com.example.jpa.example1;
public interface UserOnlyName {
    String getName();
    String getEmail();
}

其次,我们的 UserRepository 写法如下:

package com.example.jpa.example1;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User,Long> {
    /**
     * 接口的方式返回DTO
     * @param address
     * @return
     */
    UserOnlyName findByAddress(String address);
}

package com.example.jpa.example1;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User,Long> {
    /**
     * 接口的方式返回DTO
     * @param address
     * @return
     */
    UserOnlyName findByAddress(String address);
}

然后,测试用例的写法如下:

@Test
    public void testProjections() {
userRepository.save(User.builder().name("jack12").email("123456@126.com").sex("man").address("shanghai").build());
        UserOnlyName userOnlyName = userRepository.findByAddress("shanghai");
        System.out.println(userOnlyName);
    }

    @Test
    public void testProjections() {
userRepository.save(User.builder().name("jack12").email("123456@126.com").sex("man").address("shanghai").build());
        UserOnlyName userOnlyName = userRepository.findByAddress("shanghai");
        System.out.println(userOnlyName);
    }

最后,我们的运行结果如下:

Hibernate: select user0_.name as col_0_0_, user0_.email as col_1_0_ from user user0_ where user0_.address=?
org.springframework.data.jpa.repository.query.AbstractJpaQuery$TupleConverter$TupleBackedMap@1d369521

Hibernate: select user0_.name as col_0_0_, user0_.email as col_1_0_ from user user0_ where user0_.address=?
org.springframework.data.jpa.repository.query.AbstractJpaQuery$TupleConverter$TupleBackedMap@1d369521

这个时候会发现我们的 userOnlyName 接口成了一个代理对象,里面通过 Map 的格式包含了我们的要返回字段的值(如:name、email),我们用的时候直接调用接口里面的方法即可,如 userOnlyName.getName() 即可;这种方式的优点是接口为只读,并且语义更清晰,所以这种是我比较推荐的做法。

其中源码是如何实现的,我来说一个类,你可以通过 debug,看一下最终 DTO 和接口转化执行的 query 有什么不同,看下图中老师 debug 显示的 Query 语句的位置:

数据库类型为money在java里面调用是什么数据类型_JPA_25

图一:是返回 DTO 接口形式的 query 生成的 JPQL。

数据库类型为money在java里面调用是什么数据类型_java_26

图二:是返回 DTO 类的时候 QueryStructure 生成的 JPQL 语句。

两种最大的区别是 DTO 类需要构造方法 new 一个对象出来,这就是我们第二种方法里面需要注意的 DTO 构造函数的问题;而通过图一我们可以看到接口直接通过 as 别名,映射成 hashmap 即可,非常灵活。这里我顺带给你提一个 tips。

这里说一个小技巧

当我们去写userRepositor 的定义方法的时候,IDA 会为我们提供满足 JPA 语法的提示,这也是用 Spring Data JPA 的好处之一,因为这些一旦约定死了(这里是指遵守 JPA 协议),周边的工具会越来越成熟,其中 MyBatis 太灵活了,就会导致周边的工具没办法跟上。创建 defining query method 的时候就会提示,如下图所示:

数据库类型为money在java里面调用是什么数据类型_java_27

以上就是返回 DTO 的几种常见的方法了,你在实际应用时,要不断 debug 和仔细体会。当然除了这些外,还有 @Query 注解也是可以做到,下一节会有介绍。

总结

本课时我为你讲解了返回结果的类型有哪些,也为你重点介绍了返回 DTO 的实战经验和方式,其中返回 DTO 以及第一种方式,我在下一课时“@Query 帮我们解决了什么问题?什么时候应该选择 @Query?”中再详细讲,方便你做实际参考。

实际工作中可能返回结果会比这个更复杂,但是你要掌握学习的“套路”,可以举一反三,学会看源码,就可以轻松应对工作中遇到的任何问题。

你是不是通过老师的课学会了如何利用 Repository 的返回结果解决实际问题了?如果学会了就分享吧,也欢迎你在下方留言,说出自己的观点。

点击下方链接查看源码(不定时更新)
https://github.com/zhangzhenhuajack/spring-boot-guide/tree/master/spring-data/spring-data-jpa