3 Spring Data JPA 查询方法的命名语法与参数
在⼯作中,你是否经常为⽅法名的语义、命名规范⽽发愁?是否要为不同的查询条件写各种的 SQL 语句?是否为同⼀个实体的查询,写⼀个超级通⽤的查询⽅法或者 SQL?如果其他开发同事不查看你写的 SQL 语句,⽽直接看⽅法名的话,却不知道你想查什么⽽郁闷?Spring Data JPA 的 Defining Query Methods(DQM)通过⽅法名和参数,可以很好地解决上⾯的问题,也能让我们的⽅法名的语义更加清晰,开发效率也会提升很多。DQM 语法共有 2 种,可以实现上⾯的那些问题,具体如下:
-
⼀种是直接通过⽅法名就可以实现;(本节重点)
-
另⼀种是 @Query ⼿动在⽅法上定义;
下⾯将从 6 个⽅⾯来详细讲解 Defining Query Methods。先来分析⼀下“定义查询⽅法的配置和使⽤⽅法”,这个是 Defining Query Methods 中必须要掌握的语法。
3.1 定义查询方法的配置和使用方法
若想要实现 CRUD 的操作,常规做法是写⼀⼤堆 SQL 语句。但在 JPA ⾥⾯,只需要继承 Spring Data Common ⾥⾯的任意 Repository 接⼝或者⼦接⼝,然后直接通过⽅法名就可以实现,例如实现 CrudRepository 接口:
interface UserRepository extends CrudRepository<User, Long> {
User findByEmailAddress(String emailAddress);
}
这个时候就可以直接调⽤ 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 注解。
这样在 Service 层就只有 findOne、save、findByEmailAddress 这 3 个⽅法可以调⽤,不会有更多⽅法了,我们可以对 SimpleJpaRepository ⾥⾯任意已经实现的⽅法做选择性暴露。
综上所述,得出以下 2 点结论:
- MyRepository Extends Repository 接⼝可以实现 Defining Query Methods 的功能;
- 继承其他 Repository 的⼦接⼝,或者⾃定义⼦接⼝,可以选择性地暴露 SimpleJpaRepository ⾥⾯已经实现的基础公⽤⽅法。
3.2 方法的查询策略设置
在平时的⼯作中,可以通过⽅法名,或者定义⽅法名上⾯添加 @Query 注解两种⽅式来实现 CRUD 的⽬的,⽽ Spring 给我们提供了两种切换⽅式。接下来我们就讲讲“⽅法的查询策略设置”。
⽬前在实际⽣产中还没有遇到要修改默认策略的情况,但我们必须要知道有这样的配置⽅法,做到⼼中有数,这样我们才能知道为什么⽅法名可以,@Query 也可以。通过@EnableJpaRepositories 注解来配置⽅法的查询策略,详细配置⽅法如下:
@EnableJpaRepositories(queryLookupStrategy = QueryLookupStrategy.Key.CREATE_IF_NOT_FOUND)
@SpringBootApplication
public class JpaApplication {
public static void main(String[] args) {
SpringApplication.run(JpaApplication.class, args);
}
}
其中,QueryLookupStrategy.Key 的值共 3 个,具体如下:
Create
:直接根据⽅法名进⾏创建,规则是根据⽅法名称的构造进⾏尝试,⼀般的⽅法是从⽅法名中删除给定的⼀组已知前缀,并解析该⽅法的其余部分。如果⽅法名不符合规则,启动的时候会报异常,这种情况可以理解为,即使配置了 @Query 也是没有⽤的。USE_DECLARED_QUERY
:声明⽅式创建,启动的时候会尝试找到⼀个声明的查询,如果没有找到将抛出⼀个异常,可以理解为必须配置 @Query。CREATE_IF_NOT_FOUND
:这个是默认的,除⾮有特殊需求,可以理解为这是以上2 种⽅式的兼容版。先⽤声明⽅式(@Query)进⾏查找,如果没有找到与⽅法相匹配的查询,那⽤ Create 的⽅法名创建规则创建⼀个查询;这两者都不满⾜的情况下,启动就会报错。
3.3 Defining Query Method(DQM)语法
Defining Query Method 语法是:带查询功能的⽅法名由查询策略(关键字)+ 查询字段 + ⼀些限制性条件组成,具有语义清晰、功能完整的特性,我们实际⼯作中 80% 的 API 查询都可以简单实现。
我们来看⼀个复杂点的例⼦,这是⼀个 and 条件更多、distinct or 排序、忽略⼤⼩写的例⼦。下⾯代码定义了 PersonRepository,我们可以在 service 层直接使⽤,如下所示:
interface PersonRepository extends Repository<User, Long> {
// and 的查询关系
List<User> findByEmailAddressAndLastname(String 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 ⽅法语法⾥常⽤的关键字列表,⽅便你快速查阅,并满⾜在实际代码中更加复杂的场景:
关键字 Keyword | 案例 Sample | JPQL 表达式 |
---|---|---|
Distinct | findDistinctByLastnameAndFirstname | select distinct … where x.lastname = ?1 and x.firstname = ?2 |
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 , Null | findByAge(Is)Null | … where x.age is null |
IsNotNull , NotNull | findByAge(Is)NotNull | … 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> ages) | … where x.age not in ?1 |
True | findByActiveTrue() | … where x.active = true |
False | findByActiveFalse() | … where x.active = false |
IgnoreCase | findByFirstnameIgnoreCase | … where UPPER(x.firstname) = UPPER(?1) |
综上,总结 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);
//根据Lastname删除⼀堆User,并返回删除的User
List<User> removeByLastname(String lastname);
}
有的时候随着版本的更新,也会有更多的语法⽀持,或者不同的版本语法可能也不⼀样,我们通过源码来看⼀下上⾯说的⼏种语法。感兴趣的同学可以到类 org.springframework.data.repository.query.parser.PartTree
查看相关源码的逻辑和处理⽅法,关键源码如下:
再看一下两个子类:
根据源码我们也可以分析出来,query method 包含其他的表达式,⽐如 find、count、delete、exist 等关键字在 by 之前通过正则表达式匹配。
我们可以查看 org.springframework.data.repository.query.parser.Part.Type
源码,看该枚举帮我们定义好了什么关键字
public enum Type {
BETWEEN(2, "IsBetween", "Between"),
IS_NOT_NULL(0, "IsNotNull", "NotNull"),
IS_NULL(0, "IsNull", "Null"),
LESS_THAN("IsLessThan", "LessThan"),
LESS_THAN_EQUAL("IsLessThanEqual", "LessThanEqual"),
GREATER_THAN("IsGreaterThan", "GreaterThan"),
GREATER_THAN_EQUAL("IsGreaterThanEqual", "GreaterThanEqual"),
BEFORE("IsBefore", "Before"),
AFTER("IsAfter", "After"),
NOT_LIKE("IsNotLike", "NotLike"),
LIKE("IsLike", "Like"),
STARTING_WITH("IsStartingWith", "StartingWith", "StartsWith"),
ENDING_WITH("IsEndingWith", "EndingWith", "EndsWith"),
IS_NOT_EMPTY(0, "IsNotEmpty", "NotEmpty"),
IS_EMPTY(0, "IsEmpty", "Empty"),
NOT_CONTAINING("IsNotContaining", "NotContaining", "NotContains"),
CONTAINING("IsContaining", "Containing", "Contains"),
NOT_IN("IsNotIn", "NotIn"),
IN("IsIn", "In"),
NEAR("IsNear", "Near"),
WITHIN("IsWithin", "Within"),
REGEX("MatchesRegex", "Matches", "Regex"),
EXISTS(0, "Exists"),
TRUE(0, "IsTrue", "True"),
FALSE(0, "IsFalse", "False"),
NEGATING_SIMPLE_PROPERTY("IsNot", "Not"),
SIMPLE_PROPERTY("Is", "Equals");
......
}
看源码就可以知道框架⽀持了哪些逻辑关键字,⽐如 NotIn、Like、In、Exists 等,有的时候⽐查⽂档和任何⼈写的博客都准确、还快。好了,上⾯介绍了⽅⾯名的基本表达⽅式,希望你可以在⼯作中灵活运⽤,举⼀反三。接下来我们讲讲特定类型的参数:Sort 排序和 Pageable 分⻚,这是分⻚和排序必备技能。
3.4 特定类型的参数:Sort 和 Pageable
Spring Data JPA 为了⽅便我们排序和分⻚,⽀持了两个特殊类型的参数:Sort 和 Pageable。
-
Sort ⾥⾯决定了我们哪些字段的排序⽅向(ASC 正序、DESC 倒序)。
-
Pageable 在查询的时候可以实现分⻚效果和动态排序双重效果,
我们看下 Pageable 的 Structure:
我们发现 Pageable 是⼀个接⼝,⾥⾯有常⻅的分⻚⽅法排序、当前⻚、下⼀⾏、当前指针、⼀共多少⻚、⻚码、pageSize 等。
在查询⽅法中如何使⽤ Pageable 和 Sort 呢?下⾯代码定义了根据 Lastname 查询 User 的分⻚和排序的实例,此段代码是在 UserRepository 接⼝⾥⾯定义的⽅法:
//根据分⻚参数查询User,返回⼀个带分⻚结果的Page(下⼀课时详解)对象(⽅法⼀)
Page<User> findByLastname(String lastname, Pageable pageable);
//我们根据分⻚参数返回⼀个Slice的user结果(⽅法⼆)
Slice<User> findByLastname(String lastname, Pageable pageable);
//根据排序结果返回⼀个List(⽅法三)
List<User> findByLastname(String lastname, Sort sort);
//根据分⻚参数返回⼀个List对象(⽅法四)
List<User> findByLastname(String lastname, Pageable pageable);
- ⽅法⼀:允许将 org.springframework.data.domain.Pageable 实例传递给查询⽅法,将分⻚参数添加到静态定义的查询中,通过 Page 返回的结果得知可⽤的元素和⻚⾯的总数。这种分⻚查询⽅法可能是昂贵的(会默认执⾏⼀条 count 的 SQL 语句),所以⽤的时候要考虑⼀下使⽤场景。
- ⽅法⼆:返回结果是 Slice,因为只知道是否有下⼀个 Slice 可⽤,⽽不知道 count,所以当查询较⼤的结果集时,只知道数据是⾜够的,也就是说⽤在业务场景中时不⽤关⼼⼀共有多少⻚。
- ⽅法三:如果只需要排序,需在
org.springframework.data.domain.Sort
参数中添加⼀个参数,正如上⾯看到的,只需返回⼀个 List 也是有可能的。 - ⽅法四:排序选项也通过 Pageable 实例处理,在这种情况下,Page 将不会创建构建实际实例所需的附加元数据(即不需要计算和查询分⻚相关数据),⽽仅仅⽤来做限制查询给定范围的实体。
那么如何使⽤呢?我们再来看⼀下源码,也就是 Pageable 的实现类,如下图所示:
由此可知,我们可以通过 PageRequest ⾥⾯提供的⼏个 of 静态⽅法(多态),分别构建⻚码、⻚⾯⼤⼩、排序等,Sort 的源码类似。我们来看下,在使⽤中的写法,如下所示:
// 按照 id 升序查询 user ⾥⾯的第⼀⻚,每⻚⼤⼩是20条;并会返回⼀共有多少⻚的信息
Page<User> page = userRepository.findAll(PageRequest.of(0, 20, Sort.by(Sort.Direction.ASC, "id")));
在实际⼯作中,如果遇到不知道参数怎么传递的情况,可以看⼀下源码,因为 Java 是类型安全的。
3.5 限制查询结果: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);
其中:
- 查询⽅法在使⽤ First 或 Top 时,数值可以追加到 First 或 Top 后⾯,指定返回最⼤结果的⼤⼩;
- 如果数字被省略,则假设结果⼤⼩为 1;
- 限制表达式也⽀持 Distinct 关键字;
- ⽀持将结果包装到 Optional 中。
- 如果将 Pageable 作为参数,size 以 Top 和 First 后⾯的数字为准。
3.6 @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.example.jpa.example1;
UserRepository 实现如下:
interface UserRepository extends Repository<User, Long> {
User getByEmailAddress(String emailAddress);
}
这个时候当 emailAddress 参数为 null 的时候就会抛 IllegalArgumentException 异常,当返回结果为 null 的时候会抛 EmptyResultDataAccessException 异常。因为我们在 package 的 package-info.java ⾥⾯指定了NonNullApi,所有返回结果和参数不能为 null。
// 添加 @Nullable 注解之后,参数和返回结果这个时候就都会允许为 null 了;
@Nullable
User findByEmailAddress(@Nullable String emailAdress);
// 返回结果允许为 null,参数不允许为 null 的情况
Optional<User> findOptionalByEmailAddress(String emailAddress);
3.7 思考
我们学习了 Defining Query Methods 的语法和其所表达的命名规范,在实际⼯作中,也可以将⽅法名(⾮常语义化的 respository ⾥⾯所定义⽅法命名规范)的强制约定规范运⽤到 controller 和 service 层,这样全部统⼀后,可以减少很多的沟通成本。
Spring Data Common ⾥⾯的 repository 基类,是否可以应⽤推⼴到 service 层,建⽴⼀个⾃⼰的 baseService?我们来看下⾯的实战例⼦:
- 自定义 BaseRepository
@NoRepositoryBean
interface BaseRepository<T, ID extends Serializable> extends JpaRepository<T, ID> {
}
- 定义 BaseService
public interface BaseService<T, ID> {
<S extends T> S save(S entity);
}
- 定义实现类 BaseServiceImpl
public class BaseServiceImpl<T, ID extends Serializable, R extends BaseRepository<T, ID>> implements BaseService<T, ID> {
@Autowired
private R repository;
protected R getRepository() {
return repository;
}
@Override
public <S extends T> S save(S entity) {
return repository.save(entity);
}
}
然后我们就可以在 BaseServiceImpl 里面实现我们的一些通用操作,例如保存时更新修改人和修改时间。