使用命名查询来声明对实体的查询是一种有效的方法,并且适用于少量查询。由于查询本身绑定到运行它们的 Java 方法,您实际上可以使用 Spring Data JPA @Query 注释直接绑定它们,而不是将它们注解到领域类。这将领域类从持久性特定信息中解放出来,并将查询共同定位到Repository接口。
加到查询方法查询注解优先于使用 @NamedQuery 定义的查询或在 orm.xml 中声明的命名查询。
以下示例显示了使用 @Query 注释创建的查询:
示例 59.使用 @Query 在查询方法中声明查询
public interface UserRepository extends JpaRepository<User, Long> { @Query("select u from User u where u.emailAddress = ?1") User findByEmailAddress(String emailAddress); }
应用 QueryRewriter
有时,无论您尝试应用多少功能,似乎都不可能让 Spring Data JPA 在将查询发送到 EntityManager 之前将您想要的所有内容应用到查询中。
您可以在将查询发送到 EntityManager 并“重写”它之前立即处理查询。也就是说,您可以在最后一刻进行任何更改。
例子 60. 使用 @Query 声明一个 QueryRewriter
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); }
此示例显示了native(纯 SQL)重写器和 JPQL 查询,两者都利用相同的 QueryRewriter。在这个场景中,Spring Data JPA 会寻找在相应类型的应用程序上下文中注册的 bean。
您可以像这样编写查询重写器:
示例 61.示例 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 方法的一部分。
另一种选择是让Repository本身实现接口。
示例 62. 提供 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 实现实例。
使用高级 LIKE 表达式
使用@Query 创建的手动定义查询的查询运行机制允许在查询定义中定义高级 LIKE 表达式,如以下示例所示:
例子 63.@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 模式进行扩充。
Native Query
@Query 注释允许通过将 nativeQuery 标志设置为 true 来运行native查询,如以下示例所示:
示例 64.使用 @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 当前不支持对Native Query进行动态排序,因为它必须操作声明的实际查询,而对于Native SQL,它不能可靠地做到这一点。但是,您可以通过自己指定计数查询来使用本机查询进行分页,如下例所示:
示例 65.使用 @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 后缀添加到查询的副本,类似的方法也适用于命名的本机查询。不过,您可能需要为计数查询注册一个结果集映射。
使用排序
可以通过提供 PageRequest 或直接使用 Sort 来完成排序。在 Sort 的 Order 实例中实际使用的属性需要匹配您的域模型,这意味着它们需要解析为查询中使用的属性或别名。JPQL 将其定义为状态字段路径表达式。
使用任何不可引用的路径表达式都会导致异常。
但是,将 Sort 与 @Query 一起使用可以让您潜入非路径检查的 Order 实例,其中包含 ORDER BY 子句中的函数。这是可能的,因为订单附加到给定的查询字符串。默认情况下,Spring Data JPA 拒绝任何包含函数调用的 Order 实例,但您可以使用 JpaSort.unsafe 添加潜在的不安全排序。
以下示例使用 Sort 和 JpaSort,包括 JpaSort 上的不安全选项:
例子 66. 使用 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)
- 指向域模型中属性的有效排序表达式。
- 包含函数调用的排序无效。抛出异常。
- 包含明确不安全顺序的有效排序。
- 指向别名函数的有效排序表达式。
使用命名参数
默认情况下,Spring Data JPA 使用基于位置的参数绑定,如前面所有示例中所述。这使得查询方法在重构参数位置时有点容易出错。为了解决这个问题,可以使用@Param 注解给方法参数一个具体的名字,并在查询中绑定这个名字,如下例所示:
例子 67. 使用命名参数
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 注释。
使用 SpEL 表达式
从 Spring Data JPA 1.4 版开始,我们支持在使用 @Query 定义的手动定义的查询中使用受限的 SpEL 模板表达式。在运行查询时,将根据一组预定义的变量评估这些表达式。Spring Data JPA 支持一个名为 entityName 的变量。它的用法是 select x from #{#entityName} x。它插入与给定存储库关联的域类型的实体名称。entityName 解析如下:如果域类型在@Entity 注释上设置了名称属性,则使用它。否则,使用域类型的简单类名。
以下示例演示了查询字符串中 #{#entityName} 表达式的一个用例,您希望在其中使用查询方法和手动定义的查询来定义存储库接口:
示例 68. 在存储库查询方法中使用 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} 变量。
可以使用@Entity 注释自定义实体名称。SpEL 表达式不支持 orm.xml 中的自定义。
当然,您可以直接在查询声明中使用 User,但这也需要您更改查询。对#entit 的引用yName 将 User 类将来可能重新映射到不同的实体名称(例如,通过使用 @Entity(name = "MyUser"))。
查询字符串中 #{#entityName} 表达式的另一个用例是,如果您想为具体域类型定义一个具有专用存储库接口的通用存储库接口。为了不在具体接口上重复自定义查询方法的定义,可以在通用仓库接口的@Query注解的查询字符串中使用实体名表达式,如下例所示:
示例 69. 在存储库查询方法中使用 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 上调用 findByAllAttribute(…),则查询变为 select t from ConcreteType t where t.attribute = ?1。
用于操作参数的 SpEL 表达式也可用于操作方法参数。在这些 SpEL 表达式中,实体名称不可用,但参数可用。它们可以通过名称或索引访问,如以下示例所示。
示例 70. 在存储库查询方法中使用 SpEL 表达式 - 访问参数。
@Query("select u from User u where u.firstname = ?1 and u.firstname=?#{[0]} and u.emailAddress = ?#") List<User> findByFirstnameAndCurrentUserWithCustomQuery(String firstname);
对于类似条件,人们通常希望将 % 附加到字符串值参数的开头或结尾。这可以通过使用 % 附加或前缀绑定参数标记或 SpEL 表达式来完成。下面的例子再次证明了这一点。
示例 71. 在存储库查询方法中使用 SpEL 表达式 - 通配符快捷方式。
@Query("select u from User u where u.lastname like %:#{[0]}% and u.lastname like %:lastname%") List<User> findByLastnameWithSpelExpression(@Param("lastname") String lastname);
当使用来自不安全来源的值的类似条件时,应该对值进行清理,以便它们不能包含任何通配符,从而允许攻击者选择比他们应该能够选择的更多的数据。为此,在 SpEL 上下文中提供了 escape(String) 方法。它在第一个参数中的所有 _ 和 % 实例前面加上第二个参数中的单个字符。结合 JPQL 和标准 SQL 中可用的 like 表达式的转义子句,这允许轻松清理绑定参数。
示例 72. 在存储库查询方法中使用 SpEL 表达式 - 清理输入值。
@Query("select u from User u where u.firstname like %?#% escape ?#") List<User> findContainingEscaped(String namePart);
如果在存储库接口中声明了此方法,findContainingEscaped("Peter_") 将找到 Peter_Parker 而不是 Peter Parker。使用的转义字符可以通过设置@EnableJpaRepositories注解的escapeCharacter来配置。请注意,SpEL 上下文中可用的方法 escape(String) 只会转义 SQL 和 JPQL 标准通配符 _ 和 %。如果底层数据库或 JPA 实现支持额外的通配符,这些通配符将不会被转义。
文章评论