跳到主要内容

JPA 查询方法

DeepSeek V3 中英对照 JPA Query Methods

本节介绍了使用 Spring Data JPA 创建查询的各种方法。

查询查找策略

JPA 模块支持手动将查询定义为字符串,或根据方法名称自动派生查询。

使用谓词 IsStartingWithStartingWithStartsWithIsEndingWithEndingWithEndsWithIsNotContainingNotContainingNotContainsIsContainingContainingContains 的派生查询的参数将被清理。这意味着如果参数实际上包含被 LIKE 识别为通配符的字符,这些字符将被转义,以便它们仅作为字面量进行匹配。使用的转义字符可以通过设置 @EnableJpaRepositories 注解的 escapeCharacter 进行配置。与使用值表达式进行比较。

声明的查询

尽管从方法名称派生出查询非常方便,但有时可能会遇到方法名称解析器不支持想要使用的关键字,或者方法名称会变得不必要的冗长的情况。因此,您可以通过命名约定使用 JPA 命名查询(更多信息请参见 使用 JPA 命名查询),或者使用 @Query 注解来标注查询方法(详细信息请参见 使用 @Query)。

查询创建

通常,JPA 的查询创建机制如查询方法中所述。以下示例展示了 JPA 查询方法的转换过程:

示例 1. 从方法名创建查询

public interface UserRepository extends Repository<User, Long> {

List<User> findByEmailAddressAndLastname(String emailAddress, String lastname);
}
java

我们使用 JPA Criteria API 从中创建了一个查询,但本质上,这会被转换为以下查询:select u from User u where u.emailAddress = ?1 and u.lastname = ?2。Spring Data JPA 会进行属性检查并遍历嵌套属性,如属性表达式中所述。

下表描述了 JPA 支持的关键字以及包含该关键字的方法所转换的内容:

表 1. 方法名称中支持的关键字

关键字示例JPQL 片段
DistinctfindDistinctByLastnameAndFirstnameselect distinct …​ where x.lastname = ?1 and x.firstname = ?2
AndfindByLastnameAndFirstname… where x.lastname = ?1 and x.firstname = ?2
OrfindByLastnameOrFirstname… where x.lastname = ?1 or x.firstname = ?2
Is, EqualsfindByFirstname,findByFirstnameIs,findByFirstnameEquals… where x.firstname = ?1 (如果参数为 null,则 … where x.firstname IS NULL)
BetweenfindByStartDateBetween… where x.startDate between ?1 and ?2
LessThanfindByAgeLessThan… where x.age < ?1
LessThanEqualfindByAgeLessThanEqual… where x.age <= ?1
GreaterThanfindByAgeGreaterThan… where x.age > ?1
GreaterThanEqualfindByAgeGreaterThanEqual… where x.age >= ?1
AfterfindByStartDateAfter… where x.startDate > ?1
BeforefindByStartDateBefore… where x.startDate < ?1
IsNull, NullfindByAge(Is)Null… where x.age is null
IsNotNull, NotNullfindByAge(Is)NotNull… where x.age is not null
LikefindByFirstnameLike… where x.firstname like ?1
NotLikefindByFirstnameNotLike… where x.firstname not like ?1
StartingWithfindByFirstnameStartingWith… where x.firstname like ?1 (参数附加 %)
EndingWithfindByFirstnameEndingWith… where x.firstname like ?1 (参数前置 %)
ContainingfindByFirstnameContaining… where x.firstname like ?1 (参数包裹在 % 中)
OrderByfindByAgeOrderByLastnameDesc… where x.age = ?1 order by x.lastname desc
NotfindByLastnameNot… where x.lastname <> ?1
InfindByAgeIn(Collection<Age> ages)… where x.age in ?1
NotInfindByAgeNotIn(Collection<Age> ages)… where x.age not in ?1
TruefindByActiveTrue()… where x.active = true
FalsefindByActiveFalse()… where x.active = false
IgnoreCasefindByFirstnameIgnoreCase… where UPPER(x.firstname) = UPPER(?1)
备注

InNotIn 也可以接受 Collection 的任何子类作为参数,同样也接受数组或可变参数。关于相同逻辑运算符的其他语法版本,请查看 Repository query keywords

注意

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 {

}
java

使用 JPA 命名查询

备注

示例中使用了 <named-query /> 元素和 @NamedQuery 注解。这些配置元素的查询必须使用 JPA 查询语言来定义。当然,你也可以使用 <named-native-query />@NamedNativeQuery。这些元素允许你使用原生 SQL 来定义查询,但会失去数据库平台的独立性。

XML 命名查询定义

要使用 XML 配置,请将必要的 <named-query /> 元素添加到位于类路径下 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>
xml

查询有一个特殊的名称,用于在运行时解析它。

声明接口

为了允许这些命名查询,请按如下方式指定 UserRepository

示例 4. 在 UserRepository 中的查询方法声明

public interface UserRepository extends JpaRepository<User, Long> {

List<User> findByLastname(String lastname);

User findByEmailAddress(String emailAddress);
}
java

Spring Data 会尝试将这些方法的调用解析为一个命名查询,解析时以配置的领域类的简名开头,后跟方法名,两者之间用点号分隔。因此,前面的示例将使用先前定义的命名查询,而不是尝试从方法名创建查询。

使用 @Query

使用命名查询来为实体声明查询是一种有效的方法,并且在查询数量较少时效果良好。由于这些查询与执行它们的 Java 方法紧密相关,实际上你可以通过使用 Spring Data JPA 的 @Query 注解直接将它们绑定,而不是将它们注解到领域类上。这样可以将领域类从持久化特定信息中解放出来,并将查询与仓库接口放在一起。

使用 @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);
}
java

应用查询重写器

有时候,无论你尝试应用多少特性,似乎都无法让 Spring Data JPA 在查询发送到 EntityManager 之前应用所有你想要的功能。

你可以在查询被发送到 EntityManager 之前获取到它,并对其进行“重写”。也就是说,你可以在最后一刻进行任何修改。

示例 6. 使用 @Query 声明一个 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);
}
java

这个示例展示了一个原生(纯 SQL)重写器以及一个 JPQL 查询,两者都利用了相同的 QueryRewriter。在这种情况下,Spring Data JPA 会在应用程序上下文中查找注册的对应类型的 bean。

你可以这样编写一个查询重写器:

示例 7. 示例 QueryRewriter

public class MyQueryRewriter implements QueryRewriter {

@Override
public String rewrite(String query, Sort sort) {
return query.replaceAll("original_user_alias", "rewritten_user_alias");
}
}
java

你需要确保你的 QueryRewriter 被注册到应用上下文中,无论是通过使用 Spring Framework 的基于 @Component 的注解,还是将其作为 @Configuration 类中的 @Bean 方法的一部分。

另一种选择是让存储库本身实现该接口。

示例 8. 提供 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");
}
}
java

根据你对 QueryRewriter 的使用情况,可能需要配置多个,并将每个都注册到应用程序上下文中。

备注

在基于 CDI 的环境中,Spring Data JPA 将在 BeanManager 中搜索你实现的 QueryRewriter 实例。

使用高级 LIKE 表达式

使用 @Query 创建的手动定义查询的运行机制允许在查询定义中定义高级的 LIKE 表达式,如下例所示:

示例 9. @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);
}
java

在前面的示例中,LIKE 分隔符字符(%)被识别,查询被转换为有效的 JPQL 查询(去除 %)。在运行查询时,传递给方法调用的参数会与之前识别的 LIKE 模式进行增强。

原生查询

使用 @NativeQuery 注解可以运行原生查询,如下例所示:

示例 10. 在查询方法上使用 @Query 声明原生查询

public interface UserRepository extends JpaRepository<User, Long> {

@NativeQuery(value = "SELECT * FROM USERS WHERE EMAIL_ADDRESS = ?1")
User findByEmailAddress(String emailAddress);
}
java
备注

@NativeQuery 注解主要是 @Query(nativeQuery=true) 的一个组合注解,但它还提供了额外的属性,例如 sqlResultSetMapping,以利用 JPA 的 @SqlResultSetMapping(…)

备注

Spring Data 可以重写简单的查询以实现分页和排序。对于更复杂的查询,需要在类路径中包含 JSqlParser 或者在代码中声明一个 countQuery。有关更多详细信息,请参见下面的示例。

示例 11. 在查询方法中使用 @NativeQuery 声明原生分页查询

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);
}
java
提示

尽管 JSqlParser 在类路径中可用,但可以通过在 spring.properties 文件或系统属性中设置 spring.data.jpa.query.native.parser=regex 来禁用其用于解析原生查询的功能。

有效值包括(不区分大小写):

  • auto(默认值,自动选择)

  • regex(使用内置的基于正则表达式的查询增强器)

  • jsqlparser(使用 JSqlParser)

类似的方法也适用于命名的原生查询,只需在查询副本后添加 .count 后缀。不过,你可能需要为计数查询注册一个结果集映射。

除了获取映射结果之外,原生查询还允许你通过选择 Map 容器作为方法的返回类型,从数据库中读取原始的 Tuple。生成的映射包含键/值对,表示实际的数据库列名和对应的值。

示例 12. 返回原始列名/值对的原生查询

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
}
java
  • 单个由 Tuple 支持的 Map 结果。

  • 多个由 Tuples 支持的 Map 结果。

备注

基于字符串的元组查询仅由 Hibernate 支持。Eclipselink 仅支持基于 Criteria 的元组查询。

使用排序

排序可以通过提供 PageRequest 或直接使用 Sort 来完成。SortOrder 实例实际使用的属性需要与你的领域模型匹配,这意味着它们需要解析为查询中使用的属性或别名。JPQL 将其定义为状态字段路径表达式。

备注

使用任何不可引用的路径表达式都会导致 Exception

然而,将 Sort@Query 一起使用,可以让你在 ORDER BY 子句中偷偷加入包含函数的、未经路径检查的 Order 实例。这是因为 Order 会被附加到给定的查询字符串中。默认情况下,Spring Data JPA 会拒绝任何包含函数调用的 Order 实例,但你可以使用 JpaSort.unsafe 来添加可能存在安全隐患的排序。

以下示例使用了 SortJpaSort,其中包括 JpaSort 上的一个不安全选项:

示例 13. 使用 SortJpaSort

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
java
  • 指向领域模型中属性的有效 Sort 表达式。

  • 包含函数调用的无效 Sort,会抛出异常。

  • 包含显式 不安全 Order 的有效 Sort

  • 指向别名函数的有效 Sort 表达式。

滚动查询大量结果

在处理大型数据集时,滚动功能可以帮助高效处理这些结果,而无需将所有结果加载到内存中。

您有多种选择来处理大型查询结果:

  1. 分页。在上一章中,你已经学习了关于 PageablePageRequest 的内容。

  2. 基于偏移量的滚动。这是比分页更轻量的变体,因为它不需要总结果计数。

  3. 基于键集的滚动。该方法通过利用数据库索引避免了基于偏移量的结果检索的缺点

阅读更多关于最适合您特定安排的方法的内容。

你可以将 Scroll API 与查询方法、Query-by-Example 以及 Querydsl 结合使用。

备注

目前尚不支持使用基于字符串的查询方法进行滚动。使用存储的 @Procedure 查询方法也不支持滚动。

使用命名参数

默认情况下,Spring Data JPA 使用基于位置的参数绑定,如前面所有示例中所述。这使得在重构时,查询方法在参数位置上容易出错。为了解决这个问题,你可以使用 @Param 注解为方法参数指定一个具体的名称,并在查询中绑定该名称,如下例所示:

示例 14. 使用命名参数

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);
}
java
备注

方法参数根据它们在定义查询中的顺序进行了切换。

备注

从版本 4 开始,Spring 完全支持基于 -parameters 编译器标志的 Java 8 参数名称发现功能。通过在构建中使用此标志替代调试信息,您可以省略命名参数的 @Param 注解。

使用表达式

我们支持在手动定义的查询中使用受限表达式,这些查询通过 @Query 进行定义。在查询运行时,这些表达式会针对一组预定义的变量进行评估。

备注

如果您不熟悉值表达式,请参考值表达式基础来学习 SpEL 表达式和属性占位符。

Spring Data JPA 支持一个名为 entityName 的变量。它的使用方式为 select x from #{#entityName} x。它会插入与给定仓库关联的领域类型的 entityNameentityName 的解析规则如下: * 如果领域类型在 @Entity 注解中设置了 name 属性,则使用该属性值。 * 否则,使用领域类型的简单类名。

以下示例演示了在查询字符串中使用 #{#entityName} 表达式的一个用例,其中您希望定义一个包含查询方法和手动定义查询的存储库接口:

示例 15. 在仓库查询方法中使用 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);
}
java

为了避免在 @Query 注解的查询字符串中直接使用实体名称,你可以使用 #{#entityName} 变量。

备注

entityName 可以通过使用 @Entity 注解来自定义。对于 SpEL 表达式,orm.xml 中的自定义是不支持的。

当然,你可以直接在查询声明中使用 User,但那样的话,你也需要修改查询。通过引用 #entityName,你可以应对未来可能将 User 类重新映射到不同实体名称的情况(例如,使用 @Entity(name = "MyUser"))。

在查询字符串中使用 #{#entityName} 表达式的另一个用例是,如果你想为一个具体的领域类型定义一个通用的仓库接口,并为其创建专门的仓库接口。为了避免在具体接口中重复定义自定义查询方法,你可以在通用仓库接口的 @Query 注解的查询字符串中使用实体名称表达式,如下例所示:

示例 16. 在 Repository 查询方法中使用 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> {}
java

在前面的示例中,MappedTypeRepository 接口是扩展 AbstractMappedType 的几个领域类型的公共父接口。它还定义了通用的 findAllByAttribute(…) 方法,该方法可以在专门的存储库接口实例上使用。如果你现在在 ConcreteRepository 上调用 findByAllAttribute(…),查询将变为 select t from ConcreteType t where t.attribute = ?1

你也可以使用表达式来控制方法参数。在这些表达式中,实体名称不可用,但参数是可用的。它们可以通过名称或索引来访问,如下例所示。

示例 17. 在仓库查询方法中使用值表达式:访问参数

@Query("select u from User u where u.firstname = ?1 and u.firstname=?#{[0]} and u.emailAddress = ?#{principal.emailAddress}")
List<User> findByFirstnameAndCurrentUserWithCustomQuery(String firstname);
java

对于 like 条件,通常需要在字符串值参数的开头或结尾附加 %。这可以通过在绑定参数标记或 SpEL 表达式的开头或结尾附加 % 来实现。以下示例再次演示了这一点。

示例 18. 在仓库查询方法中使用值表达式:通配符快捷方式

@Query("select u from User u where u.lastname like %:#{[0]}% and u.lastname like %:lastname%")
List<User> findByLastnameWithSpelExpression(@Param("lastname") String lastname);
java

在使用 like 条件时,如果值来自不安全的来源,应对这些值进行清理,以确保它们不包含任何通配符,从而防止攻击者选择超出他们应有权限的数据。为此,SpEL 上下文中提供了 escape(String) 方法。该方法会将第一个参数中的所有 _% 实例前缀为第二个参数中的单个字符。结合 JPQL 和标准 SQL 中 like 表达式的 escape 子句,可以轻松清理绑定参数。

示例 19. 在存储库查询方法中使用值表达式:清理输入值

@Query("select u from User u where u.firstname like %?#{escape([0])}% escape ?#{escapeCharacter()}")
List<User> findContainingEscaped(String namePart);
java

在仓库接口中声明的方法 findContainingEscaped("Peter_") 将会找到 Peter_Parker,但不会找到 Peter Parker。使用的转义字符可以通过设置 @EnableJpaRepositories 注解的 escapeCharacter 进行配置。需要注意的是,SpEL 上下文中可用的方法 escape(String) 只会转义 SQL 和 JPQL 标准通配符 _%。如果底层数据库或 JPA 实现支持额外的通配符,这些通配符将不会被转义。

示例 20. 在仓库查询方法中使用值表达式:配置属性

@Query("select u from User u where u.applicationName = ?${spring.application.name:unknown}")
List<User> findContainingEscaped(String namePart);
java

你可以在查询方法中引用配置属性名称,包括回退机制,如果你希望在运行时从 Environment 中解析一个属性。该属性在查询执行时会被评估。通常,属性占位符解析为类似字符串的值。

其他方法

Spring Data JPA 提供了许多构建查询的方式。但有时,你的查询可能过于复杂,超出了这些技术的能力范围。在这种情况下,可以考虑:

  • 如果你还没有这样做,直接使用 @Query 自己编写查询即可。

  • 如果这不符合你的需求,考虑实现一个 自定义实现。这允许你在仓库中注册一个方法,同时完全由你自己决定实现方式。这使你可以:

    • 直接与 EntityManager 进行交互(编写纯 HQL/JPQL/EQL/原生 SQL 或使用 Criteria API

    • 利用 Spring Framework 的 JdbcTemplate(原生 SQL)

    • 使用其他第三方数据库工具。

  • 另一个选择是将查询放入数据库中,然后使用 Spring Data JPA 的 @StoredProcedure 注解,或者如果它是一个数据库函数,使用 @Query 注解 并通过 CALL 调用它。

当您需要对查询进行最大程度的控制,同时仍希望 Spring Data JPA 提供资源管理时,这些策略可能是最有效的。

修改查询

前面的所有部分都描述了如何声明查询以访问给定的实体或实体集合。你可以通过使用 Spring Data 存储库的自定义实现 中描述的自定义方法工具来添加自定义的修改行为。虽然这种方法适用于全面的自定义功能,但对于仅需要参数绑定的查询,你可以通过在查询方法上添加 @Modifying 注解来修改查询,如下例所示:

示例 21. 声明操作查询

@Modifying
@Query("update User u set u.firstname = ?1 where u.lastname = ?2")
int setFixedFirstnameFor(String firstname, String lastname);
java

这样做会触发方法上的注解查询作为更新查询而非选择查询执行。由于在执行修改查询后,EntityManager 可能包含过时的实体,我们不会自动清除它(有关详细信息,请参阅 EntityManager.clear()JavaDoc),因为这实际上会丢弃 EntityManager 中所有尚未刷新的挂起更改。如果你希望 EntityManager 自动清除,可以将 @Modifying 注解的 clearAutomatically 属性设置为 true

@Modifying 注解仅在与 @Query 注解结合使用时才相关。派生查询方法或自定义方法不需要此注解。

派生删除查询

Spring Data JPA 还支持派生删除查询,这使得你无需显式声明 JPQL 查询,如下例所示:

示例 22. 使用派生删除查询

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);
}
java

虽然 deleteByRoleId(…) 方法看起来基本上与 deleteInBulkByRoleId(…) 方法产生相同的结果,但这两个方法声明在运行方式上存在重要差异。顾名思义,后一种方法会针对数据库发出一个单一的 JPQL 查询(即在注解中定义的查询)。这意味着即使当前已加载的 User 实例也不会触发生命周期回调。

为了确保生命周期查询被实际调用,deleteByRoleId(…) 的调用会先执行一个查询,然后逐个删除返回的实例,这样持久化提供程序就可以在这些实体上实际调用 @PreRemove 回调。

事实上,派生的删除查询是运行查询并在结果上调用 CrudRepository.delete(Iterable<User> users) 的快捷方式,同时与其他 CrudRepositorydelete(…) 方法的实现保持行为一致。

备注

在删除大量对象时,您需要考虑性能影响,以确保有足够的内存可用。所有生成的对象在删除之前都会被加载到内存中,并且在刷新或完成事务之前会一直保留在会话中。

应用查询提示

要在你的仓库接口中声明的查询上应用 JPA 查询提示,你可以使用 @QueryHints 注解。它接受一个 JPA @QueryHint 注解数组以及一个布尔标志,用于可能禁用应用于分页时触发的额外计数查询的提示,如下例所示:

示例 23. 在仓库方法中使用 QueryHints

public interface UserRepository extends Repository<User, Long> {

@QueryHints(value = { @QueryHint(name = "name", value = "value")},
forCounting = false)
Page<User> findByLastname(String lastname, Pageable pageable);
}
java

前面的声明将配置的 @QueryHint 应用于实际的查询,但会将其忽略应用于触发计算总页数的计数查询。

向查询添加注释

有时候,你需要根据数据库性能来调试查询。你的数据库管理员展示给你的查询可能与你使用 @Query 编写的查询看起来非常不同,或者它可能与你认为 Spring Data JPA 生成的关于自定义查找器或通过示例查询的内容完全不同。

为了使这个过程更加容易,您可以通过应用 @Meta 注解在几乎任何 JPA 操作中插入自定义注释,无论是查询还是其他操作。

示例 24. 在仓库操作中应用 @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);
}
java

这个示例仓库中混合了自定义的查找器以及覆盖从 JpaRepository 继承的操作。无论哪种方式,@Meta 注解都允许你添加一个 comment,该注释将在查询发送到数据库之前插入到查询中。

同样重要的是要注意,这一特性并不仅限于查询。它还扩展到了 countexists 操作。虽然未展示,但它也扩展到了某些 delete 操作。

important

虽然我们已尽可能在所有地方应用此功能,但底层 EntityManager 的某些操作不支持注释。例如,entityManager.createQuery() 明确记录为支持注释,但 entityManager.find() 操作则不支持。

JPQL 日志记录和 SQL 日志记录都不是 JPA 的标准功能,因此每个提供商都需要进行自定义配置,如下面章节所示。

激活 Hibernate 注释

要在 Hibernate 中启用查询注释,你必须将 hibernate.use_sql_comments 设置为 true

如果你使用的是基于 Java 的配置设置,可以像这样进行配置:

示例 25. 基于 Java 的 JPA 配置

@Bean
public Properties jpaProperties() {

Properties properties = new Properties();
properties.setProperty("hibernate.use_sql_comments", "true");
return properties;
}
java

如果你有一个 persistence.xml 文件,你可以在那里应用它:

示例 26. 基于 persistence.xml 的配置

<persistence-unit name="my-persistence-unit">

...registered classes...

<properties>
<property name="hibernate.use_sql_comments" value="true" />
</properties>
</persistence-unit>
xml

最后,如果你使用的是 Spring Boot,那么你可以在 application.properties 文件中进行配置:

示例 27. 基于 Spring Boot 属性的配置

spring.jpa.properties.hibernate.use_sql_comments=true

要在 EclipseLink 中激活查询注释,你必须将 eclipselink.logging.level.sql 设置为 FINE

如果你正在使用基于 Java 的配置设置,可以这样操作:

示例 28. 基于 Java 的 JPA 配置

@Bean
public Properties jpaProperties() {

Properties properties = new Properties();
properties.setProperty("eclipselink.logging.level.sql", "FINE");
return properties;
}
java

如果你有一个 persistence.xml 文件,你可以在其中进行配置:

示例 29. 基于 persistence.xml 的配置

<persistence-unit name="my-persistence-unit">

...registered classes...

<properties>
<property name="eclipselink.logging.level.sql" value="FINE" />
</properties>
</persistence-unit>
xml

最后,如果你使用的是 Spring Boot,那么你可以在 application.properties 文件中进行配置:

示例 30. 基于 Spring Boot 属性的配置

spring.jpa.properties.eclipselink.logging.level.sql=FINE

配置 Fetch 和 Load 图表

JPA 2.1 规范引入了对 FetchGraph 和 LoadGraph 的支持,我们也通过 @EntityGraph 注解提供了支持,该注解允许你引用 @NamedEntityGraph 定义。你可以在实体上使用该注解来配置查询结果的获取计划。获取的类型(FetchLoad)可以通过在 @EntityGraph 注解上使用 type 属性来配置。更多信息请参见 JPA 2.1 规范 3.7.4 章节。

以下示例展示了如何在实体上定义一个命名的实体图:

示例 31. 在实体上定义一个命名的实体图。

@Entity
@NamedEntityGraph(name = "GroupInfo.detail",
attributeNodes = @NamedAttributeNode("members"))
public class GroupInfo {

// default fetch mode is lazy.
@ManyToMany
List<GroupMember> members = new ArrayList<GroupMember>();


}
java

以下示例展示了如何在仓库查询方法中引用一个命名的实体图:

示例 32. 在仓库查询方法上引用命名的实体图定义。

public interface GroupRepository extends CrudRepository<GroupInfo, String> {

@EntityGraph(value = "GroupInfo.detail", type = EntityGraphType.LOAD)
GroupInfo getByGroupName(String name);

}
java

也可以通过使用 @EntityGraph 来定义临时的实体图。所提供的 attributePaths 会被转换为相应的 EntityGraph,而不需要显式地在领域类型中添加 @NamedEntityGraph,如下例所示:

示例 33. 在仓库查询方法上使用临时实体图定义

public interface GroupRepository extends CrudRepository<GroupInfo, String> {

@EntityGraph(attributePaths = { "members" })
GroupInfo getByGroupName(String name);

}
java

滚动

滚动(Scrolling)是一种更细粒度的方式,用于迭代处理较大的结果集块。滚动包括稳定的排序、滚动类型(基于偏移量或键集的滚动)以及结果限制。您可以通过使用属性名称定义简单的排序表达式,并通过查询派生使用 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
}

// obtain the next Scroll
users = repository.findFirst10ByLastnameOrderByFirstname("Doe", users.positionAt(users.size() - 1));
} while (!users.isEmpty() && users.hasNext());
java
备注

ScrollPosition 标识元素在整个查询结果中的确切位置。查询执行将位置参数视为 排他性,结果将从给定位置 之后 开始。ScrollPosition#offset()ScrollPosition#keyset() 作为 ScrollPosition 的特殊表现形式,表示滚动操作的开始。

备注

上述示例展示了静态排序和限制。你可以定义接受 Sort 对象的查询方法,以定义更复杂的排序顺序或基于每个请求的排序。类似地,提供 Limit 对象允许你基于每个请求定义动态限制,而不是应用静态限制。更多关于动态排序和限制的信息,请参阅查询方法详情

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
}
java

使用偏移量进行滚动

偏移滚动(Offset scrolling)与分页(pagination)类似,它使用一个偏移计数器(Offset counter)来跳过一定数量的结果,并让数据源只返回从给定偏移量开始的结果。这种简单的机制避免了将大量结果发送到客户端应用程序。然而,大多数数据库需要在服务器返回结果之前完全物化(materialize)整个查询结果。

示例 34. 在 Repository 查询方法中使用 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
java
  • 从无偏移开始,以包含位置 0 处的元素。

警告

ScollPosition.offset()ScollPosition.offset(0L) 之间存在差异。前者表示滚动操作的开始,没有指向特定的偏移量,而后者标识结果中的第一个元素(位于位置 0)。考虑到滚动的独占性质,使用 ScollPosition.offset(0) 会跳过第一个元素,并将其转换为偏移量 1

使用键集过滤进行滚动

基于偏移量的分页要求大多数数据库在服务器能够返回结果之前,需要将整个结果物化。因此,虽然客户端只看到请求结果的一部分,但服务器需要构建完整的结果,这会导致额外的负载。

Keyset-Filtering 方法通过利用数据库的内置功能来实现子集检索,旨在减少单个查询的计算和 I/O 需求。该方法通过将一组键传递给查询来维护这些键,以便继续滚动浏览,从而有效地修改过滤条件。

Keyset-Filtering 的核心思想是使用稳定的排序顺序开始检索结果。当您想要滚动到下一个数据块时,您会获取一个 ScrollPosition,用于重建排序结果中的位置。ScrollPosition 捕获当前 Window 中最后一个实体的键集。为了运行查询,重建过程会重写条件子句,以包含所有排序字段和主键,从而使数据库能够利用潜在的索引来运行查询。数据库只需从给定的键集位置构建一个更小的结果,而不需要完全物化一个大的结果集,然后跳过结果直到达到特定的偏移量。

注意

Keyset-Filtering 要求用于排序的 keyset 属性不能为可空值。这一限制是由于存储层对比较运算符的 null 值处理方式以及需要在索引源上运行查询的需求所导致的。如果在可空属性上使用 Keyset-Filtering,可能会导致意外的结果。

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
java
  • 从最开始的地方开始,不要应用额外的过滤。

Keyset-Filtering 在数据库中存在与排序字段匹配的索引时效果最佳,因此静态排序效果良好。应用 Keyset-Filtering 的滚动查询要求查询返回用于排序的属性,并且这些属性必须在返回的实体中进行映射。

你可以使用接口和 DTO 投影,但要确保包含所有已排序的属性,以避免 keyset 提取失败。

在指定 Sort 排序顺序时,只需包含与查询相关的排序属性即可;如果您不希望确保查询结果的唯一性,则无需强制执行。键集查询机制会通过包含主键(或复合主键的剩余部分)来修正您的排序顺序,以确保每个查询结果都是唯一的。