分类
Spring Data

Spring Data JPA @Query

Spring Data JPA @Query

1. 概述

Spring Data提供了多种数据查询方法,本文我们将讨论使用Spring Data JPA中的@Query注解的方式。该注解同时支持JPQL以及SQL语句。不过在处理一些动态查询时,@Query注解有时候便力不从心了,为弥补该缺憾,在文章的后半部分将给出如何使用JPA Criteria API进行动态查询。

JPQL(Java Persistence Query Language) 是Java持久层查询语言。 JPQL和SQL的语法接近,但不同于SQL直接查询数据表,JPQL是对JPA实体来进行查询。最终JPQL会根据不同的底层数据库转换为对应的SQL语句。因此,使用JPQL可以减小因数据库版本差异造成的影响。

2. select语句

可以在Spring Data中的仓库层定义查询方法并使用@Query注解标识以实现特定的数据查询,@Query注解中的value属性可以接收JPQL或SQL语句,当被注解的方法被调用时,相应的JPQL或SQL语句则会对应并执行,从而达到自定义数据查询的目的。

在优先级方面,@Query注解的优先级大于@NameQuery注解或是在orm.xml文件定义的查询映射。

在Spring Data JPA中,往往会将对数据的所有操作都习惯性的集中到某个仓库接口(类)中,@Query注解可以应用到数据仓库的方法中,所以在选型时只要@Query注解能实现的,我们应该优先使用。

2.1 JPQL查询

默认情况下@Query注解使用JPQL做为查询语言。让我们看看如何使用JPQL来查询出状态为1的所有学生:

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

2.2 Native原生查询

除使用JPQL语句外,还可以定义原生的SQL语句。使用原生语句时,仅需要将nativeQuery属性设置为true:

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

3. 在查询中定义排序方式

我们在可以在@Query注解下的方法中传入类型为Sort的额外参数以实现排序的目的。

3.1 使用JPA提供的方法进行排序

JPA内置了像findAll(Sort)这样的开箱即用的方法,我们可以直接使用它在参数中传递Sort进行实现排序功能:

userRepository.findAll(Sort.by(Sort.Direction.ASC, "name")

但需要注意的是JPA中接收的Sort.by方法不能够使用诸如LENGTH之类的函数,如果我们传入如下的Sort,则会得到一个异常:

userRepository.findAll(Sort.by("LENGTH(name)"))

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

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

未找到类型为User的属性lENGTH(name)

如你如见,将字符串LENGTH(name)做为参数传给Sort.by()方法时,LENGTH(name)并没有被当成一个特殊的方法来对待,而被当前了普通的属性。由于User类中并无lENGTH(name)属性,所以发生了上述异常。

3.2 JPQL

当我们使用JPQL作为查询语言时,JPA同样可以很轻松的处理排序问题。方法与上一小节相同,同样是直接传入一个Sort类型即可:

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

比如我们想按User的name属性对结果进行排序:

userRepository.findAllUsersSortUsingJPQL(Sort.by("name"))

与JPA不的是,@Query注解支持Sort中的诸如LENGTH之类的函数名,比如要实现按用户名称的长度来获取用户的排序列表

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

创建Sort时,使用JpaSort.unsafe()至关重要。如果直接使用Sort.by,则将收到调用原生findAll(Sort.by("LENGTH(name)"))一模一样的异常 :

Sort.by("LENGTH(name)")

在使用@Query注解时,Spring Data发现unsafe不安全的排序方法时将跳过检查传入的排序字符串是否属于当前实体的属性,而是仅仅将sort子句添加到查询中。从而达到了LENGTH()方法生效的目的。

3.3 Native

@Query注解使用原生查询(即native=true)时,无法使用在参考中传入Sort的方式来达到排序的目的:

如果这样使用Sort:

    // @Query(value = "SELECT * FROM USERS u WHERE u.status = 1", nativeQuery = true)
    // List<User> findAllUsersSortUsingNative(Sort sort);

则将触发InvalidJpaQueryMethodException异常。

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

无法将Native原生查询与动态排序和/或分页一起使用

控制台中的异常消息提示我们:JPA无法在原生的查询中动态地处理排序或是分页。

4.分页

分页功能允许我们仅在Page中返回所有数据的一部分。例如,在浏览网页上的几页数据时,当用户浏览到第一页时,服务器只需要返回第一页的数据即可。

分页的另一个优点是:相较于返回所有数据,返回某一页的数据减小了服务器发送到客户端的数据量。在同等情况下,较少的数据量意味着较高的性能。

4.1 JPQL

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

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

Pageable是一个接口,而PageRequest是它的一个实现类。我们可以传递PageRequest参数来获取数据页面。原生查询也支持分页,但是需要一些额外的调整。

4.2 原生Native查询

我们可以通过声明附加属性countQuery启用原生查询的分页功能,该属性的主要作用是获取符合当前查询条件的总数,也更生成Page分页的具体信息(总共多少页,共多少条,是否为首页等):

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

4.3 2.0.4之前的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> findAllUsersWithPaginationUsingNativeBeforeJPA2_0_4(Pageable pageable);

在上面的示例中,我们添加“ \ n– #pageable \ n”作为分页参数的占位符。这告诉Spring Data JPA如何解析查询并注入pageable参数。

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

5.索引参数查询

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

索引参数命名参数本质是一样的。它们的不同点在于是否需要定义查询参数的名称以及是否需要注意参数的顺序:

  • 索引参数的模式下: 不需要定义查询条件的名称,但要保证查询条件的顺序和参数的顺序相同
  • 在命名参数的模式下:需要手动设置参数的名称,但不用考虑参数出现的顺序。

5.1 JPQL

对于JPQL中的索引参数,Spring Data会将方法参数按照在方法声明中出现的顺序传递给查询:

@Query("SELECT u FROM User u WHERE u.status = ?1")
User findUserByStatusUsingJPQL(Integer status);
 
@Query("SELECT u FROM User u WHERE u.status = ?1 and u.name = ?2")
User findUserByStatusAndNameUsingJPQL(Integer status, String name);

对于上述查询,status参数将被分配给查询参数?1,name参数将被分配给查询参数?2。方法中参数出现的顺序,和SQL中查询条件的顺序相对应。

5.2 Native

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

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

6.命名参数查询

我们还可以使用命名参数将方法来传递查询条件。使用@Param注解定义这些参数。

@Param注解中的参数字符串,必须与相应的JPQL或SQL查询参数名称匹配。与索引参数相比,使用命名参数易于阅读,并且在需要重构查询的情况下更不易出错

6.1 JPQL

如上所述,我们在方法声明中使用@Param注解,将JPQL中的参数与方法中的参数进行匹配:

@Query("SELECT u FROM User u WHERE u.status = :status and u.name = :name")
User findUserByStatusAndNameNamedParamsUsingJPQL(
  @Param("status") Integer status, 
  @Param("name") String name);

请注意,在上面的示例中,只要 Param中的字符串 和 JPQL中的查询条件 对应即可,不需要 传入的变量 和 查询条件 对应。所以以下在@Query中调换了name与status的写法也是完全正确的:

@Query("SELECT u FROM User u WHERE u.name = :name and u.status = :status")
User findUserByStatusAndNameNamedParamsUsingJPQL(
  @Param("status") Integer status, 
  @Param("name") String name);

6.2 Native

对于原生查询定义,与JPQL相比没有什么不同。同样是使用@Param注解,只不过语句换成了SQL:

@Query(value = "SELECT * FROM Users u WHERE u.status = :status and u.name = :name",
nativeQuery = true)
User findUserByStatusAndNameNamedParamsUsingNative(
@Param("status") Integer status, @Param("name") String name);

7.集合参数查询

让我们看下如何处理JPQL或SQL查询中若包含  IN(或NOT IN)关键字的情况:

SELECT u FROM User u WHERE u.name IN :names

上面JPQL的含义是:传入一个姓名集合,只要一个用户的姓名被包含在这个集合中,这条记录就会被查出来。在这种情况下,我们可以定义一个以Collection 为参数的查询方法  :

@Query(value = "SELECT u FROM User u WHERE u.name IN :names")
List<User> findUserByNameListUsingJPQL(@Param("names") Collection<String> names);

由于参数是Collection是集合接口,而所有的列表都继承自这个接口,因此可以与List,HashSet等一起使用。接下来,我们将展示如何使用@Modifying注解修改数据。

8.使用@Modifying执行更新操作

我们还可以通过将@Modifying注解添加到仓库层的方法上,并结合@Query注解中的语句,来完成对数据库进行更新操作。

8.1 JPQL

select查询相比,update方法有两个区别:一是它具有@Modifying注解,二是查询使用update关键字,而不是select

@Modifying
@Query("update User u set u.status = :status where u.name = :name")
int updateUserSetStatusForNameUsingJPQL(@Param("status") Integer status, 
  @Param("name") String name);

索引参数和命名参数都可以在更新语句中使用。此外如果声明了返回值,则返回值中会给出“执行相关的方法后具体更新了多少行记录”。

8.2 Native

我们还可以在原生查询上使用@Modifying注解以达到修改数据的目的:

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

8.3 Inserts 执行插入操作

由于INSERT并不是JPA接口的一部分,所以执行插入操作我们必须同时应用@Modifying并使用原生查询

@Modifying
@Query(value = "insert into User (name, status, email) values (:name, :status, :email)", nativeQuery = true)
void insertUserUsingNative(@Param("name") String name, @Param("status") Integer status, @Param("email") String email);

9.动态查询

通常我们会遇到这种情况:软件运行前无法确定具体的SQL语句,而只有运行时才知道某些参数的值,近而再根据这些值来构建不同的SQL语句。在这种情况下静态查询便不能胜任了,此时便需要借助于Criteria来实现动态查询。

9.1 动态查询的例子

假设我们需要查询电子邮件包含在一组列表中的用户,而此电子邮件列表是动态生成(不确定)的:

SELECT u FROM User u WHERE u.email LIKE '%email1%' 
    or  u.email LIKE '%email2%'
    ... 
    or  u.email LIKE '%emailn%'

由于电子邮件列表是动态构造的,因此在编译时我们无法知道要添加多少个LIKE子句。

那么这种情况下,我们使用@Query注解便无法完成功能需求了,因为我们无法提供静态SQL语句

此时我们可以通过实现定制一个动态的查询方法,并在该方法中根据当前的实际逻辑来动态的生成查询语句。最后,为了让其能够与Spring Data JPA完结合,再按一定的规范来组织接口、类与方法,从而达到自定义动态查询方法的目的。

9.2 自定义仓库与JPA Criteria API

Spring Data 创建自定义可组合仓库一文详细的表达了如果自定义仓库来优雅地完成动态查询。基本步骤如下:

创建自定义的接口,并在接口中定义查询方法、参数:

public interface UserRepositoryCustom {
    List<User> findUserByEmailsUsingCriteria(Set<String> emails);
}

接下来,新建一个上述接口的实现类,该类的名称必须为 接口名+Impl:

public class UserRepositoryCustomImpl implements UserRepositoryCustom {
    @PersistenceContext
    private EntityManager entityManager;

    @Override
    public List<User> findUserByEmailsUsingCriteria(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在findUserByEmailsUsingCriteria中构建了动态查询。

另外,我们需要确保在类名为:接口名+Impl因为只有这样Spring才会认为UserRepositoryCustomImpl是UserRepositoryCustom在仓库层面的实现类。Spring依靠此接口名+Impl机制来查找仓库的实现类。如果名称不对应,Spring会把findUserByEmailsUsingCriteria接口当成仓库的内部方法,并尝试对其进行解析,在这种情况下Sring会根据方法来尝试User实体emailsUsingCriteria字段,由于User实体并不存在该字段,所以最终会触发“EmailsUsingCriteria属性不存在”的异常。

9.3 扩展现有仓库层

我们通过继承JpaRepository仓库的同时继承自定义UserRepositoryCustom来达到扩展UserRepository的目的。

public interface UserRepository extends JpaRepository<User, Long>, UserRepositoryCustom {
    // 第2-7节中的代码
}

此时,当调用Spring自动生成的UserRepository对象的findUserByEmailsUsingCriteria方法时,将是终调用UserRepositoryCustomImpl.findUserByEmailsUsingCriteria()方法。

9.4 使用仓库层

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

Set<String> emails = new HashSet<>();
// 在集合中添加一些邮箱...
emails.add("test@test.com");
List<User> users = userRepository.findUserByEmailsUsingCriteria(emails);

10.总结

在本文中,我们介绍了使用@Query注解在Spring Data JPA仓库方法中定义查询的几种方法。包括使用JPQL、使用原生SQL、以及如何自定义仓库层,并使用Criteria实现动态查询。