JPA Query Methods

Spring Data JPA 提供多种用于创建查询的方法,包括:

  • 使用谓词派生查询

  • 声明查询,通过在方法名中包含关键词或通过 @Query 注解来指定

  • 编写手动定义的查询,其中 SQL 将作为字符串传递

  • 使用 NamedQueries 通过命名约定或 @NamedQuery 注解定义查询

  • 使用 @QueryHints 应用 JPA 查询提示

  • 使用 @Meta 注解向查询添加注释 for 调试

  • 配置提取图和加载图以优化实体图提取

此部分描述了使用 Spring Data JPA 创建查询的多种方法。

Query Lookup Strategies

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

衍生查询配有谓词 IsStartingWith, StartingWith, StartsWith, IsEndingWith, EndingWith, EndsWith,IsNotContaining, NotContaining, NotContains, IsContaining, Containing, Contains,这些查询的相应参数将得到清理。这意味着,如果参数实际上包含某个特定通配符中被 LIKE 识别的字符,这些字符将得到转义,以便只能作为文本匹配。所使用的转义字符可以通过设置 @EnableJpaRepositories 注释的 escapeCharacter 来配置。与 Using SpEL Expressions 相比。

Declared Queries

尽管从方法名称中获取衍生查询非常方便,但我们可能会遇到一种情况,其中方法名称解析器不支持我们想使用的关键字,或者方法名称会变得非常难看。因此,可以通过命名约定使用 JPA 命名查询(有关更多信息,请参阅 Using JPA Named Queries),或者通过 @Query 注释查询方法(有关详细信息,请参阅 Using @Query)。

Query Creation

通常,JPA 的查询创建机制的工作原理如 查询方法 中所述。下面的示例展示了 JPA 查询方法如何转换为:

Example 1. Query creation from method names
public interface UserRepository extends Repository<User, Long> {

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

我们使用 JPA 标准 API 依据此项创建查询,但从本质上来说,翻译成以下查询:select u from User u where u.emailAddress = ?1 and u.lastname = ?2。正如 Property Expressions 中所述,Spring Data JPA 执行属性检查并遍历嵌套属性。

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

Table 1. Supported keywords inside method names
Keyword Sample JPQL snippet

Distinct

findDistinctByLastnameAndFirstname

select distinct &#8230;&#8203; 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 &lt; ?1

LessThanEqual

findByAgeLessThanEqual

… where x.age &lt;= ?1

GreaterThan

findByAgeGreaterThan

… where x.age &gt; ?1

GreaterThanEqual

findByAgeGreaterThanEqual

… where x.age &gt;= ?1

After

findByStartDateAfter

… where x.startDate &gt; ?1

Before

findByStartDateBefore

… where x.startDate &lt; ?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 (与附带的 % 一起绑定参数)

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 &lt;&gt; ?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)

InNotIn 还将 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 来最好地捕获您要查找的信息,因为您可能还需要投影来捕获结果集。

Annotation-based Configuration

基于注解的配置的优点是不需要编辑另一个配置文件,从而降低了维护工作量。为此好处付出的代价是需要为每个新的查询声明重新编译您的领域类。

Example 2. Annotation-based named query configuration
@Entity
@NamedQuery(name = "User.findByEmailAddress",
  query = "select u from User u where u.emailAddress = ?1")
public class User {

}

Using JPA Named Queries

示例使用 <named-query /> 元素和 @NamedQuery 注解。这些配置元素的查询必须在 JPA 查询语言中定义。当然,您也可以使用 <named-native-query />@NamedNativeQuery。这些元素允许您通过放弃数据库平台独立性而在本地 SQL 中定义查询。

XML Named Query Definition

要使用 XML 配置,请将必要的 <named-query /> 元素添加到 classpath 的 META-INF 文件夹中位于的 orm.xml JPA 配置文件中。通过使用某些定义的命名约定,可以启用命名查询的自动调用。有关更多详细信息,请参见以下内容。

Example 3. XML named query configuration
<named-query name="User.findByLastname">
  <query>select u from User u where u.lastname = ?1</query>
</named-query>

该查询具有用于在运行时解析它的特殊名称。

Declaring Interfaces

要允许这些命名查询,请如下指定 UserRepositoryWithRewriter

Example 4. Query method declaration in UserRepository
public interface UserRepository extends JpaRepository<User, Long> {

  List<User> findByLastname(String lastname);

  User findByEmailAddress(String emailAddress);
}

Spring Data 尝试将对这些方法的调用解析为命名查询,从已配置领域类的简单名称开始,后面跟用点分隔的方法名。因此,先前的示例将使用前面定义的命名查询,而不是尝试从方法名创建查询。

Using @Query

使用命名查询来声明实体的查询是一种有效的方法,适用于少数查询。由于查询本身与运行它们的 Java 方法相关,因此您可以使用 Spring Data JPA @Query 注解直接绑定它们,而不是将它们注释到领域类。这使领域类摆脱了与持久性相关的信息,并将查询与存储库接口并置。

为查询方法添加注释的查询优先于使用 @NamedQuery 或在 orm.xml 中声明的命名查询定义的查询。

以下示例显示了使用 @Query 注解创建的查询:

Example 5. Declare query at the query method using @Query
public interface UserRepository extends JpaRepository<User, Long> {

  @Query("select u from User u where u.emailAddress = ?1")
  User findByEmailAddress(String emailAddress);
}

Applying a QueryRewriter

有时,无论您尝试应用多少特性,似乎都无法让 Spring Data JPA 在将其发送到 EntityManager 之前将您想应用于查询的一切都应用到查询。

您可以在将查询发送到 EntityManager 之前直接掌握它并“重写”它。也就是说,您可以在最后一刻进行任何更改。

Example 6. Declare a QueryRewriter using @Query
public interface MyRepository extends JpaRepository<User, Long> {

		@Query(value = "select original_user_alias.* from SD_USER original_user_alias",
                nativeQuery = true,
				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。

您可以像这样编写查询重写程序:

Example 7. Example 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 方法的一部分。

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

Example 8. Repository that provides the 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 的用途,可能建议使用多个 QueryRewriter,每个 QueryRewriter 都在应用程序上下文中注册。

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

Using Advanced LIKE Expressions

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

Example 9. Advanced like expressions in @Query
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 模式进行扩充。

Native Queries

@Query 注解允许通过将 nativeQuery 标志设置为 true 来运行本机查询,如下例所示:

Example 10. Declare a native query at the query method using @Query
public interface UserRepository extends JpaRepository<User, Long> {

  @Query(value = "SELECT * FROM USERS WHERE EMAIL_ADDRESS = ?1", nativeQuery = true)
  User findByEmailAddress(String emailAddress);
}

Spring Data JPA 目前不支持本机查询的动态排序,因为它必须处理已声明的实际查询,而它无法可靠地对本机 SQL 执行此操作。但是,您可以使用本机查询进行分页,方法是自己指定计数查询,如下例所示:

Example 11. Declare native count queries for pagination at the query method by using @Query
public interface UserRepository extends JpaRepository<User, Long> {

  @Query(value = "SELECT * FROM USERS WHERE LASTNAME = ?1",
    countQuery = "SELECT count(*) FROM USERS WHERE LASTNAME = ?1",
    nativeQuery = true)
  Page<User> findByLastname(String lastname, Pageable pageable);
}

类似的方法也适用于已命名的本机查询,方法是在您的查询副本之后追加 .count 后缀。不过,您可能需要为您的计数查询注册一个结果集映射。

Using Sort

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

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

但是,使用 Sort 和 xref:jpa/query-methods.adoc#jpa.query-methods.at-query[@Query 可以悄悄潜入包含 ORDER BY 子句中的函数的非路径检查 Order 实例。这是可能的,因为 Order 附加到给定的查询字符串中。默认情况下,Spring Data JPA 拒绝任何包含函数调用的 Order 实例,但可以使用 JpaSort.unsafe 添加可能不安全的排序。

以下示例使用 SortJpaSort,包括 JpaSort 上的不安全选项:

Example 12. Using Sort and 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
1 有效的 Sort 表达式指向域模型中的属性。
2 包含函数调用的无效 Sort。抛出异常。
3 包含明确 unsafe Order 的有效 Sort
4 有效的 Sort 表达式指向别名函数。

Scrolling Large Query Results

当处理大型数据集时,repositories.scrolling 可以帮助有效地处理这些结果,而无需将所有结果加载到内存中。

您有多种选择来使用大型查询结果:

  1. Paging.您在上一章中了解了 Pageable and PageRequest

  2. Offset-based scrolling。这是一种比分页更轻量级的变体,因为它不需要总结果计数。

  3. Keyset-baset scrolling.此方法避免了 the shortcomings of offset-based result retrieval by leveraging database indexes

对于您的特定布置,请阅读 [使用哪种方法最合适,repositories.scrolling.guidance]

可以在查询方法中使用 Scroll API、Query-by-ExampleQuerydsl

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

Using Named Parameters

在默认情况下,Spring Data JPA 使用基于位置的参数绑定,如前面所有示例所述。它在重构有关参数位置时会使查询方法易于出错。为解决此问题,可以使用 @Param 注解为方法参数提供具体名称,并按查询进行绑定,如下面的示例所示:

Example 13. Using named parameters
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 注解以用于命名参数。

Using SpEL Expressions

从 Spring Data JPA 版本 1.4 开始,我们支持在以 @Query 定义的手动定义查询中使用受限的 SpEL 模板表达式。在运行查询时,这些表达式相对于预定义变量集进行评估。Spring Data JPA 支持名为 entityName 的变量。其用法是 select x from #{#entityName} x。它插入与给定存储库关联的域类型的 entityNameentityName 的解析方式如下:如果域类型在 @Entity 注解中设置了名称属性,则使用该属性。否则,使用域类型的简单类名。

以下示例演示了在查询字符串中针对 #{#entityName} 表达式的用例,其中您要使用查询方法和手动定义查询来定义存储库接口:

Example 14. Using SpEL expressions in repository query methods - 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} 变量。

可以使用 @Entity 注解自定义 entityName。SpEL 表达式不支持 orm.xml 中的自定义。

当然,您也可以直接在查询声明中使用 User,但这需要您也更改查询。对 #entityName 的引用会拾取 User 类日后潜在的重新映射到不同的实体名称(例如,通过使用 @Entity(name = "MyUser"))。

在查询字符串中针对 #{#entityName} 表达式的另一个用例是,当您要使用专门于具体域类型的专用存储库接口来定义泛型存储库接口。为避免在具体接口上重复自定义查询方法的定义,您可以在泛型存储库接口中 @Query 注解的查询字符串中使用实体名称表达式,如下面的示例所示:

Example 15. Using SpEL expressions in repository query methods - entityName with inheritance
@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 上调用 findByAllAttribute(…),查询将变为 select t from ConcreteType t where t.attribute = ?1

用于处理参数的 SpEL 表达式还可用于处理方法参数。在这些 SpEL 表达式中,实体名称不可用,但参数可用。可以通过名称或索引来访问它们,如下面的示例所示。

Example 16. Using SpEL expressions in repository query methods - accessing arguments.
@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 表达式来完成。以下示例再次对此进行了演示。

Example 17. Using SpEL expressions in repository query methods - wildcard shortcut.
@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 子句结合使用,可以轻松清理绑定参数。

Example 18. Using SpEL expressions in repository query methods - sanitizing input values.
@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 实现支持其他通配符,则不会对其进行转义。

Other Methods

Spring Data JPA 提供了许多构建查询的方法。但有时,您的查询可能对所提供技术来说过于复杂。在这种情况下,请考虑:

  • 如果您还没有查询,则只需要使用 @Query 自己编写查询即可。

  • 如果这种方法不满足您的需求,可以考虑实现 custom implementation。这样,您可以在存储库中注册一个方法,同时让您完全负责实现。这使您可以:

    • 通过 EntityManager 直接对话(编写纯 HQL/JPQL/EQL/本机 SQL 或使用 Criteria API

    • 利用 Spring 框架的 JdbcTemplate(本机 SQL)

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

  • 另一种选择是将查询放在数据库中,然后使用 Spring Data JPA 的 xref:jpa/stored-procedures.adoc[@StoredProcedure 注解或如果数据库功能使用 xref:jpa/query-methods.adoc#jpa.query-methods.at-query[@Query 注解,并用 CALL 调用它。

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

Modifying Queries

先前所有部分描述了如何声明查询,以访问一个给定的实体或实体集合。您还可以通过使用 用于 Spring 数据仓库的自定义实现 中所述的自定义方法工具添加自定义修改行为。由于此方法对于全面自定义功能是可行的,因此可以通过使用 @Modifying 注释查询方法来修改只需要参数绑定的查询,如下例所示:

Example 19. Declaring manipulating queries
@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 注解结合使用时才相关。派生的查询方法或自定义方法不要求此注释。

Derived Delete Queries

Spring Data JPA 还支持派生的删除查询,它允许您避免必须显式声明 JPQL 查询,如下面的示例所示:

Example 20. Using a derived delete query
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(…) 方法的实现保持同步。

Applying Query Hints

要对存储库接口中声明的查询应用 JPA 查询提示,可以使用 @QueryHints 注解。它采用一个 JPA @QueryHint 注解数组加上一个布尔标志,该标志可用于禁用应用于应用分页时触发的附加计数查询的提示,如下例所示:

Example 21. Using QueryHints with a repository method
public interface UserRepository extends Repository<User, Long> {

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

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

Adding Comments to Queries

有时,你可能需要基于数据库性能调试一个查询。数据库管理人员向你展示的查询可能与你在编写时使用 @Query 所编写的内容非常不同,或者它可能看起来与你假设 Spring Data JPA 生成的定制查找器或查询结果完全不一样。

为了让此过程更轻松,你可以在几乎任何 JPA 操作中插入评论,无论它是查询还是其他操作,这可以通过应用 @Meta 注解来实现。

Example 22. Apply @Meta annotation to repository operations
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 注解可让你添加 comments,这些注释将在查询发送到数据库前插入到查询中。

同样重要的是,这一特性不只限于查询。它扩展到 countexists 操作。虽然没有显示,但它也扩展到某些 delete 操作。

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

JPQL 日志记录和 SQL 日志记录都不是 JPA 中的标准,因此每个提供者都需要定制配置,如下面的部分所示。

Activating Hibernate comments

要激活 Hibernate 中的查询注释,你必须将 hibernate.use_sql_comments 设置为 true

如果你使用基于 Java 的配置设置,可以这样做:

Example 23. Java-based JPA configuration
@Bean
public Properties jpaProperties() {

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

如果你有一个 persistence.xml 文件,则可以在其中应用:

Example 24. persistence.xml-based configuration
<persistence-unit name="my-persistence-unit">

   ...registered classes...

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

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

Example 25. Spring Boot property-based configuration
spring.jpa.properties.hibernate.use_sql_comments=true

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

如果你使用基于 Java 的配置设置,可以这样做:

Example 26. Java-based JPA configuration
@Bean
public Properties jpaProperties() {

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

如果你有一个 persistence.xml 文件,则可以在其中应用:

Example 27. persistence.xml-based configuration
<persistence-unit name="my-persistence-unit">

   ...registered classes...

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

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

Example 28. Spring Boot property-based configuration
spring.jpa.properties.eclipselink.logging.level.sql=FINE

Configuring Fetch- and LoadGraphs

JPA 2.1 规范引入了对指定提取和负载图的支持,我们也使用 @EntityGraph 注解支持这一规范,它允许你引用一个 @NamedEntityGraph 定义。你可以在实体上使用该注释来配置所得查询的提取计划。可以通过使用 @EntityGraph 注解上的 type 属性来配置提取的类型(FetchLoad)。有关更多参考,请参阅 JPA 2.1 规范 3.7.4。

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

Example 29. Defining a named entity graph on an entity.
@Entity
@NamedEntityGraph(name = "GroupInfo.detail",
  attributeNodes = @NamedAttributeNode("members"))
public class GroupInfo {

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

  …
}

以下示例展示了如何在存储库查询方法上引用命名实体图:

Example 30. Referencing a named entity graph definition on a repository query method.
public interface GroupRepository extends CrudRepository<GroupInfo, String> {

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

}

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

Example 31. Using AD-HOC entity graph definition on an repository query method.
public interface GroupRepository extends CrudRepository<GroupInfo, String> {

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

}

Scrolling

滚动是迭代处理较大结果集区块的一种更细粒度的处理方式。滚动包含稳定的排序、滚动类型(基于偏移或基于键集的滚动)以及结果限制。你可以使用属性名称定义简单的排序表达式,并使用 xref:repositories/query-methods-details.adoc#repositories.limit-query-result[TopFirst 关键字通过查询派生定义静态结果限制。你可以联接表达式以将多个条件收集到一个表达式中。 滚动查询返回一个 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());

WindowIterator 提供了一个实用工具,可通过消除检查是否存在下一个 Window 和应用 ScrollPosition 的需求来简化跨 Window 的滚动。

WindowIterator<User> users = WindowIterator.of(position -> repository.findFirst10ByLastnameOrderByFirstname("Doe", position))
  .startingAt(OffsetScrollPosition.initial());

while (users.hasNext()) {
  User u = users.next();
  // consume the user
}

Scrolling using Offset

偏移量滚动类似于分页,它使用偏移量计数器跳过一定数目的结果,并让数据源仅从给定偏移量开始返回结果。此简单机制可避免向客户端应用程序发送较大的结果。但是,在服务器返回结果之前,大多数数据库都需要实现完整的查询结果。

Example 32. Using OffsetScrollPosition with Repository Query Methods
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
1 从位置 0 上的起始偏移量开始。

Scrolling using Keyset-Filtering

基于偏移量的方法需要大多数数据库在服务器返回结果之前实现全部结果的物化。因此,当客户端只看到请求结果的一部分时,服务器需要构建全部结果,这会导致额外的负载。

键集过滤方法通过利用数据库的内置功能来检索结果子集,旨在降低单个查询的计算和 I/O 需求。此方法维护一组键用于通过查询中传入键来恢复滚动,有效修正筛选条件。

键集过滤的核心思想是使用稳定的排序顺序开始检索结果。一旦想要滚动到下一个块,就可以获取用于重建排序结果中位置的 ScrollPositionScrollPosition 捕获当前 Window 中最后一个实体的键集。要运行查询,重建会重写条件子句以包括所有排序字段和主键,以便数据库可以利用潜在的索引来运行查询。数据库只需要根据给定的键集位置构建一个更小的结果,而无需完全实现一个较大的结果,然后跳过一些结果直到达到特定的偏移量。

键集过滤需要键集属性(用于排序的属性)为非空。此限制适用于由于比较运算符对存储特定的 null 值处理以及由于需要针对索引来源运行查询。针对空属性的键集过滤会导致意外的结果。

Using KeysetScrollPosition with Repository Query Methods
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
1 从一开始开始,不使用其他筛选条件。

当数据库包含与排序字段相匹配的索引时,键集过滤效果最佳,因此静态排序效果很好。应用键集过滤的滚动查询需要将排序顺序中使用的属性通过查询返回,而这些属性必须映射在返回的实体中。

可以使用接口和 DTO 投影,但是确保包括已对其进行排序的所有属性,以避免键集提取失败。

在指定 Sort 顺序时,包含与查询相关的排序属性就足够了;如果您不想确保唯一查询结果,就不需要这样做。键集查询机制会通过包括主键(或复合主键的任何剩余部分)来修正排序顺序,确保每个查询结果都是唯一的。