Spring Data提供了许多方法来定义我们可以执行的查询。其中之一是@Query注释。
在本教程中,我们将演示如何使用春季数据JPA中的@Query注释来执行JPQL和本机SQL查询。
我们还将展示在@Query注释不够用时如何构建动态查询。
探索春季数据 JPA 中的查询派生机制。
阅读更多→
通过组合@Query和@Modifying注释,在春季数据 JPA 中创建 DML 和 DDL 查询
阅读更多→
为了定义要为Spring数据存储库方法执行的SQL,我们可以使用@Query注释来注释该方法 - 其值属性包含要执行的JPQL或SQL。
@Query批注优先于命名查询,命名查询使用@NamedQuery进行批注或在 orm.xml 文件中定义。
将查询定义放在存储库内的方法正上方,而不是作为命名查询放在域模型内部,这是一种很好的方法。存储库负责持久性,因此它是存储这些定义的更好地方。
默认情况下,查询定义使用 JPQL。
让我们看一个简单的存储库方法,该方法从数据库中返回活动的 User 实体:
- @Query("SELECT u FROM User u WHERE u.status = 1")
- Collection
findAllActiveUsers();
我们也可以使用本机 SQL 来定义我们的查询。我们所要做的就是将本机Query属性的值设置为true,并在注释的值属性中定义本机SQL查询:
- @Query(
- value = "SELECT * FROM USERS u WHERE u.status = 1",
- nativeQuery = true)
- Collection<User> findAllActiveUsersNative();
我们可以将 Sort 类型的附加参数传递给具有@Query批注的 Spring Data 方法声明。它将被转换为传递给数据库的 ORDER BY 子句。
对于我们开箱即用的方法,例如 findAll(Sort) 或通过解析方法签名生成的方法,我们只能使用对象属性来定义我们的排序:
userRepository.findAll(Sort.by(Sort.Direction.ASC, "name"));
现在想象一下,我们要按 name 属性的长度进行排序:
userRepository.findAll(Sort.by("LENGTH(name)"));
当我们执行上述代码时,我们将收到一个异常:
org.springframework.data.mapping.PropertyReferenceException: No property LENGTH(name) found for type User!
当我们使用JPQL进行查询定义时,Spring Data可以毫无问题地处理排序 - 我们所要做的就是添加一个Sort类型的方法参数:
- @Query(value = "SELECT u FROM User u")
- List<User> findAllUsers(Sort sort);
我们可以调用此方法并传递 Sort 参数,该参数将按 User 对象的 name 属性对结果进行排序:
userRepository.findAllUsers(Sort.by("name"));
由于我们使用了@Query注释,因此我们可以使用相同的方法来按名称的长度获取排序的用户列表:
userRepository.findAllUsers(JpaSort.unsafe("LENGTH(name)"));
使用 JpaSort.unsafe() 创建排序对象实例至关重要。
当我们使用:
Sort.by("LENGTH(name)");
然后,我们将收到与上面看到的 findAll() 方法完全相同的异常。
当Spring Data发现使用@Query注释的方法的不安全排序顺序时,它只会将排序子句附加到查询中 - 它会跳过检查排序依据的属性是否属于域模型。
当@Query批注使用本机 SQL 时,则无法定义排序。
如果我们这样做,我们将收到一个例外:
org.springframework.data.jpa.repository.query.InvalidJpaQueryMethodException: Cannot use native queries with dynamic sorting and/or pagination
正如异常情况所说,本机查询不支持排序。错误消息提示分页也会导致异常。
但是,有一种启用分页的解决方法,我们将在下一节中介绍它。
分页允许我们在主页中仅返回整个结果的子集。例如,在网页上的多个数据页面中导航时,这很有用。
分页的另一个优点是,从服务器发送到客户端的数据量是最小的。通过发送较小的数据片段,我们通常可以看到性能的提高。
在 JPQL 查询定义中使用分页非常简单:
- @Query(value = "SELECT u FROM User u ORDER BY id")
- Page<User> findAllUsersWithPagination(Pageable pageable);
我们可以传递一个页面请求参数来获取一页数据。
本机查询也支持分页,但需要一些额外的工作。
我们可以通过声明其他属性 countQuery 来为本机查询启用分页。
这定义了要执行的 SQL,以计算整个结果中的行数:
- @Query(
- value = "SELECT * FROM Users ORDER BY id",
- countQuery = "SELECT count(*) FROM Users",
- nativeQuery = true)
- Page<User> findAllUsersWithPagination(Pageable pageable);
上述本机查询解决方案适用于Spring数据JPA版本2.0.4及更高版本。
在该版本之前,当我们尝试执行此类查询时,我们将收到我们在上一节有关排序中描述的相同异常。
我们可以通过在查询中添加一个额外的分页参数来克服这个问题:
- @Query(
- value = "SELECT * FROM Users ORDER BY id \n-- #pageable\n",
- countQuery = "SELECT count(*) FROM Users",
- nativeQuery = true)
- Page<User> findAllUsersWithPagination(Pageable pageable);
在上面的例子中,我们添加
\n-- #pageable\n
作为分页参数的占位符。这告诉春季数据JPA如何解析查询并注入可分页参数。此解决方案适用于 H2 数据库。
我们已经介绍了如何通过 JPQL 和本机 SQL 创建简单的选择查询。接下来,我们将演示如何定义其他参数。
有两种可能的方法可以将方法参数传递给查询:索引参数和命名参数。
在本节中,我们将介绍索引参数。
对于 JPQL 中的索引参数,Spring Data 将按照方法声明中出现的顺序将方法参数传递给查询:
- @Query("SELECT u FROM User u WHERE u.status = ?1")
- User findUserByStatus(Integer status);
-
- @Query("SELECT u FROM User u WHERE u.status = ?1 and u.name = ?2")
- User findUserByStatusAndName(Integer status, String name);
对于上述查询,状态方法参数将分配给索引为 1 的查询参数,并将 name 方法参数分配给索引为 2 的查询参数。
本机查询的索引参数的工作方式与 JPQL 完全相同:
- @Query(
- value = "SELECT * FROM Users u WHERE u.status = ?1",
- nativeQuery = true)
- User findUserByStatusNative(Integer status);
在下一节中,我们将展示一种不同的方法:通过 name 传递参数。
我们还可以使用命名参数将方法参数传递给查询。我们使用存储库方法声明中的@Param注释来定义这些内容。
用 @Param 注释的每个参数都必须具有与相应的 JPQL 或 SQL 查询参数名称匹配的值字符串。具有命名参数的查询更易于阅读,并且在需要重构查询时不易出错。
如上所述,我们在方法声明中使用@Param注释,以将JPQL中按名称定义的参数与方法声明中的参数进行匹配:
- @Query("SELECT u FROM User u WHERE u.status = :status and u.name = :name")
- User findUserByStatusAndNameNamedParams(
- @Param("status") Integer status,
- @Param("name") String name);
请注意,在上面的示例中,我们将 SQL 查询和方法参数定义为具有相同的名称,但只要值字符串相同,就不需要它:
- @Query("SELECT u FROM User u WHERE u.status = :status and u.name = :name")
- User findUserByUserStatusAndUserName(@Param("status") Integer userStatus,
- @Param("name") String userName);
对于本机查询定义,与 JPQL 相比,我们如何通过名称将参数传递给查询没有区别 — 我们使用@Param注释:
- @Query(value = "SELECT * FROM Users u WHERE u.status = :status and u.name = :name",
- nativeQuery = true)
- User findUserByStatusAndNameNamedParamsNative(
- @Param("status") Integer status, @Param("name") String name);
让我们考虑一下 JPQL 或 SQL 查询的 where 子句包含输入(或非 IN)关键字的情况:
SELECT u FROM User u WHERE u.name IN :names
在这种情况下,我们可以定义一个将 Collection 作为参数的查询方法:
- @Query(value = "SELECT u FROM User u WHERE u.name IN :names")
- List
findUserByNameList(@Param("names") Collection names);
由于参数是集合,因此可以与列表、哈希集等一起使用。
接下来,我们将演示如何使用 @修改注释修改数据。
我们可以使用 @Query 注释来修改数据库的状态,方法是将 @Modifying 注释添加到存储库方法中。
与选择查询相比,修改数据的存储库方法有两个区别 — 它具有@Modifying注释,当然,JPQL 查询使用 update 而不是 select:
- @Modifying
- @Query("update User u set u.status = :status where u.name = :name")
- int updateUserSetStatusForName(@Param("status") Integer status,
- @Param("name") String name);
返回值定义执行查询时更新的行数。索引参数和命名参数都可以在更新查询中使用。
我们也可以使用本机查询来修改数据库的状态。我们只需要添加@Modifying注释:
- @Modifying
- @Query(value = "update Users u set u.status = ? where u.name = ?",
- nativeQuery = true)
- int updateUserSetStatusForNameNative(Integer status, String name);
要执行插入操作,我们必须应用@Modifying并使用本机查询,因为 INSERT 不是 JPA 接口的一部分:
- @Modifying
- @Query(
- value =
- "insert into Users (name, age, email, status) values (:name, :age, :email, :status)",
- nativeQuery = true)
- void insertUser(@Param("name") String name, @Param("age") Integer age,
- @Param("status") Integer status, @Param("email") String email);
通常,我们会遇到基于条件或数据集构建SQL语句的需要,这些条件或数据集的值仅在运行时才知道。在这些情况下,我们不能只使用静态查询。
例如,让我们想象一种情况,我们需要从运行时定义的集合中选择所有电子邮件类似于一个的用户 - email1,email2,...,emailn:
- SELECT u FROM User u WHERE u.email LIKE '%email1%'
- or u.email LIKE '%email2%'
- ...
- or u.email LIKE '%emailn%'
由于集合是动态构造的,因此我们无法在编译时知道要添加多少 LIKE 子句。
在这种情况下,我们不能只使用@Query注释,因为我们无法提供静态 SQL 语句。
相反,通过实现自定义复合存储库,我们可以扩展基本的 JpaRepository 功能,并提供我们自己的逻辑来构建动态查询。让我们来看看如何做到这一点。
对我们来说幸运的是,Spring提供了一种通过使用自定义片段接口来扩展基本存储库的方法。然后,我们可以将它们链接在一起以创建复合存储库。
我们将从创建自定义片段接口开始:
- public interface UserRepositoryCustom {
- List<User> findUserByEmails(Set<String> emails);
- }
然后我们将实现它:
- public class UserRepositoryCustomImpl implements UserRepositoryCustom {
-
- @PersistenceContext
- private EntityManager entityManager;
-
- @Override
- public List<User> findUserByEmails(Set<String> emails) {
- CriteriaBuilder cb = entityManager.getCriteriaBuilder();
- CriteriaQuery<User> query = cb.createQuery(User.class);
- Root<User> user = query.from(User.class);
-
- Path<String> emailPath = user.get("email");
-
- List<Predicate> predicates = new ArrayList<>();
- for (String email : emails) {
- predicates.add(cb.like(emailPath, email));
- }
- query.select(user)
- .where(cb.or(predicates.toArray(new Predicate[predicates.size()])));
-
- return entityManager.createQuery(query)
- .getResultList();
- }
- }
如上所示,我们利用 JPA 标准 API 来构建动态查询。
此外,我们需要确保在类名中包含 Impl 后缀。Spring 将搜索“用户存储库自定义”实现为“用户存储库自定义”。由于片段本身不是存储库,因此Spring依靠这种机制来查找片段实现。
请注意,从第 2 节到第 7 节的所有查询方法都在用户存储库中。
因此,现在我们将通过在用户存储库中扩展新界面来集成我们的片段:
- public interface UserRepository extends JpaRepository<User, Integer>, UserRepositoryCustom {
- // query methods from section 2 - section 7
- }
最后,我们可以调用我们的动态查询方法:
- Set<String> emails = new HashSet<>();
- // filling the set with any number of items
-
- userRepository.findUserByEmails(emails);
我们已成功创建了一个复合存储库,并调用了我们的自定义方法。
在本文中,我们介绍了使用@Query注释在Spring数据JPA存储库方法中定义查询的几种方法。
我们还学习了如何实现自定义存储库和创建动态查询。
与往常一样,本文中使用的完整代码示例可在 GitHub 上找到。