作者简介
陈喆,现就职于中科院某研究所担任副研究员,专注于工业云平台、MES系统的设计与研发。
有两种方法可以实现定义数据库查询:
- 通过方法名称直接生成查询
- 自定义查询
1. 查询查找策略
当使用XML配置时,可以通过query-lookup-strategy
属性配置策略。当使用Java配置时,可以使用Enable${store}Repositories
注解的queryLookupStrategy
属性。一些策略可能在特定的数据库中不适用。
CREATE
通过解析方法名字来创建查询。即使有 @Query,@NameQuery都会忽略。USE_DECLARED_QUERY
通过执行@Query定义的语句来执行查询,如果没有,则看看有没有通过执行@NameQuery来执行查询,还没有则抛出异常CREATE_IF_NOT_FOUND(默认)
合并CREATE
和USE_DECLARED_QUERY
。如果通过 @Query指定查询语句,则执行该语句,如果没有,则看看有没有@NameQuery指定的查询语句,如果还没有,则通过解析方法名进行查询
2. 创建查询
Spring Data repository架构内建的查询构建机制可以实现基于repository的实体构建约束查询。该机制首先会从方法名剥离find…By
, read…By
, query…By
, count…By
和get…By
等前缀,然后再解析剩余部分。进一步可以引入包含一些额外的表达式的子句,比如Distinct
用于在创建查询时设置唯一查询。By
表示从此开始作为实际查询条件。你也可以通过And
和Or
将不同实体属性组合在一起,构成组合查询。
下面是通过方法名称创建查询的例子:
interface PersonRepository extends Repository<User, Long> {
List<Person> findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname);
// Enables the distinct flag for the query
List<Person> findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname);
List<Person> findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname);
// Enabling ignoring case for an individual property
List<Person> findByLastnameIgnoreCase(String lastname);
// Enabling ignoring case for all suitable properties
List<Person> findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname);
// Enabling static ORDER BY for a query
List<Person> findByLastnameOrderByFirstnameAsc(String lastname);
List<Person> findByLastnameOrderByFirstnameDesc(String lastname);
}
实际解析的结果取决于创建查询时依赖的持久性存储。然后,请注意以下几点共同特征:
- 函数表达式是通过连接符将属性连接起来的。你可以使用AND和OR整合属性表达式,也可以使用Betwen,LessThan,GreaterThan和Like。不同数据库支持的连接符不同,需要查询相应的参考文献。
- 支持给独立属性设置IgnoreCase标记(例如,
findByLastnameIgnoreCase(…)
)或者给所有支持忽略大小写的属性设置IgnoreCase标记(例如:findByLastnameAndFirstnameAllIgnoreCase(…)
)。是否支持忽略大小写,不同数据库也不同,需要查询相应参考文献。 - 你可以通过添加OrderBy子句实现静态排序,并且在要排序的属性后面设置排序方向(Asc或者Desc)。如果要实现动态排序,查阅“Special parameter handling”.
3. 属性表达式
属性表达式只能用于实体的直接属性。在创建查询时,你需要确定解析的属性就是实体类的属性。然而,你也可以定义遍历内嵌属性的约束。看一下下面的方法声明:
List<Person> findByAddressZipCode(ZipCode zipCode);
假设一个Person拥有一个带有ZipCode的Address. 在这种情况下,会按照x.address.zipCode作属性遍历。该算法首先会将整个部分(AddressZipCode
)当作一个属性并到实体类中检查是否具有该名称的属性。如果查找到了,则使用该属性。如果没有查找到,算法会按照camel大小写将字符串从右至左作拆分,将右侧部分放到头部,剩余部分放到尾部,在本例中会拆分成AddressZip和Code.如果找到了头部对应的属性,就继续对尾部进行解析。如果没找到匹配的属性,会将分割点左移一个(Address,ZipCode),然后继续解析。
这种方式在绝大多数场景都适用,但也有可能选择到错误的属性。假设Person还有一个addressZip属性。解析算法会在第一次分割时匹配到属性,但并不是想要的Address属性,然后失败(因为没有code属性)。
为了避免这种歧义,可以在方法名中使用\_来人工定义遍历点。所以上面的函数名可以定义成如下形式:
List<Person> findByAddress_ZipCode(ZipCode zipCode);
因为这里将下划线当作保留字符,所以强烈建议按照标准java命名规范命名(不要在属性名中使用下划线,要使用驼峰规范)。
4. 特殊参数操作
在前面的例子中,通过函数参数来操作查询中的参数。除此之外,还有两个特殊类型的参数Pageable和Sort,用于动态实现分页查询和排序。
下例在查询中使用到了Pageable,Slice和Sort:
Page<User> findByLastname(String lastname, Pageable pageable);
Slice<User> findByLastname(String lastname, Pageable pageable);
List<User> findByLastname(String lastname, Sort sort);
List<User> findByLastname(String lastname, Pageable pageable);
第一个方法传递了一个org.springframework.data.domain.Pageable
实例实现在定义的查询上动态添加分页。Page知道元素的总数和页数。这个数量是通过触发一个计数查询得到的。但这样作可能比较消耗资源(取决于选用的数据库),作为替代方案可以返回一个Slice。Slice只知道下一个Slice是否可用,一般这样可能就足以满足查询大数据结果集的需求。
排序操作也通过Pageable实例处理。如果只需要排序,添加org.springframework.data.domain.Pageable参数
到你的函数。这样会返回一个List。这种情况下,创建Page实例所需的额外元数据并没有被创建(额外的计数查询)。当然,它要求查询限定在给定范围的实体中。
5. 限制查询结果范围
可以通过first或top关键字限制查询结果范围,两者可以交换使用。在top或first后面设置一个可选的数字值用于指定返回结果的数量上限。如果没设置该数量,默认值为1.
下例演示限定返回结果集大小:
User findFirstByOrderByLastnameAsc();
User findTopByOrderByAgeDesc();
Page<User> queryFirst10ByLastname(String lastname, Pageable pageable);
Slice<User> findTop3ByLastname(String lastname, Pageable pageable);
List<User> findFirst10ByLastname(String lastname, Sort sort);
List<User> findTop10ByLastname(String lastname, Pageable pageable);
限定数量表达式也支持Distinct关键字。通过,如果将查询结果限定为一个实例,也支持使用Optional关键字封装结果。
6. 流化查询结果
可以通过Java 8的Stream<T>作为返回类型实现对查询结果的增量处理。
@Query("select u from User u")
Stream<User> findAllByCustomQueryAndStream();
Stream<User> readAllByFirstnameNotNull();
@Query("select u from User u")
Stream<User> streamAllPaged(Pageable pageable);
流封装了底层数据库资源并且要求在使用后关闭。你可以通过close()方法手动关闭流,也可以使用java 7的try-with-resources块,如下例:
try (Stream<User> stream = repository.findAllByCustomQueryAndStream()) {
stream.forEach(…);
}
注:不是所有的Spring Data模块支持Stream<T>返回类型
7. 异步查询
Repository查询可以通过Spring’s asynchronous method execution capability异步执行。这表示函数在调用后会立即返回,而实际查询会在Spring 的TaskExecutor作为一个任务执行。异步查询和响应式查询不同,所以不要混淆。
下例演示了几种异步查询:
@Async
Future<User> findByFirstname(String firstname);
@Async
CompletableFuture<User> findOneByFirstname(String firstname);
@Async
ListenableFuture<User> findOneByLastname(String lastname);