Spring Data JPA 的最大特色是利用方法名定义查询方法(Defining Query Methods)来做 CRUD 操作,这一课时我将围绕这个内容来详细讲解。
在工作中,你是否经常为方法名的语义、命名规范而发愁?是否要为不同的查询条件写各种的 SQL 语句?是否为同一个实体的查询,写一个超级通用的查询方法或者 SQL?如果其他开发同事不查看你写的 SQL 语句,而直接看方法名的话,却不知道你想查什么而郁闷?
Spring Data JPA 的 Defining Query Methods(DQM)通过方法名和参数,可以很好地解决上面的问题,也能让我们的方法名的语义更加清晰,开发效率也会提升很多。DQM 语法共有 2 种,可以实现上面的那些问题,具体如下:
下面我将从 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 findByEmailAddress(String emailAddress);
}
第 2 步,对于 Service 层就可以直接使用 UserRepository 接口:
复制代码
@Service
public class UserServiceImpl{
@Autowired
UserRepository userRepository;
public void testJpa() {
userRepository.deleteAll();
userRepository.findAll();
userRepository.findByEmailAddress("[email protected]");
}
这个时候就可以直接调用 CrudRepository 里面暴露的所有接口方法,以及 UserRepository 里面定义的方法,不需要写任何 SQL 语句,也不需要写任何实现方法。通过上面的两步我们完成了 Defining Query Methods(DQM)的基本使用,下面来看另外一种情况:选择性暴露方法。
然而,有时如果不想暴露 CrudRepository 里面的所有方法,那么可以直接继承我们认为需要暴露的那些方法的接口。假如 UserRepository 只想暴露 findOne 和 save,除了这两个方法之外不允许任何的 User 操作,其做法如下。
我们选择性地暴露 CRUD 方法,直接继承Repository(因为这里面没有任何方法),把CrudRepository 里面的 save 和 findOne 方法复制到我们自己的 MyBaseRepository 接口即可,代码如下:
复制代码
@NoRepositoryBean
interface MyBaseRepository extends Repository {
T findOne(ID id);
T save(T entity);
}
interface UserRepository extends MyBaseRepository {
User findByEmailAddress(String emailAddress);
}
这样在 Service 层就只有 findOne、save、findByEmailAddress 这 3 个方法可以调用,不会有更多方法了,我们可以对 SimpleJpaRepository 里面任意已经实现的方法做选择性暴露。
综上所述,得出以下 2 点结论:
在平时的工作中,你可以通过方法名,或者定义方法名上面添加 @Query 注解两种方式来实现 CRUD 的目的,而 Spring 给我们提供了两种切换方式。接下来我们就讲讲“方法的查询策略设置”。
目前在实际生产中还没有遇到要修改默认策略的情况,但我们必须要知道有这样的配置方法,做到心中有数,这样我们才能知道为什么方法名可以,@Query 也可以。通过 @EnableJpaRepositories 注解来配置方法的查询策略,详细配置方法如下:
复制代码
@EnableJpaRepositories(queryLookupStrategy= QueryLookupStrategy.Key.CREATE_IF_NOT_FOUND)
其中,QueryLookupStrategy.Key 的值共 3 个,具体如下:
以 Spring Boot 项目为例,更改其配置方法如下:
复制代码
@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)语法”,这是可以让方法生效的详细语法。
该语法是:带查询功能的方法名由查询策略(关键字)+ 查询字段 + 一些限制性条件组成,具有语义清晰、功能完整的特性,我们实际工作中 80% 的 API 查询都可以简单实现。
我们来看一个复杂点的例子,这是一个 and 条件更多、distinct or 排序、忽略大小写的例子。下面代码定义了 PersonRepository,我们可以在 service 层直接使用,如下所示:
复制代码
interface PersonRepository extends Repository {
// and 的查询关系
List findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname);
// 包含 distinct 去重,or 的 sql 语法
List findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname);
// 根据 lastname 字段查询忽略大小写
List findByLastnameIgnoreCase(String lastname);
// 根据 lastname 和 firstname 查询 equal 并且忽略大小写
List findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname);
// 对查询结果根据 lastname 排序,正序
List findByLastnameOrderByFirstnameAsc(String lastname);
// 对查询结果根据 lastname 排序,倒序
List findByLastnameOrderByFirstnameDesc(String lastname);
}
下面表格是一个我们在上面 DQM 方法语法里常用的关键字列表,方便你快速查阅,并满足在实际代码中更加复杂的场景:
综上,总结 3 点经验:
我们看到上面的表格虽然大多是 find 开头的方法,除此之外,JPA 还支持read、get、query、stream、count、exists、delete、remove等前缀,如字面意思一样。我们来看看 count、delete、remove 的例子,其他前缀可以举一反三。实例代码如下:
复制代码
interface UserRepository extends CrudRepository {
long countByLastname(String lastname);//查询总数
long deleteByLastname(String lastname);//根据一个字段进行删除操作,并返回删除行数
List removeByLastname(String lastname);//根据Lastname删除一堆User,并返回删除的User
}
有的时候随着版本的更新,也会有更多的语法支持,或者不同的版本语法可能也不一样,我们通过源码来看一下上面说的几种语法。感兴趣的同学可以到类 org.springframework.data.repository.query.parser.PartTree 查看相关源码的逻辑和处理方法,关键源码如下:
根据源码我们也可以分析出来,query method 包含其他的表达式,比如 find、count、delete、exist 等关键字在 by 之前通过正则表达式匹配。
由此可知,我们方法中的关键字不是乱填的,是枚举帮我们定义好的。接下来打开枚举类 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"});
....}
看源码就可以知道框架支持了哪些逻辑关键字,比如 NotIn、Like、In、Exists 等,有的时候比查文档和任何人写的博客都准确、还快。好了,上面介绍了方面名的基本表达方式,希望你可以在工作中灵活运用,举一反三。接下来我们讲讲特定类型的参数:Sort 排序和 Pageable 分页,这是分页和排序必备技能。
Spring Data JPA 为了方便我们排序和分页,支持了两个特殊类型的参数:Sort 和 Pageable。
Sort 在查询的时候可以实现动态排序,我们看下其源码:
复制代码
public Sort(Direction direction, String... properties) {
this(direction, properties == null ? new ArrayList<>() : Arrays.asList(properties));
}
Sort 里面决定了我们哪些字段的排序方向(ASC 正序、DESC 倒序)。
Pageable 在查询的时候可以实现分页效果和动态排序双重效果,我们看下 Pageable 的 Structure,如下图所示:
我们发现 Pageable 是一个接口,里面有常见的分页方法排序、当前页、下一行、当前指针、一共多少页、页码、pageSize 等。
在查询方法中如何使用 Pageable 和 Sort 呢?下面代码定义了根据 Lastname 查询 User 的分页和排序的实例,此段代码是在 UserRepository 接口里面定义的方法:
复制代码
Page findByLastname(String lastname, Pageable pageable);//根据分页参数查询User,返回一个带分页结果的Page(下一课时详解)对象(方法一)
Slice findByLastname(String lastname, Pageable pageable);//我们根据分页参数返回一个Slice的user结果(方法二)
List findByLastname(String lastname, Sort sort);//根据排序结果返回一个List(方法三)
List 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 的实现类,如下图所示:
由此可知,我们可以通过 PageRequest 里面提供的几个 of 静态方法(多态),分别构建页码、页面大小、排序等。我们来看下,在使用中的写法,如下所示:
复制代码
//查询user里面的lastname=jk的第一页,每页大小是20条;并会返回一共有多少页的信息
Page users = userRepository.findByLastname("jk",PageRequest.of(1, 20));
//查询user里面的lastname=jk的第一页的20条数据,不知道一共多少条
Slice users = userRepository.findByLastname("jk",PageRequest.of(1, 20));
//查询出来所有的user里面的lastname=jk的User数据,并按照name正序返回List
List users = userRepository.findByLastname("jk",new Sort(Sort.Direction.ASC, "name"))
//按照createdAt倒序,查询前一百条User数据
List users = userRepository.findByLastname("jk",PageRequest.of(0, 100, Sort.Direction.DESC, "createdAt"));
上面讲解了分页和排序的应用场景,在实际工作中,如果遇到不知道参数怎么传递的情况,可以看一下源码,因为 Java 是类型安全的。接下来讲解“限制查询结果 First 和 Top”,这是分页的另一种表达方式。
有的时候我们想直接查询前几条数据,也不需要动态排序,那么就可以简单地在方法名字中使用 First 和 Top 关键字,来限制返回条数。
我们来看看 userRepository 里面可以定义的一些限制返回结果的使用。在查询方法上加限制查询结果的关键字 First 和 Top。
复制代码
User findFirstByOrderByLastnameAsc();
User findTopByOrderByAgeDesc();
List findDistinctUserTop3ByLastname(String lastname, Pageable pageable);
List findFirst10ByLastname(String lastname, Sort sort);
List findTop10ByLastname(String lastname, Pageable pageable);
其中:
First 和 Top 关键字的使用非常简单,可以让我们的方法名语义更加清晰。接下来讲讲 NULL 的情况作了哪些支持。
从 Spring Data 2.0 开始,JPA 新增了@NonNull @NonNullApi @Nullable,是对 null 的参数和返回结果做的支持。
我在自己的 Repository 所在 package 的 package-info.java 类里面做如下声明:
复制代码
@org.springframework.lang.NonNullApi
package com.myrespository;
myrespository 下面的 UserRepository 实现如下:
复制代码
package com.myrespository;
import org.springframework.lang.Nullable;
interface UserRepository extends Repository {
User getByEmailAddress(EmailAddress emailAddress);
}
这个时候当 emailAddress 参数为 null 的时候就会抛异常,当返回结果为 null 的时候也会抛异常。因为我们在package 的 package-info.java里面指定了NonNullApi,所有返回结果和参数不能为 Null。
复制代码
@Nullable
User findByEmailAddress(@Nullable EmailAddress emailAdress);//当我们添加@Nullable 注解之后,参数和返回结果这个时候就都会允许为 null 了;
Optional findOptionalByEmailAddress(EmailAddress emailAddress); //返回结果允许为 null,参数不允许为 null 的情况
以上就是对 Defining Query Methods 的方法名和分页参数整体学习了。
我们学习了 Defining Query Methods 的语法和其所表达的命名规范,在实际工作中,也可以将方法名(非常语义化的 respository 里面所定义方法命名规范)的强制约定规范运用到 controller 和 service 层,这样全部统一后,可以减少很多的沟通成本。
Spring Data Common 里面的 repository 基类,我们是否可以应用推广到 service 层呢?能否也建立一个自己的 baseService?我们来看下面的实战例子:
复制代码
public interface BaseService {
Class getDomainClass();
S save(S entity);
List saveAll(Iterable entities);
void delete(T entity);
void deleteById(ID id);
void deleteAll();
void deleteAll(Iterable entities);
void deleteInBatch(Iterable entities);
void deleteAllInBatch();
T getOne(ID id);
Optional findOne(Example example);
Optional findById(ID id);
List findAll();
List findAll(Sort sort);
Page findAll(Pageable pageable);
List findAll(Example example);
List findAll(Example example, Sort sort);
Page findAll(Example example, Pageable pageable);
List findAllById(Iterable ids);
long count();
long count(Example example);
boolean exists(Example example);
boolean existsById(ID id);
void flush();
S saveAndFlush(S entity);
}
我们模仿JpaRepository接口也自定义了一个自己的BaseService,声明了常用的CRUD操作,上面的代码是生产代码,可以作为参考。当然了我们也可以建立自己的 PagingAndSortingService、ComplexityService、SampleService 等来划分不同的 service接口,供不同目的 Service 子类继承。
我们再来模仿一个 SimpleJpaRepository,来实现自己的 BaseService 的实现类。
复制代码
public class BaseServiceImpl> implements BaseService {
private static final Map DOMAIN_CLASS_CACHE = new ConcurrentHashMap<>();
private final R repository;
public BaseServiceImpl(R repository) {
this.repository = repository;
}
@Override
public Class getDomainClass() {
Class thisClass = getClass();
Class 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 save(S entity) {
return repository.save(entity);
}
@Override
public List saveAll(Iterable 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 entities) {
repository.deleteAll(entities);
}
@Override
public void deleteInBatch(Iterable entities) {
repository.deleteInBatch(entities);
}
@Override
public void deleteAllInBatch() {
repository.deleteAllInBatch();
}
@Override
public T getOne(ID id) {
return repository.getOne(id);
}
@Override
public Optional findOne(Example example) {
return repository.findOne(example);
}
@Override
public Optional findById(ID id) {
return repository.findById(id);
}
@Override
public List findAll() {
return repository.findAll();
}
@Override
public List findAll(Sort sort) {
return repository.findAll(sort);
}
@Override
public Page findAll(Pageable pageable) {
return repository.findAll(pageable);
}
@Override
public List findAll(Example example) {
return repository.findAll(example);
}
@Override
public List findAll(Example example, Sort sort) {
return repository.findAll(example, sort);
}
@Override
public Page findAll(Example example, Pageable pageable) {
return repository.findAll(example, pageable);
}
@Override
public List findAllById(Iterable ids) {
return repository.findAllById(ids);
}
@Override
public long count() {
return repository.count();
}
@Override
public long count(Example example) {
return repository.count(example);
}
@Override
public boolean exists(Example example) {
return repository.exists(example);
}
@Override
public boolean existsById(ID id) {
return repository.existsById(id);
}
@Override
public void flush() {
repository.flush();
}
@Override
public S saveAndFlush(S entity) {
return repository.saveAndFlush(entity);
}
}
以上代码就是 BaseService 常用的 CURD 实现代码,我们这里面大部分也是直接调用 Repository 提供的方法。需要注意的是,当继承 BaseServiceImpl 的时候需要传递自己的 Repository,如下面实例代码:
复制代码
@Service
public class UserServiceImpl extends BaseServiceImpl implements UserService {
public UserServiceImpl(UserRepository repository) {
super(repository);
}
.....
}
实战思考只是提供一种常见的实现思路,你也可以根据实际情况进行扩展和扩充。