Spring Data JPA @Query


1.概述

Spring Data提供了许多方法来定义我们可以执行的查询。其中之一是 @Query注释。

在本教程中,我们将演示如何在Spring Data JPA中使用@Query注释来执行JPQL和本机SQL查询。

此外,我们将展示如何在@Query注释不够时构建动态查询 。

2.Select Query

为了定义要为Spring Data存储库方法执行的SQL,我们可以使用@Query注释来注释该方法 - 它的 value属性包含要执行的JPQL或SQL。

该@Query注释优先于命名查询,这些注释与@NamedQuery或在定义的orm.xml中的文件。

将查询定义放在存储库中的方法上方而不是在我们的域模型中作为命名查询是一种很好的方法。存储库负责持久性,因此它是存储这些定义的更好位置。

2.1。JPQL

默认情况下,查询定义使用JPQL。

让我们看一个简单的存储库方法,它从数据库中返回活动的User实体:

@Query("SELECT u FROM User u WHERE u.status = 1")
Collection<User> findAllActiveUsers();

2.2. Native

我们也可以使用本机SQL来定义我们的查询。我们所要做的就是将nativeQuery属性的值设置为true,并在注释的value属性中定义本机SQL查询:

@Query(
  value = "SELECT * FROM USERS u WHERE u.status = 1",
  nativeQuery = true)
Collection<User> findAllActiveUsersNative();

3.在查询中定义订单

我们可以将Sort 类型的附加参数传递给具有@Query注释的Spring Data方法声明 。它将被转换为 传递给数据库的ORDER BY子句。

3.1。对JPA提供和派生方法进行排序

对于我们开箱即用的方法,如 findAll(Sort)或通过解析方法签名生成的方法,我们只能使用对象属性来定义我们的排序:

userRepository.findAll(new Sort(Sort.Direction.ASC, "name"));

现在假设我们想要按名称属性的长度排序:

userRepository.findAll(new Sort("LENGTH(name)"));

当我们执行上面的代码时,我们会收到一个异常:

org.springframework.data.mapping.PropertyReferenceException: No property lENGTH(name) found for type User!

3.2. JPQL

当我们将JPQL用于查询定义时,Spring Data可以毫无问题地处理排序 - 我们所要做的就是添加Sort类型的方法参数:

@Query(value = "SELECT u FROM User u")
List<User> findAllUsers(Sort sort);

我们可以调用此方法并传递一个Sort参数,该参数将按User对象的name属性对结果进行排序:

userRepository.findAllUsers(new Sort("name"));

因为我们使用了@Query注释,所以我们可以使用相同的方法按用户名的长度获取用户的排序列表:

userRepository.findAllUsers(JpaSort.unsafe("LENGTH(name)"));

我们使用JpaSort.unsafe()创建Sort对象实例至关重要。

当我们使用时:

new Sort("LENGTH(name)");

那么我们将收到与上面针对findAll()方法看到的完全相同的异常 。

当Spring Data发现使用@Query注释的方法的不安全 排序顺序时 ,它只是将sort子句附加到查询 - 它会跳过检查要排序的属性是否属于域模型。

3.3。Native

当@Query注释使用本机SQL时,则无法定义排序。

如果我们这样做,我们将收到一个例外:

org.springframework.data.jpa.repository.query.InvalidJpaQueryMethodException: Cannot use native queries with dynamic sorting and/or pagination

如异常所述,本机查询不支持排序。错误消息给我们一个提示,即分页也会导致异常。

但是,有一种解决方法可以实现分页,我们将在下一节中介绍。

4.分页

分页允许我们只返回页面中整个结果的子集 。例如,当浏览网页上的多页数据时,这很有用。

分页的另一个优点是从服务器发送到客户端的数据量最小化。通过发送较小的数据,我们通常可以看到性能的提高。

4.1。JPQL

在JPQL查询定义中使用分页非常简单:

@Query(value = "SELECT u FROM User u ORDER BY id")
Page<User> findAllUsersWithPagination(Pageable pageable);

我们可以传递PageRequest 参数来获取数据页面。本机查询也支持分页,但需要一些额外的工作。

4.2。Native

我们可以通过声明一个额外的属性countQuery来启用本机查询的分页 - 这定义了要执行的SQL来计算整个结果中的行数:

@Query(
  value = "SELECT * FROM Users ORDER BY id",
  countQuery = "SELECT count(*) FROM Users",
  nativeQuery = true)
Page<User> findAllUsersWithPagination(Pageable pageable);

4.3。2.0.1之前的Spring Data JPA版本

上面的本机查询解决方案适用于Spring Data 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”作为分页参数的占位符。这告诉Spring Data JPA如何解析查询并注入可分页参数。此解决方案适用于H2数据库。

我们已经介绍了如何通过JPQL和本机SQL创建简单的选择查询。接下来,我们将展示如何定义其他参数。

5.索引查询参数

我们可以通过两种方法将方法参数传递给查询。在本节中,我们将介绍索引参数。

5.1。JPQL

对于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,和 名称方法参数将被分配给查询参数具有索引2。

5.2。Native

本机查询的索引参数的工作方式与JPQL完全相同:

@Query(
  value = "SELECT * FROM Users u WHERE u.status = ?1",
  nativeQuery = true)
User findUserByStatusNative(Integer status);

在下一节中,我们将展示一种不同的方法 - 通过名称传递参数。

6.命名参数

我们还可以使用命名参数将方法参数传递给查询。我们使用存储库方法声明中的@Param注释来定义它们。

使用@Param注释的每个参数都必须具有与相应的JPQL或SQL查询参数名称匹配的值字符串。具有命名参数的查询更易于阅读,并且在查询需要重构时不易出错。

6.1。JPQL

如上所述,我们在方法声明中使用 @Param注释来匹配JPQL中name定义的参数和方法声明中的参数:

@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);

6.2。Native

对于本机查询定义,与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);

7.收集参数

让我们考虑一下我们的JPQL或SQL查询的 where子句包含 IN(或NOT 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<User> findUserByNameList(@Param("names") Collection<String> names);

由于参数是Collection, 因此可以与List,HashSet等一起使用 。

接下来,我们将展示如何使用@ Modifying 注释修改数据。

8.使用@Modifying更新查询

我们可以 使用@Query注释来修改数据库的状态,还可以将@ Modifying 注释添加到存储库方法。

8.1。JPQL

修改数据的存储库方法与select查询相比有两个不同之处- 它具有 @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);

返回值定义查询执行更新的行数。索引和命名参数都可以在更新查询中使用。

8.2。 Native

我们也可以使用本机查询修改数据库的状态 - 我们只需要添加@Modifying注释:

@Modifying
@Query(value = "update Users u set u.status = ? where u.name = ?",
  nativeQuery = true)
int updateUserSetStatusForNameNative(Integer status, String name);

8.3。Inserts

要执行插入操作,我们必须同时应用 @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);

9.动态查询

通常,我们会遇到基于条件或数据集构建SQL语句的需要,这些条件或数据集的值仅在运行时已知。而且,在这些情况下,我们不能只使用静态查询。

9.1。动态查询的示例

例如,让我们想象一下这样一种情况,我们需要从运行时定义的集合中选择电子邮件是LIKE的所有用户- 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功能并提供我们自己的逻辑来构建动态查询。我们来看看如何做到这一点。

9.2。自定义存储库和JPA Criteria API

幸运的是,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 Criteria API构建动态查询。

此外,我们需要确保在类名中包含Impl后缀。Spring将UserRepositoryCustom实现作为UserRepositoryCustomImpl进行搜索。由于片段本身不是存储库,因此Spring依赖此机制来查找片段实现。

9.3。扩展现有存储库

请注意,第2节 - 第7节中的所有查询方法都在UserRepository中。 所以现在,我们将通过扩展UserRepository中的新接口来集成我们的片段:

public interface UserRepository extends JpaRepository<User, Integer>, UserRepositoryCustom {
    //  query methods from section 2 - section 7
}

9.4。使用存储库

最后,我们可以调用我们的动态查询方法:

Set<String> emails = new HashSet<>();
// filling the set with any number of items

userRepository.findUserByEmails(emails);

我们已成功创建了一个复合存储库并调用了我们的自定义方法。