|
此版本仍在开发中,尚未被视为稳定版。如需使用最新的稳定版本,请访问 Spring Data JPA 4.0.4! |
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 查询方法会被转换成什么形式:
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 支持的关键字,以及包含该关键字的方法所对应的翻译结果:
| 关键字 | 示例 | JPQL 片段 |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
In 和 NotIn 也接受任何 Collection 的子类作为参数,以及数组或可变参数(varargs)。有关同一逻辑运算符的其他语法形式,请参阅Repository 查询关键字。 |
|
然而,后一个查询会将焦点仅限于
这个查询到底有什么意义呢?是为了查找具有某个特定姓氏的人数吗?是为了查找具有该绑定姓氏的不同人数吗?
还是为了查找不同姓氏的数量?(最后一种情况完全是另一种查询!)
有时使用 |
使用 JPA 命名查询
示例使用了 <named-query /> 元素和 @NamedQuery 注解。这些配置元素所对应的查询必须使用 JPA 查询语言来定义。当然,你也可以使用 <named-native-query /> 或 @NamedNativeQuery。这些元素允许你使用原生 SQL 来定义查询,但会丧失数据库平台的独立性。 |
XML 命名查询定义
要使用 XML 配置,请将必要的 <named-query /> 元素添加到位于类路径下 orm.xml 文件夹中的 META-INF JPA 配置文件中。通过采用某些定义好的命名约定,即可启用对命名查询的自动调用。更多详细信息,请参见下文。
<named-query name="User.findByLastname">
<query>select u from User u where u.lastname = ?1</query>
</named-query>
该查询具有一个特殊名称,用于在运行时解析它。
声明接口
为了启用这些命名查询,请按如下方式指定 UserRepository:
public interface UserRepository extends JpaRepository<User, Long> {
List<User> findByLastname(String lastname);
User findByEmailAddress(String emailAddress);
}
Spring Data 会尝试将对这些方法的调用解析为命名查询,其名称以所配置领域类的简单名称开头,后跟一个点号(.)和方法名。 因此,前面的示例将使用之前定义的命名查询,而不是尝试根据方法名创建查询。
使用@Query
使用命名查询(named queries)来为实体声明查询是一种有效的方法,在查询数量较少时效果良好。由于这些查询本身与执行它们的 Java 方法紧密关联,因此实际上可以直接使用 Spring Data JPA 的 @Query 注解将查询绑定到对应的方法上,而无需将查询注解添加到领域类中。这样做可以避免在领域类中混入持久化相关的具体信息,并将查询逻辑与仓库接口放在一起。
通过查询方法注解指定的查询优先于使用 @NamedQuery 定义的查询或在 orm.xml 中声明的命名查询。
以下示例展示了一个使用 @Query 注解创建的查询:
@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 表达式,如下例所示:
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 注解可以执行原生查询,如下例所示:
@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 可以重写简单的查询以支持分页和排序。
更复杂的查询则要求类路径中包含 JSqlParser,或者在您的代码中声明一个 countQuery。
更多详细信息,请参见下面的示例。 |
@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 后缀即可。不过,你可能需要为你的计数查询注册一个结果集映射。
除了获取映射结果外,原生查询还允许你通过将方法的返回类型指定为 Tuple 容器,从数据库中读取原始的 Map。
生成的 Map 包含键/值对,其中键表示实际的数据库列名,值表示对应的数据。
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)
}
| 1 | 由 Map 支持的单个 Tuple 结果。 |
| 2 | 由 Map 支持的多个 Tuple 结果。 |
| 基于字符串的元组查询仅由 Hibernate 支持。 EclipseLink 仅支持基于 Criteria 的元组查询。 |
查询内省与重写
Spring Data JPA 提供了丰富的功能,可用于执行各种类型的查询。 具体来说,给定一个已声明的查询,Spring Data JPA 可以:
-
内省查询以获取其投影,并为接口投影运行元组查询
-
如果查询使用构造函数表达式,则使用 DTO 投影;当查询声明了实体别名或仅包含多个表达式的多选(multi-select)时,请重写该投影。
-
应用动态排序
-
派生一个
COUNT查询
为此,我们内置了专门针对 HQL(Hibernate)和 EQL(EclipseLink)方言的查询解析器,因为这些方言定义明确。
而 SQL 在不同方言之间则存在相当大的差异。
正因如此,Spring Data 无法支持所有复杂程度的查询。
我们并非通用的 SQL 解析器库,而是旨在通过简化查询执行来提升开发者的生产力。
我们内置的 SQL 查询增强器仅支持用于内省的简单查询,例如 COUNT 查询推导。
对于更复杂的查询,则需要使用 JSqlParser,或者通过 COUNT 显式提供一个 @Query(countQuery=…) 查询。
如果 JSqlParser 位于类路径中,Spring Data JPA 将在原生查询中使用它。
对于更精细的选择控制,您可以使用 @EnableJpaRepositories 配置 QueryEnhancerSelector:
@Configuration
@EnableJpaRepositories(queryEnhancerSelector = MyQueryEnhancerSelector.class)
class ApplicationConfig {
// …
}
QueryEnhancerSelector 是一个策略接口,旨在根据特定查询选择 QueryEnhancer。
如果您愿意,也可以提供自己的 QueryEnhancer 实现。
应用 QueryRewriter
有时候,无论你尝试应用多少功能,似乎都无法让 Spring Data JPA 在将查询发送给 EntityManager 之前,应用你希望的所有内容。
你可以在查询发送给 EntityManager 之前获取该查询并对其进行“重写”。
也就是说,你可以在最后一刻对查询进行任意修改。
查询重写适用于实际的查询,在适用的情况下,也适用于计数查询。
计数查询经过了优化,因此要么没有必要执行,要么会通过其他方式(例如,如果存在一个外层事务,则从 Hibernate 的 SelectionQuery 中派生)来获取计数值。
@Query 和 @NativeQuery 声明一个 QueryRewriterpublic 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。
你可以像这样编写一个查询重写器:
QueryRewriterpublic 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 的注解之一,还是将其作为 @Bean 类中某个 @Configuration 方法的一部分。
另一种选择是让仓库本身实现该接口。
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 实例。 |
使用排序
排序可以通过提供 PageRequest 或直接使用 Sort 来实现。Order 中 Sort 实例实际使用的属性必须与您的领域模型相匹配,这意味着它们需要解析为查询中使用的某个属性或别名。JPQL 将其定义为状态字段路径表达式。
使用任何不可引用的路径表达式都会导致一个Exception。 |
然而,将 Sort 与 @Query 一起使用,可以让包含函数的非路径检查 Order 实例潜入 ORDER BY 子句中。这是可能的,因为 Order 会被附加到给定的查询字符串中。默认情况下,Spring Data JPA 会拒绝任何包含函数调用的 Order 实例,但您可以使用 JpaSort.unsafe 来添加潜在不安全的排序。
以下示例使用了 Sort 和 JpaSort,其中包括在 JpaSort 上使用了一个不安全(unsafe)的选项:
Sort 和 JpaSortpublic 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 | 包含显式不安全Sort的有效Order。 |
| 4 | 指向别名函数的有效 Sort 表达式。 |
JpaSort.unsafe(…) 的限制
JpaSort.unsafe(…) 以两种模式运行:
-
当与派生查询或基于字符串的查询一起使用时,排序字符串将附加到查询语句中。
-
当与示例查询(Query by Example)或规范(Specifications,使用
CriteriaQuery)一起使用时,排序表达式将被解析并作为表达式添加到CriteriaQuery中。 -
JpaSort.JpaOrder.withUnsafe(…)使用当前的方向、大小写敏感性和空值处理规则,根据给定的属性创建一个新的JpaSort。 查询表达式可以包含函数调用、各种子句(例如CASE WHEN)、算术表达式或属性路径。 排序转换不支持子查询表达式、TREAT和CAST。
滚动处理大型查询结果
在处理大型数据集时,滚动可以帮助高效地处理这些结果,而无需将所有结果加载到内存中。
你有多种选项来处理大型查询结果:
-
分页。 您在上一章中已经学习了
Pageable和PageRequest。 -
基于偏移量的滚动。 这是一种比分页更轻量的变体,因为它不需要总结果数。
-
基于键集的滚动。 此方法通过利用数据库索引,避免了基于偏移量的结果检索的缺点。
阅读更多关于哪种方法最适合您的特定配置。
您可以将 Scroll API 与查询方法、按示例查询(Query-by-Example) 以及 Querydsl 一起使用。
基于字符串的查询方法尚不支持滚动。
使用存储的 @Procedure 查询方法也不支持滚动。 |
使用命名参数
默认情况下,Spring Data JPA 使用基于位置的参数绑定,如前面所有示例中所述。
当重构方法时,如果参数位置发生变化,这种绑定方式会使查询方法更容易出错。
为了解决这个问题,您可以使用 @Param 注解为方法参数指定一个具体名称,并在查询中绑定该名称,如下例所示:
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 完全支持基于 Java 8 的 -parameters 编译器标志进行参数名称发现。在构建过程中使用此标志(作为调试信息的替代方案),您可以省略命名参数上的 @Param 注解。 |
参数化查询和表达式
我们支持在使用 @Query 手动定义的查询中使用受限表达式。
当查询执行时,这些表达式会根据一组预定义的变量进行求值。
| 如果您不熟悉值表达式,请参阅值表达式基础,以了解 SpEL 表达式和属性占位符。 |
Spring Data JPA 支持一个名为 entityName 的模板变量。
其用法为 select x from #{#entityName} x。
它会插入与给定仓库关联的领域类型的 entityName。
entityName 的解析规则如下:
* 如果领域类型在 @Entity 注解中设置了 name 属性,则使用该属性值。
* 否则,使用领域类型的简单类名。
以下示例演示了在查询字符串中使用 #{#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 注解的查询字符串中使用实体名称表达式,如下例所示:
@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(…),该方法可用于专门化仓库接口的实例。
如果现在在 findAllByAttribute(…) 上调用 ConcreteRepository,生成的查询语句将为 select t from ConcreteType t where t.attribute = ?1。
您还可以使用表达式来控制方法参数。在这些表达式中,实体名称不可用,但方法参数是可用的。它们可以通过名称或索引进行访问,如下例所示。
@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 表达式前或后加上 % 来实现。
下面的示例再次演示了这一点。
@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 中 escape 表达式所提供的 like 子句,即可轻松清理绑定参数。
@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。
所使用的转义字符可通过设置 escapeCharacter 注解的 @EnableJpaRepositories 属性进行配置。
请注意,SpEL 上下文中可用的 escape(String) 方法仅会转义 SQL 和 JPQL 标准通配符 _ 和 %。
如果底层数据库或 JPA 实现支持额外的通配符,则这些通配符将不会被转义。
@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,或使用Criteria API) -
利用 Spring Framework 的
JdbcTemplate(原生 SQL) -
使用另一个第三方数据库工具包。
-
-
另一种选择是将查询放入数据库中,然后使用 Spring Data JPA 的
@StoredProcedure注解,或者如果是数据库函数,则使用@Query注解,并通过CALL进行调用。
当你需要对查询拥有最大程度的控制,同时又希望 Spring Data JPA 提供资源管理时,这些策略可能最为有效。
修改查询
前面所有章节描述了如何声明查询以访问某个实体或实体集合。
你可以通过使用Spring Data Repository 的自定义实现中所述的自定义方法功能来添加自定义的修改行为。
由于这种方法适用于全面的自定义功能,因此对于仅需参数绑定的修改查询,你可以通过在查询方法上添加 @Modifying 注解来实现,如下例所示:
@Modifying
@Query("update User u set u.firstname = ?1 where u.lastname = ?2")
int setFixedFirstnameFor(String firstname, String lastname);
这样做会触发方法上标注的查询作为更新查询(updating query)执行,而不是选择查询(selecting query)。由于在执行修改查询后,EntityManager 中可能包含过时的实体,因此我们不会自动清除它(有关详情,请参见 https://jakarta.ee/specifications/persistence/2.2/apidocs/javax/persistence/entitymanager 的JavaDoc),因为这实际上会丢弃 EntityManager 中所有尚未刷新的待处理更改。
如果您希望 EntityManager 被自动清除,可以将 @Modifying 注解的 clearAutomatically 属性设置为 true。
@Modifying 注解仅在与 @Query 注解结合使用时才有意义。
派生的查询方法或自定义方法不需要此注解。
派生删除查询
Spring Data JPA 还支持派生的删除查询,让你无需显式声明 JPQL 查询,如下例所示:
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) 的一种快捷方式,并且其行为与 delete(…) 中其他 CrudRepository 方法的实现保持一致。
| 在删除大量对象时,你需要考虑其对性能的影响,以确保有足够的内存可用。 所有待删除的对象在被删除前都会加载到内存中,并在会话中保留,直到执行刷新(flushing)或事务完成。 |
应用查询提示
要将 JPA 查询提示(query hints)应用于在仓库接口中声明的查询,可以使用 @QueryHints 注解。它接收一个 JPA @QueryHint 注解数组,以及一个布尔标志,用于在分页时选择性地禁用应用于额外计数查询(count query)的提示,如下例所示:
public interface UserRepository extends Repository<User, Long> {
@QueryHints(value = { @QueryHint(name = "name", value = "value")},
forCounting = false)
Page<User> findByLastname(String lastname, Pageable pageable);
}
上述声明将为实际查询应用已配置的 @QueryHint,但不会将其应用于为计算总页数而触发的计数查询。
为查询添加注释
有时,你需要根据数据库性能来调试一个查询。
数据库管理员向你展示的查询语句,可能与你使用 @Query 编写的代码看起来大相径庭,
也可能完全不同于你所认为的 Spring Data JPA 为自定义查找方法或示例查询(Query by Example)所生成的语句。
为了简化这一过程,您可以通过应用 @Meta 注解,在几乎任何 JPA 操作(无论是查询还是其他操作)中插入自定义注释。
@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() 操作则不支持。 |
JPA 中既没有 JPQL 日志记录的标准,也没有 SQL 日志记录的标准,因此每个提供者都需要进行自定义配置,如下文各节所示。
激活 Hibernate 注释
要激活 Hibernate 中的查询注释,您必须将 hibernate.use_sql_comments 设置为 true。
如果你使用的是基于 Java 的配置设置,可以按如下方式完成:
@Bean
public Properties jpaProperties() {
Properties properties = new Properties();
properties.setProperty("hibernate.use_sql_comments", "true");
return properties;
}
如果你有一个 persistence.xml 文件,可以在其中应用它:
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 文件中进行配置:
spring.jpa.properties.hibernate.use_sql_comments=true
激活 EclipseLink 注释
要在 EclipseLink 中启用查询注释,您必须将 eclipselink.logging.level.sql 设置为 FINE。
如果你使用的是基于 Java 的配置设置,可以按如下方式完成:
@Bean
public Properties jpaProperties() {
Properties properties = new Properties();
properties.setProperty("eclipselink.logging.level.sql", "FINE");
return properties;
}
如果你有一个 persistence.xml 文件,可以在其中应用它:
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 文件中进行配置:
spring.jpa.properties.eclipselink.logging.level.sql=FINE
配置 Fetch- 和 LoadGraphs
JPA 2.1 规范引入了对指定 FetchGraph 和 LoadGraph 的支持,我们也通过 @EntityGraph 注解来支持这一特性,该注解允许你引用一个 @NamedEntityGraph 的定义。你可以在实体上使用该注解,以配置生成查询的抓取计划(fetch plan)。抓取的类型(Fetch 或 Load)可以通过 type 注解上的 @EntityGraph 属性进行配置。更多详情请参见 JPA 2.1 规范第 3.7.4 节。
以下示例展示了如何在实体上定义一个命名实体图:
@Entity
@NamedEntityGraph(name = "GroupInfo.detail",
attributeNodes = @NamedAttributeNode("members"))
public class GroupInfo {
// default fetch mode is lazy.
@ManyToMany
List<GroupMember> members = new ArrayList<GroupMember>();
…
}
以下示例展示了如何在仓库查询方法中引用一个命名的实体图:
public interface GroupRepository extends CrudRepository<GroupInfo, String> {
@EntityGraph(value = "GroupInfo.detail", type = EntityGraphType.LOAD)
GroupInfo getByGroupName(String name);
}
也可以通过使用 @EntityGraph 来定义临时的实体图。所提供的 attributePaths 会被直接转换为相应的 EntityGraph,而无需显式地在您的领域类型上添加 @NamedEntityGraph,如下例所示:
public interface GroupRepository extends CrudRepository<GroupInfo, String> {
@EntityGraph(attributePaths = { "members" })
GroupInfo getByGroupName(String name);
}
滚动
滚动是一种更细粒度的方法,用于迭代处理较大的结果集分块。
滚动包含稳定排序、滚动类型(基于偏移量或基于键集的滚动)以及结果限制。
您可以使用属性名定义简单的排序表达式,并通过查询派生使用 Top 或 First 关键字 定义静态结果限制。
您可以连接多个表达式以将多个条件合并为一个表达式。
滚动查询返回一个 Window<T>,该对象允许获取元素的滚动位置,以便继续获取下一个 Window<T>,直到您的应用程序消费完整个查询结果。
与通过获取下一批结果来消费 Java Iterator<List<…>> 类似,查询结果滚动允许您通过 ScrollPosition 访问一个 Window.positionAt(…),如下例所示:
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());
|
|
|
上述示例展示了静态排序和限制。
您可以另外定义接受 |
滚动遍历消费 Window 实例时,需要编写大量条件判断才能实现最优的数据库往返次数,而使用 WindowIterator 可以简化这一重复性任务。
WindowIterator 提供了一种实用工具,通过省去检查是否存在下一个 Window 以及应用 Window 的需要,从而简化了在多个 ScrollPosition 之间的滚动操作。
WindowIterator<User> users = WindowIterator.of(position -> repository.findFirst10ByLastnameOrderByFirstname("Doe", position))
.startingAt(ScrollPosition.offset());
while (users.hasNext()) {
User u = users.next();
// consume the user
}
使用偏移量滚动
偏移量滚动(Offset scrolling)类似于分页,使用一个偏移量计数器来跳过一定数量的结果,让数据源仅返回从指定偏移量开始的结果。 这种简单机制避免了将大量结果发送到客户端应用程序。 然而,大多数数据库在服务器能够返回结果之前,需要先物化整个查询结果。
OffsetScrollPositioninterface 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 处的元素。 |
|
|
使用键集过滤进行滚动
基于偏移量(offset-based)的分页方式要求大多数数据库在服务器返回结果之前,必须先物化(materializing)整个查询结果。 因此,尽管客户端只看到所请求结果的一部分,但服务器仍需构建完整的查询结果,从而造成额外的负载。
键集过滤(Keyset-Filtering)方法通过利用数据库的内置功能来获取结果子集,旨在降低单个查询的计算和 I/O 开销。 该方法通过将一组键传递到查询中以维持滚动状态,从而有效地修改您的过滤条件。
Keyset 过滤(Keyset-Filtering)的核心思想是使用一个稳定的排序顺序来开始获取结果。
当你想要滚动到下一个数据块时,会获得一个 ScrollPosition,用于在已排序的结果中重建当前位置。
该 ScrollPosition 会捕获当前 Window 中最后一个实体的键集(keyset)。
为了执行查询,重建过程会重写查询条件子句,使其包含所有排序字段和主键,以便数据库能够利用潜在的索引来高效执行查询。
数据库只需从给定的键集位置构建一个规模小得多的结果集,而无需完整生成庞大的结果集后再跳过大量记录以到达特定偏移位置。
|
键集过滤(Keyset-Filtering)要求用于排序的键集属性必须为非空(non-nullable)。
此限制源于数据存储对比较运算符中 |
KeysetScrollPositioninterface 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 | 从最开始处启动,且不应用额外的过滤。 |
键集过滤(Keyset-Filtering)在数据库包含与排序字段匹配的索引时效果最佳,因此静态排序非常适用。 使用键集过滤的滚动查询要求查询返回排序顺序中使用的属性,并且这些属性必须映射到返回的实体中。
您可以使用接口和 DTO 投影,但请确保包含所有用于排序的属性,以避免键集(keyset)提取失败。
在指定您的 Sort 排序顺序时,只需包含与查询相关的排序属性即可;
如果您不希望确保查询结果的唯一性,则无需特别处理。
键集(keyset)查询机制会通过在排序顺序中加入主键(或复合主键中剩余的部分)来确保每个查询结果都是唯一的。