JPA 查询方法
本节介绍使用 Spring Data JPA 创建查询的各种方式。
查询查找策略
JPA 模块支持以字符串形式手动定义查询,或从方法名派生查询。
使用 IsStartingWith、StartingWith、StartsWith、IsEndingWith、EndingWith、EndsWith、IsNotContaining、NotContaining、NotContains、IsContaining、Containing、Contains 这些谓词的派生查询,其相应参数会被净化。这意味着如果参数实际包含 LIKE 所识别为通配符的字符,这些字符将被转义,使其仅作为字面量匹配。所用的转义字符可以通过设置 @EnableJpaRepositories 注解的 escapeCharacter 来配置。请与使用值表达式对比。
声明式查询
尽管从方法名派生查询相当方便,但您可能会遇到这样的情况:方法名解析器不支持您想使用的关键词,或者方法名变得不必要地冗长难看。因此,您既可以通过命名约定使用 JPA 命名查询(更多信息请参阅使用 JPA 命名查询),也可以改用 @Query 注解标注查询方法(详情请参阅使用 @Query)。
查询创建
一般而言,JPA 的查询创建机制如查询方法中所述。以下示例展示了 JPA 查询方法会被翻译成什么:
示例 1. 从方法名创建查询
public interface UserRepository extends Repository<User, Long> {
List<User> findByEmailAddressAndLastname(String emailAddress, String lastname);
}
我们使用 JPQL 创建的查询会翻译成如下查询:select u from User u where u.emailAddress = ?1 and u.lastname = ?2。Spring Data JPA 会执行属性检查并遍历嵌套属性,如属性表达式中所述。
下表描述了 JPA 支持的关键词,以及包含该关键词的方法会被翻译成什么:
表 1. 方法名内部支持的关键词
| 关键词 | 示例 | 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(如果参数为 null,则为 … where x.firstname IS NULL) |
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 is not null |
Like | findByFirstnameLike | … where x.firstname like ?1 |
NotLike | findByFirstnameNotLike | … where x.firstname not like ?1 |
StartingWith | findByFirstnameStartingWith | … where x.firstname like ?1(参数绑定时在其后追加 %) |
EndingWith | findByFirstnameEndingWith | … where x.firstname like ?1(参数绑定时在其前添加 %) |
Containing | findByFirstnameContaining | … where x.firstname like ?1(参数绑定时两端包裹 %) |
OrderBy | findByAgeOrderByLastnameDesc | … where x.age = ?1 order by x.lastname desc |
Not | findByLastnameNot | … where x.lastname <> ?1(如果参数为 null,则为 … where x.lastname IS NOT NULL) |
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) |
In 和 NotIn 还可以接受 Collection 的任何子类作为参数,也可以接受数组或可变参数。有关同一逻辑运算符的其他语法形式,请查阅仓库查询关键词。
DISTINCT 可能很棘手,并不总能产生您预期的结果。例如,select distinct u from User u 产生的结果与 select distinct u.lastname from User u 完全不同。在第一种情况下,由于您包含了 User.id,不会有任何重复,因此您会得到整张表,且结果是 User 对象。
然而,后一个查询会将范围缩小到仅 User.lastname,找出该表中所有不重复的姓氏。它返回的结果集也将是 List<String> 而非 List<User> 结果集。
countDistinctByLastname(String lastname) 也可能产生意外的结果。Spring Data JPA 会派生出 select count(distinct u.id) from User u where u.lastname = ?1。同样,由于 u.id 不会遇到重复,该查询会统计所有具有该绑定姓氏的用户。这与 countByLastname(String lastname) 完全相同!
那么这个查询的意义究竟是什么?是要找出具有某一姓氏的人数?找出具有该绑定姓氏的不重复人数?还是找出不重复姓氏的数量?(最后一个是完全不同的查询!)使用 distinct 有时需要手写查询并使用 @Query,才能最好地捕获您所寻求的信息,因为您可能还需要一个投影来捕获结果集。
基于注解的配置
基于注解的配置的优点是无需再编辑另一个配置文件,从而降低维护工作量。而这一好处的代价是:每声明一个新查询,您都需要重新编译领域类。
示例 2. 基于注解的命名查询配置
@Entity
@NamedQuery(name = "User.findByEmailAddress",
query = "select u from User u where u.emailAddress = ?1")
public class User {
}
使用 JPA 命名查询
各示例使用 <named-query /> 元素和 @NamedQuery 注解。这些配置元素对应的查询必须使用 JPA 查询语言定义。当然,您也可以使用 <named-native-query /> 或 @NamedNativeQuery。这些元素让您可以用原生 SQL 定义查询,但会失去数据库平台独立性。
XML 命名查询定义
要使用 XML 配置,请将必要的 <named-query /> 元素添加到位于 classpath 的 META-INF 文件夹下的 orm.xml JPA 配置文件中。通过使用某种已定义的命名约定,可以启用命名查询的自动调用。更多详情见下文。
示例 3. XML 命名查询配置
<named-query name="User.findByLastname">
<query>select u from User u where u.lastname = ?1</query>
</named-query>
该查询有一个特殊的名称,用于在运行时解析它。
声明接口
要支持这些命名查询,请按如下方式声明 UserRepository:
示例 4. 在 UserRepository 中声明查询方法
public interface UserRepository extends JpaRepository<User, Long> {
List<User> findByLastname(String lastname);
User findByEmailAddress(String emailAddress);
}
Spring Data 会尝试将对这些方法的调用解析为命名查询:以所配置领域类的简单类名开头,后跟以点分隔的方法名。因此,前述示例会使用先前定义的命名查询,而不是尝试从方法名创建查询。
使用 @Query
使用命名查询为实体声明查询是一种有效的方式,对于少量查询效果很好。由于查询本身与运行它们的 Java 方法绑定,您实际上可以使用 Spring Data JPA 的 @Query 注解直接绑定它们,而不是将其注解到领域类上。这使领域类摆脱了持久化相关信息,并将查询与仓库接口放在一起。
注解在查询方法上的查询,优先于使用 @NamedQuery 定义的查询或在 orm.xml 中声明的命名查询。
以下示例展示了使用 @Query 注解创建的查询:
示例 5. 在查询方法上使用 @Query 声明查询
public interface UserRepository extends JpaRepository<User, Long> {
@Query("select u from User u where u.emailAddress = ?1")
User findByEmailAddress(String emailAddress);
}
使用高级 LIKE 表达式
使用 @Query 创建的手动定义查询,其查询运行机制允许在查询定义中定义高级 LIKE 表达式,如下例所示:
示例 6. @Query 中的高级 like 表达式
public interface UserRepository extends JpaRepository<User, Long> {
@Query("select u from User u where u.firstname like %?1")
List<User> findByFirstnameEndsWith(String firstname);
}
在前述示例中,LIKE 分隔符(%)被识别出来,查询被转换为合法的 JPQL 查询(去除 %)。在运行查询时,传递给方法调用的参数会被先前识别出的 LIKE 模式增强。
原生查询
使用 @NativeQuery 注解可以运行原生查询,如下例所示:
示例 7. 在查询方法上使用 @NativeQuery 声明原生查询
public interface UserRepository extends JpaRepository<User, Long> {
@NativeQuery(value = "SELECT * FROM USERS WHERE EMAIL_ADDRESS = ?1")
User findByEmailAddress(String emailAddress);
}
@NativeQuery 注解在很大程度上是 @Query(nativeQuery=true) 的组合注解,但它还提供了额外的属性,例如 sqlResultSetMapping,以利用 JPA 的 @SqlResultSetMapping(…)。
Spring Data 可以为分页和排序重写简单查询。更复杂的查询则要求 classpath 中存在 JSqlParser,或者在代码中声明 countQuery。更多详情请参阅以下示例。
示例 8. 在查询方法上使用 @NativeQuery 为分页声明原生 count 查询
public interface UserRepository extends JpaRepository<User, Long> {
@NativeQuery(value = "SELECT * FROM USERS WHERE LASTNAME = ?1",
countQuery = "SELECT count(*) FROM USERS WHERE LASTNAME = ?1")
Page<User> findByLastname(String lastname, Pageable pageable);
}
类似的方式也适用于命名原生查询:为查询的副本添加 .count 后缀。不过,您可能需要为 count 查询注册一个结果集映射。
除了获取映射结果之外,原生查询还允许您通过选择 Map 容器作为方法返回类型,从数据库读取原始 Tuple。所得的 map 包含键值对,分别表示实际的数据库列名和值。
示例 9. 返回原始列名/值对的原生查询
interface UserRepository extends JpaRepository<User, Long> {
@NativeQuery("SELECT * FROM USERS WHERE EMAIL_ADDRESS = ?1")
Map<String, Object> findRawMapByEmail(String emailAddress); 1
@NativeQuery("SELECT * FROM USERS WHERE LASTNAME = ?1")
List<Map<String, Object>> findRawMapByLastname(String lastname); 2
}
由
Tuple支撑的单个Map结果。由多个
Tuple支撑的多个Map结果。
基于字符串的 Tuple 查询仅受 Hibernate 支持。EclipseLink 仅支持基于条件的 Tuple 查询。
查询内省与重写
Spring Data JPA 提供了广泛的功能,可用于运行各种形式的查询。具体而言,对于一个已声明的查询,Spring Data JPA 可以:
-
内省查询的投影,并为接口投影运行 tuple 查询
-
如果查询使用构造函数表达式,则使用 DTO 投影;当查询声明了实体别名或仅是多表达式选择时,重写投影
-
应用动态排序
-
派生
COUNT查询
为此,我们内置了专用于 HQL(Hibernate)和 EQL(EclipseLink)方言的查询解析器,因为这些方言定义良好。而 SQL 在各方言之间存在相当大的差异。正因如此,Spring Data 永远无法支持所有复杂度级别的查询。我们不是通用的 SQL 解析器库,而是一个通过简化查询执行来提高开发人员生产力的库。我们内置的 SQL 查询增强器仅支持简单查询的内省和 COUNT 查询派生。更复杂的查询则需要使用 JSqlParser,或者通过 @Query(countQuery=…) 提供 COUNT 查询。如果 classpath 中存在 JSqlParser,Spring Data JPA 会将其用于原生查询。
要对选择进行细粒度控制,您可以使用 @EnableJpaRepositories 配置 QueryEnhancerSelector:
示例 10. 使用 JavaConfig 的 Spring Data JPA 仓库
@Configuration
@EnableJpaRepositories(queryEnhancerSelector = MyQueryEnhancerSelector.class)
class ApplicationConfig {
// …
}
QueryEnhancerSelector 是一个策略接口,旨在根据特定查询选择 QueryEnhancer。如果您愿意,也可以提供自己的 QueryEnhancer 实现。
应用 QueryRewriter
有时,无论您尝试应用多少功能,似乎都无法让 Spring Data JPA 在查询发送到 EntityManager 之前,把您想要的一切都应用到查询上。
您能够在查询即将发送到 EntityManager 之前直接接触查询并对其进行"重写"。也就是说,您可以在最后一刻进行任何改动。查询重写适用于实际查询,并在适用时也适用于 count 查询。count 查询经过优化,因此要么不必要,要么通过其他方式获得计数,例如在存在外层事务时从 Hibernate SelectionQuery 派生。
示例 11. 使用 @Query 和 @NativeQuery 声明 QueryRewriter
public interface MyRepository extends JpaRepository<User, Long> {
@NativeQuery(value = "select original_user_alias.* from SD_USER original_user_alias",
queryRewriter = MyQueryRewriter.class)
List<User> findByNativeQuery(String param);
@Query(value = "select original_user_alias from User original_user_alias",
queryRewriter = MyQueryRewriter.class)
List<User> findByNonNativeQuery(String param);
}
此示例同时展示了原生(纯 SQL)重写器和 JPQL 查询,二者都利用同一个 QueryRewriter。在这种场景下,Spring Data JPA 会在应用上下文中查找注册的、对应类型的 Bean。
您可以像这样编写查询重写器:
示例 12. QueryRewriter 示例
public class MyQueryRewriter implements QueryRewriter {
@Override
public String rewrite(String query, Sort sort) {
return query.replaceAll("original_user_alias", "rewritten_user_alias");
}
}
您必须确保 QueryRewriter 已注册到应用上下文中,无论是通过应用 Spring Framework 的某个基于 @Component 的注解,还是将其作为 @Configuration 类中 @Bean 方法的一部分。
另一种选择是让仓库自身实现该接口。
示例 13. 提供 QueryRewriter 的仓库
public interface MyRepository extends JpaRepository<User, Long>, QueryRewriter {
@Query(value = "select original_user_alias.* from SD_USER original_user_alias",
nativeQuery = true,
queryRewriter = MyRepository.class)
List<User> findByNativeQuery(String param);
@Query(value = "select original_user_alias from User original_user_alias",
queryRewriter = MyRepository.class)
List<User> findByNonNativeQuery(String param);
@Override
default String rewrite(String query, Sort sort) {
return query.replaceAll("original_user_alias", "rewritten_user_alias");
}
}
根据您对 QueryRewriter 的使用方式,可能建议配置多个,并分别注册到应用上下文中。
在基于 CDI 的环境中,Spring Data JPA 会在 BeanManager 中查找您的 QueryRewriter 实现的实例。
使用 Sort
排序可以通过提供 PageRequest 或直接使用 Sort 来完成。Sort 的 Order 实例中实际使用的属性需要与您的领域模型匹配,这意味着它们必须解析为某个属性或查询中使用的别名。JPQL 将其定义为状态字段路径表达式。
使用任何不可引用的路径表达式都会导致 Exception。
然而,将 Sort 与@Query一起使用,可以让您在 ORDER BY 子句中悄悄塞入包含函数、且未经路径检查的 Order 实例。这之所以可行,是因为 Order 会被追加到给定的查询字符串中。默认情况下,Spring Data JPA 会拒绝任何包含函数调用的 Order 实例,但您可以使用 JpaSort.unsafe 来添加可能不安全的排序。
以下示例使用了 Sort 和 JpaSort,并在 JpaSort 上使用了不安全选项:
示例 14. 使用 Sort 和 JpaSort
public interface UserRepository extends JpaRepository<User, Long> {
@Query("select u from User u where u.lastname like ?1%")
List<User> findByAndSort(String lastname, Sort sort);
@Query("select u.id, LENGTH(u.firstname) as fn_len from User u where u.lastname like ?1%")
List<Object[]> findByAsArrayAndSort(String lastname, Sort sort);
}
repo.findByAndSort("lannister", Sort.by("firstname")); 1
repo.findByAndSort("stark", Sort.by("LENGTH(firstname)")); 2
repo.findByAndSort("targaryen", JpaSort.unsafe("LENGTH(firstname)")); 3
repo.findByAsArrayAndSort("bolton", Sort.by("fn_len")); 4
指向领域模型中属性的合法
Sort表达式。包含函数调用的非法
Sort,会抛出异常。包含显式_不安全_
Order的合法Sort。指向别名函数的合法
Sort表达式。
JpaSort.unsafe(…) 的限制
JpaSort.unsafe(…) 以两种模式运行:
-
当与派生查询或基于字符串的查询一起使用时,排序字符串会被追加到查询中。
-
当与按示例查询或 Specifications(使用
CriteriaQuery)一起使用时,排序表达式会被解析,并作为表达式添加到CriteriaQuery中。 -
JpaSort.JpaOrder.withUnsafe(…)会创建一个新的JpaSort,将当前的方向、大小写敏感性和空值处理应用于给定属性。查询表达式可以包含函数调用、各种子句(例如CASE WHEN、算术表达式)或属性路径。排序转换不支持子查询表达式、TREAT和CAST。
滚动处理大型查询结果
处理大型数据集时,滚动可以帮助您高效地处理这些结果,而无需将所有结果加载到内存中。
您有多种方式来消费大型查询结果:
-
分页。您已在前一章学习了
Pageable和PageRequest。 -
基于偏移量的滚动。这是比分页更轻量的方式,因为它不需要统计结果总数。
-
基于键集的滚动。该方法通过利用数据库索引,避免了基于偏移量的结果检索的缺点。
请阅读针对您的特定场景应使用哪种方式最好以了解更多。
基于字符串的查询方法暂不支持滚动。使用存储的 @Procedure 查询方法也不支持滚动。
使用命名参数
默认情况下,如前面所有示例所述,Spring Data JPA 使用基于位置的参数绑定。这使得查询方法在重构参数位置时容易出错。要解决这个问题,您可以使用 @Param 注解为方法参数指定一个具体名称,并在查询中绑定该名称,如下例所示:
示例 15. 使用命名参数
public interface UserRepository extends JpaRepository<User, Long> {
@Query("select u from User u where u.firstname = :firstname or u.lastname = :lastname")
User findByLastnameOrFirstname(@Param("lastname") String lastname,
@Param("firstname") String firstname);
}
方法参数会按照它们在所定义查询中的顺序进行交换。
从 4 版本起,Spring 完全支持基于 -parameters 编译器标志的 Java 8 参数名发现。通过在构建中使用该标志(作为调试信息的替代),您可以为命名参数省略 @Param 注解。
模板化查询与表达式
我们支持在使用 @Query 定义的手动查询中使用受限表达式。在运行查询时,这些表达式会针对一组预定义的变量进行求值。
如果您不熟悉值表达式,请参阅值表达式基础,了解 SpEL 表达式和属性占位符。
Spring Data JPA 支持一个名为 entityName 的模板变量。其用法为 select x from #{#entityName} x。它会插入与给定仓库关联的领域类型的 entityName。entityName 的解析方式如下:* 如果领域类型在 @Entity 注解上设置了 name 属性,则使用该名称。* 否则,使用领域类型的简单类名。
以下示例演示了查询字符串中 #{#entityName} 表达式的一个用例:您想定义一个带有查询方法和手动定义查询的仓库接口:
示例 16. 在仓库查询方法中使用 SpEL 表达式:entityName
@Entity
public class User {
@Id
@GeneratedValue
Long id;
String lastname;
}
public interface UserRepository extends JpaRepository<User,Long> {
@Query("select u from #{#entityName} u where u.lastname = ?1")
List<User> findByLastname(String lastname);
}
为了避免在 @Query 注解的查询字符串中写出实际的实体名,您可以使用 #{#entityName} 变量。
entityName 可以通过 @Entity 注解自定义。SpEL 表达式不支持在 orm.xml 中进行自定义。
当然,您本可以直接在查询声明中使用 User,但那样做也需要您随之修改查询。对 #entityName 的引用可以应对未来 User 类被重新映射为其他实体名的情况(例如使用 @Entity(name = "MyUser"))。
查询字符串中 #{#entityName} 表达式的另一个用例是:您想定义一个泛型仓库接口,并为具体的领域类型定义专门的仓库接口。为了避免在具体接口上重复定义自定义查询方法,您可以在泛型仓库接口的 @Query 注解查询字符串中使用实体名表达式,如下例所示:
示例 17. 在仓库查询方法中使用 SpEL 表达式:带继承的 entityName
@MappedSuperclass
public abstract class AbstractMappedType {
…
String attribute;
}
@Entity
public class ConcreteType extends AbstractMappedType { … }
@NoRepositoryBean
public interface MappedTypeRepository<T extends AbstractMappedType>
extends Repository<T, Long> {
@Query("select t from #{#entityName} t where t.attribute = ?1")
List<T> findAllByAttribute(String attribute);
}
public interface ConcreteRepository
extends MappedTypeRepository<ConcreteType> { … }
在前述示例中,MappedTypeRepository 接口是若干个扩展 AbstractMappedType 的领域类型的公共父接口。它还定义了泛型 findAllByAttribute(…) 方法,该方法可用于专门仓库接口的实例。如果此时在 ConcreteRepository 上调用 findAllByAttribute(…),查询将变为 select t from ConcreteType t where t.attribute = ?1。
您还可以使用表达式来控制方法参数。在这些表达式中,实体名不可用,但参数可用。可以按名称或索引访问它们,如下例所示。
示例 18. 在仓库查询方法中使用值表达式:访问参数
@Query("select u from User u where u.firstname = ?1 and u.firstname=?#{[0]} and u.emailAddress = ?#{principal.emailAddress}")
List<User> findByFirstnameAndCurrentUserWithCustomQuery(String firstname);
对于 like 条件,人们常常希望在字符串类型参数的开头或末尾追加 %。这可以通过在绑定参数标记或 SpEL 表达式后追加 % 或在其前添加 % 来实现。以下示例再次演示了这一点。
示例 19. 在仓库查询方法中使用值表达式:通配符快捷方式
@Query("select u from User u where u.lastname like %:#{[0]}% and u.lastname like %:lastname%")
List<User> findByLastnameWithSpelExpression(@Param("lastname") String lastname);
当 like 条件使用来自不可信来源的值时,这些值应被净化,使其不包含任何通配符,从而防止攻击者选出超出其应有权限的数据。为此,SpEL 上下文中提供了 escape(String) 方法。它会用第二个参数中的单个字符为第一个参数中所有的 _ 和 % 实例添加前缀。结合 JPQL 和标准 SQL 中 like 表达式可用的 escape 子句,这可以方便地清理绑定参数。
示例 20. 在仓库查询方法中使用值表达式:净化输入值
@Query("select u from User u where u.firstname like %?#{escape([0])}% escape ?#{escapeCharacter()}")
List<User> findContainingEscaped(String namePart);
在仓库接口中给出此方法声明后,findContainingEscaped("Peter_") 会找到 Peter_Parker,但不会找到 Peter Parker。所用的转义字符可以通过设置 @EnableJpaRepositories 注解的 escapeCharacter 来配置。请注意,SpEL 上下文中可用的 escape(String) 方法只会转义 SQL 和 JPQL 的标准通配符 _ 和 %。如果底层数据库或 JPA 实现支持额外的通配符,这些通配符不会被转义。
示例 21. 在仓库查询方法中使用值表达式:配置属性
@Query("select u from User u where u.applicationName = ?${spring.application.name:unknown}")
List<User> findContainingEscaped(String namePart);
如果您希望在运行时从 Environment 解析属性,也可以在查询方法中引用配置属性名(包括回退值)。该属性会在查询执行时求值。通常,属性占位符会解析为类似字符串的值。
其他方法
Spring Data JPA 提供了多种构建查询的方式。但有时,您的查询对于所提供的技术来说可能过于复杂。在这种情况下,请考虑:
-
如果您尚未这样做,直接使用@Query自己编写查询。
-
如果这不满足您的需求,请考虑实现一个自定义实现。这让您可以在仓库中注册一个方法,而将实现完全交给您自己。这使您能够:
-
直接与
EntityManager交互(编写纯 HQL/JPQL/EQL/原生 SQL,或使用条件 API) -
利用 Spring Framework 的
JdbcTemplate(原生 SQL) -
使用其他第三方数据库工具包。
-
-
另一种选择是将查询放入数据库,然后使用 Spring Data JPA 的@StoredProcedure 注解;如果是数据库函数,则使用@Query 注解并通过
CALL调用它。
当您需要对查询拥有最大控制力、同时仍希望 Spring Data JPA 提供资源管理时,这些策略可能最为有效。
修改性查询
前面各节介绍了如何声明查询以访问给定实体或实体集合。您可以使用Spring Data 仓库的自定义实现中所述的自定义方法设施来添加自定义修改行为。由于这种方式适合全面的自定义功能,因此对于只需要参数绑定的修改查询,您可以用 @Modifying 注解标注查询方法,如下例所示:
示例 22. 声明修改性查询
@Modifying
@Query("update User u set u.firstname = ?1 where u.lastname = ?2")
int setFixedFirstnameFor(String firstname, String lastname);
这样做会将注解在方法上的查询作为更新查询而非选择查询来触发。由于在执行修改性查询后,EntityManager 可能包含过时的实体,我们不会自动清除它(详情请参阅 EntityManager.clear() 的 JavaDoc),因为这实际上会丢弃 EntityManager 中所有尚未刷新的待处理更改。如果您希望自动清除 EntityManager,可以将 @Modifying 注解的 clearAutomatically 属性设为 true。
@Modifying 注解仅在与 @Query 注解结合使用时才有意义。派生查询方法或自定义方法不需要该注解。
派生删除查询
Spring Data JPA 还支持派生删除查询,让您无需显式声明 JPQL 查询,如下例所示:
示例 23. 使用派生删除查询
interface UserRepository extends Repository<User, Long> {
void deleteByRoleId(long roleId);
@Modifying
@Query("delete from User u where u.role.id = ?1")
void deleteInBulkByRoleId(long roleId);
}
尽管 deleteByRoleId(…) 方法看起来与 deleteInBulkByRoleId(…) 基本产生相同的结果,但这两个方法声明在运行方式上存在重要区别。顾名思义,后一个方法会向数据库发出单个 JPQL 查询(即注解中定义的那个)。这意味着即便是当前已加载的 User 实例,也不会触发生命周期回调。
为了确保生命周期回调真正被调用,deleteByRoleId(…) 的调用会先运行一个查询,然后逐个删除返回的实例,以便持久化提供方能够在这些实体上真正调用 @PreRemove 回调。
实际上,派生删除查询是一种快捷方式:先运行查询,然后对结果调用 CrudRepository.delete(Iterable<User> users),并使其行为与 CrudRepository 中其他 delete(…) 方法的实现保持一致。
删除大量对象时,您需要考虑性能影响,以确保有充足的可用内存。所有结果对象在删除前都会被加载到内存中,并在会话中保持,直到刷新或事务完成。
应用查询提示
要将 JPA 查询提示应用到仓库接口中声明的查询上,您可以使用 @QueryHints 注解。它接受一个 JPA @QueryHint 注解数组,外加一个布尔标志,可用于禁用应用分页时触发的附加 count 查询上所应用的提示,如下例所示:
示例 24. 在仓库方法上使用 QueryHints
public interface UserRepository extends Repository<User, Long> {
@QueryHints(value = { @QueryHint(name = "name", value = "value")},
forCounting = false)
Page<User> findByLastname(String lastname, Pageable pageable);
}
前述声明会将配置的 @QueryHint 应用于实际查询,但不会将其应用于为计算总页数而触发的 count 查询。
使用 Java Stream<T> 时,请结合您的 JPA 提供方和 JDBC 驱动程序的上下文,查阅Stream 语义与资源处理,尤其要考虑提供方特定的抓取行为和驱动程序的 fetch-size 设置。
向查询添加注释
有时,您需要基于数据库性能调试一个查询。您的数据库管理员展示给您的查询,可能与您使用 @Query 编写的查询大相径庭;对于自定义查找器或使用按示例查询时,它可能与您所设想的 Spring Data JPA 生成结果完全不同。
为了简化这一过程,您可以通过应用 @Meta 注解,向几乎任何 JPA 操作(无论是查询还是其他操作)插入自定义注释。
示例 25. 将 @Meta 注解应用于仓库操作
public interface RoleRepository extends JpaRepository<Role, Integer> {
@Meta(comment = "find roles by name")
List<Role> findByName(String name);
@Override
@Meta(comment = "find roles using QBE")
<S extends Role> List<S> findAll(Example<S> example);
@Meta(comment = "count roles for a given name")
long countByName(String name);
@Override
@Meta(comment = "exists based on QBE")
<S extends Role> boolean exists(Example<S> example);
}
这个示例仓库既包含自定义查找器,也包含对 JpaRepository 继承操作的重写。无论哪种方式,@Meta 注解都让您添加一个 comment,该注释会在查询发送到数据库之前插入其中。
同样需要注意的是,该功能并不仅限于查询。它还扩展到 count 和 exists 操作。虽然未展示,它也扩展到某些 delete 操作。
尽管我们已尽可能在各处应用此功能,但底层 EntityManager 的某些操作并不支持注释。例如,entityManager.createQuery() 在文档中明确说明支持注释,而 entityManager.find() 操作则不支持。
JPQL 日志和 SQL 日志在 JPA 中都不是标准,因此每个提供方都需要自定义配置,如下文各节所示。
启用 Hibernate 注释
要在 Hibernate 中启用查询注释,您必须将 hibernate.use_sql_comments 设为 true。
如果您使用基于 Java 的配置设置,可以这样做:
示例 26. 基于 Java 的 JPA 配置
@Bean
public Properties jpaProperties() {
Properties properties = new Properties();
properties.setProperty("hibernate.use_sql_comments", "true");
return properties;
}
如果您有 persistence.xml 文件,可以在其中应用:
示例 27. 基于 persistence.xml 的配置
<persistence-unit name="my-persistence-unit">
...registered classes...
<properties>
<property name="hibernate.use_sql_comments" value="true" />
</properties>
</persistence-unit>
最后,如果您使用 Spring Boot,则可以在 application.properties 文件中进行设置:
示例 28. 基于 Spring Boot 属性的配置
spring.jpa.properties.hibernate.use_sql_comments=true
启用 EclipseLink 注释
要在 EclipseLink 中启用查询注释,您必须将 eclipselink.logging.level.sql 设为 FINE。
如果您使用基于 Java 的配置设置,可以这样做:
示例 29. 基于 Java 的 JPA 配置
@Bean
public Properties jpaProperties() {
Properties properties = new Properties();
properties.setProperty("eclipselink.logging.level.sql", "FINE");
return properties;
}
如果您有 persistence.xml 文件,可以在其中应用:
示例 30. 基于 persistence.xml 的配置
<persistence-unit name="my-persistence-unit">
...registered classes...
<properties>
<property name="eclipselink.logging.level.sql" value="FINE" />
</properties>
</persistence-unit>
最后,如果您使用 Spring Boot,则可以在 application.properties 文件中进行设置:
示例 31. 基于 Spring Boot 属性的配置
spring.jpa.properties.eclipselink.logging.level.sql=FINE
配置抓取图与加载图
JPA 2.1 规范引入了对指定抓取图(FetchGraph)和加载图(LoadGraph)的支持,我们也通过 @EntityGraph 注解支持这一能力,该注解让您可以引用 @NamedEntityGraph 定义。您可以在实体上使用该注解来配置所得查询的抓取计划。抓取的类型(Fetch 或 Load)可以通过 @EntityGraph 注解的 type 属性来配置。进一步参考请参阅 JPA 2.1 规范 3.7.4 节。
以下示例展示了如何在实体上定义命名实体图:
示例 32. 在实体上定义命名实体图。
@Entity
@NamedEntityGraph(name = "GroupInfo.detail",
attributeNodes = @NamedAttributeNode("members"))
public class GroupInfo {
// default fetch mode is lazy.
@ManyToMany
List<GroupMember> members = new ArrayList<GroupMember>();
…
}
以下示例展示了如何在仓库查询方法上引用命名实体图:
示例 33. 在仓库查询方法上引用命名实体图定义。
public interface GroupRepository extends CrudRepository<GroupInfo, String> {
@EntityGraph(value = "GroupInfo.detail", type = EntityGraphType.LOAD)
GroupInfo getByGroupName(String name);
}
也可以通过 @EntityGraph 定义临时实体图。所提供的 attributePaths 会被翻译成相应的 EntityGraph,而无需向领域类型显式添加 @NamedEntityGraph,如下例所示:
示例 34. 在仓库查询方法上使用临时实体图定义
public interface GroupRepository extends CrudRepository<GroupInfo, String> {
@EntityGraph(attributePaths = { "members" })
GroupInfo getByGroupName(String name);
}
滚动
滚动是一种更细粒度的方式,用于遍历大型结果集的分块。滚动由稳定排序、滚动类型(基于偏移量或基于键集的滚动)和结果限制组成。您可以使用属性名定义简单的排序表达式,并通过查询派生使用 Top 或 First 关键词定义静态结果限制。您可以拼接表达式,将多个条件收集到一个表达式中。
滚动查询返回一个 Window<T>,它允许获取元素的滚动位置,以抓取下一个 Window<T>,直到您的应用程序消费完整查询结果。类似于通过获取下一批结果来消费 Java Iterator<List<…>>,查询结果滚动让您可以通过 Window.positionAt(…) 访问 ScrollPosition,如下例所示:
Window<User> users = repository.findFirst10ByLastnameOrderByFirstname("Doe", ScrollPosition.offset());
do {
for (User u : users) {
// consume the user
}
if (users.isLast() || users.isEmpty()) {
break;
}
// obtain the next Scroll
users = repository.findFirst10ByLastnameOrderByFirstname("Doe", users.positionAt(users.size() - 1));
} while (!users.isEmpty());
ScrollPosition 标识元素在整个查询结果中的确切位置。查询执行将位置参数视为_排他_的,结果将从给定位置_之后_开始。ScrollPosition#offset() 和 ScrollPosition#keyset() 是 ScrollPosition 的特殊形式,表示滚动操作的起点。
以上示例展示了静态排序和限制。您也可以定义接受 Sort 对象的查询方法,以按请求定义更复杂的排序顺序或排序。类似地,提供 Limit 对象让您可以按请求定义动态限制,而不是应用静态限制。有关动态排序和限制的更多信息,请阅读查询方法详情。
通过消费 Window 实例进行滚动,需要相当多的条件判断才能达到最优的数据库往返,而且很快就会变成一项重复性任务,使用 WindowIterator 可以简化它。
WindowIterator 提供了一个工具,通过免去检查下一个 Window 是否存在并应用 ScrollPosition 的工作,简化跨 Window 的滚动。
WindowIterator<User> users = WindowIterator.of(position -> repository.findFirst10ByLastnameOrderByFirstname("Doe", position))
.startingAt(ScrollPosition.offset());
while (users.hasNext()) {
User u = users.next();
// consume the user
}
使用偏移量滚动
与分页类似,偏移量滚动使用一个偏移量计数器来跳过一定数量的结果,并让数据源只返回从给定偏移量开始的结果。这个简单的机制避免了将大型结果发送到客户端应用程序。然而,大多数数据库要求在服务器返回结果之前先物化完整的查询结果。
示例 35. 将 OffsetScrollPosition 与仓库查询方法一起使用
interface UserRepository extends Repository<User, Long> {
Window<User> findFirst10ByLastnameOrderByFirstname(String lastname, OffsetScrollPosition position);
}
WindowIterator<User> users = WindowIterator.of(position -> repository.findFirst10ByLastnameOrderByFirstname("Doe", position))
.startingAt(OffsetScrollPosition.initial()); 1
以无偏移量开始,以包含位置
0处的元素。
ScrollPosition.offset() 与 ScrollPosition.offset(0L) 之间存在差异。前者表示滚动操作的起点,不指向任何特定偏移量;而后者标识结果的第一个元素(位于位置 0)。鉴于滚动的_排他_特性,使用 ScrollPosition.offset(0) 会跳过第一个元素,并转换为偏移量 1。
使用键集过滤滚动
基于偏移量的滚动要求大多数数据库在服务器返回结果之前物化整个结果。因此,虽然客户端只看到所请求结果的一部分,但您的服务器需要构建完整结果,这会造成额外的负载。
键集过滤利用数据库的内置能力来实现结果子集检索,旨在降低单个查询的计算和 I/O 需求。该方式维护一组键,通过将键传入查询来恢复滚动,从而有效地修正您的过滤条件。
键集过滤的核心思想是使用稳定的排序顺序开始检索结果。当您想滚动到下一个分块时,您会获得一个 ScrollPosition,用于在已排序结果中重建位置。ScrollPosition 捕获当前 Window 中最后一个实体的键集。运行查询时,重建过程会重写条件子句,使其包含所有排序字段和主键,以便数据库利用潜在索引来运行查询。数据库只需从给定键集位置构建一个小得多的结果,而无需完整物化大型结果然后跳过结果直到达到特定偏移量。
键集过滤要求键集属性(即用于排序的属性)为非空。施加这一限制,是因为比较运算符对 null 值的处理是特定于存储的,同时也要求针对带索引的数据源运行查询。对可空属性进行键集过滤会导致意外的结果。
interface UserRepository extends Repository<User, Long> {
Window<User> findFirst10ByLastnameOrderByFirstname(String lastname, KeysetScrollPosition position);
}
WindowIterator<User> users = WindowIterator.of(position -> repository.findFirst10ByLastnameOrderByFirstname("Doe", position))
.startingAt(ScrollPosition.keyset()); 1
从最开始处开始,不应用额外的过滤。
当数据库包含与排序字段匹配的索引时,键集过滤效果最佳,因此静态排序运行良好。应用键集过滤的滚动查询要求排序顺序中使用的属性必须由查询返回,并且这些属性必须映射到返回的实体中。
您可以使用接口投影和 DTO 投影,但务必包含所有用于排序的属性,以避免键集提取失败。
指定 Sort 顺序时,包含与查询相关的排序属性即可;如果您不希望,也无需确保查询结果唯一。键集查询机制会通过包含主键(或复合主键的其余部分)来修正您的排序顺序,以确保每个查询结果都是唯一的。