分类
Spring Data

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实现动态查询。

分类
Spring Data

Spring Data JPA 简介

1. 概述

本文将围绕Spring项目中的Spring Data JPA展开介绍。如果你尚需要了解如何使用Spring来构造一个基本的应用程序,那么可以先阅读此篇文章。Spring JPA是一种新颖、优雅的数据访问方式,它允许我们在只定义接口及方法名称的前提下,快速的实现对数据库的访问。

2. Spring Data中的DAO

DAO模式:Data Access Object数据访问(存储)对象模式,简单来讲就是使用JAVA语言来快速、简单的操作数据库。

该模式一般由以下部分组成:

DAO接口:将对数据库的操作定义为抽象方法,比如定义save方来新增数据。

DAO实现类:负责DAO接口中抽象方法的具体实现。往往会根据数据库类型而给出不同的实现。

实体类:用于与数据库中的数据表进行映射。DAO可以将实体对象操作到数据表中,也可以将查询出的数据绑定到实体上。

基础类:为一些样板代码提供一些基础的支持,避免写过多的样板化代码。

https://www.runoob.com/note/27029

使用Spring简化DAO层的样板代码一文中,我们阐述了DAO层通常包含了过多重复的样板化的代码。对样板代码简单可以减少代码体量,降低维护与升级的成本,统一数据访问方式、配置信息等。

Spring Data对样板代码的简化做到了极致,在Spring Data中我们只需要定义相关接口即可,完全不需要对该接口进行实现。

若使定义的DAO接口生效,则需要使其继承JPA中指定的仓库接口 -- JpaRepository。Spring Data将会自动扫描继承了JpaRepository的接口并自动为其创建一个实现。

通过继承JpaRepository的方法,我们可以轻松的获取到一个包含了CRUD操作的标准DAO。

3. 自定义方法以及查询

虽然能过继承JpaRepository已经实现了基本的CRUD操作,但大多数的项目中,仅仅有CRUD操作是远远不够的。

为此,Spring JPA提供了多种自定义数据操作的方法:

  • 直接在接口中定义一个新的方法,并使用支持JPQL的@Query注解来进行标识
  • 使用高级用法:SpecificationQuerydsl
  • 通过JPA命名查询(Named Queries)来自定义查询方法

第二种方法与JPA的标准查询比较相似,不同的是使用这种方法将更加灵活、方便。这将使我们的代码具有更高的可读性,可复用性也会更强。特别是当我们处理一些复杂的查询逻辑时,这种方法的优势将更加突出。

3.1 仅需要定义方法名

当Spring Data扫描到继承了JpaRepository的接口并生成实现时,Spring Data将扫描接口定义的方法并尝试依据方法名将其自动转换为特定的查询语句。尽管说这种依据方法名来自动生成查询语句的方法有一定的局限性,但这种方法使用起来真的是太强大、太方便、太优雅了。

假设当前有学生(student)实体,我们此时想以学生姓名做为关键字进行查询,则仅仅需要在定义的DAO接口中加入以下方法:

/**
 * 操作学生表的DAO
 *
 * @author panjie
 */
public interface StudentDAO extends JpaRepository<Student, Long> {
    /**
     * 通过姓名查找某个学生
     *
     * @param name 学生姓名
     * @return
     */
    Student findByName(String name);
}

没错,仅仅需要定义一个方法,Spring Data将自动按此方法名称转换为:根据关键字来查询某个学生。这种查询方法支持很多种关键字,如果在不需要对逻辑进行处理时,不失为最佳的一种方法。更多的关键字请参考官方文档

当然了,在使用上述方法时,需要保证实体中的字段信息与方法中给出的信息一致,以避免发生异常。比如findByUsername方法的成功执行依赖于学生实体(student)中存在username字段,而此时学生实体并不存在userName字段,则解析器将抛出如下异常:

club.codedemo.thepersistencelayerwithspringdatajpa.StudentDAO.findByUsername(java.lang.String)! No property username found for type Student!

3.2 自定义查询语句

可以在方法上添加@Query注解以自定义查询语句:

    /**
     * 不区分大小写的根据name查询某个学生
     * 比如通过Zhangsan可以查询出学生名为zhangsan、zhangSan等的学生
     *
     * @param name 学生姓名
     * @return
     */
    @Query("SELECT s FROM Student s WHERE LOWER(s.name) = LOWER(:name)")
    Student retrieveByName(@Param("name") String name);

欲了解更多关于自定义查询语句的知识,请参考官方文档

4. 事务配置

Spring自动实现接口的特性决定了我们无法直接获取到整个实现过程,当然也无法得知Spring Data JPA是如何对事务进行配置的了。值得庆幸的事,我们可以通过观察Spring Data JPA中的org.springframework.data.jpa.repository.support.SimpleJpaRepository类了解其事务配置。

Spring使用了@Transaction(readOnly = true)对该类进行注解,表示该类中的方法默认采用的都是只读(read-only)模式。接着又在个别的非查询模式的方法上加入了@Transaction,从而覆盖了在类上标注的只读模式,近而达到了:如果该类中的某个方法上没有使用@Transaction注解,则标识在该类上的@Transaction(readOnly = true)起作用;如果某个方法使用了@Transaction注解,则忽略类上的@Transaction(readOnly = true)注解。

4.1 异常转换依然有效

现在可能你有一个疑问:既然Srping Data JPA并不依赖于已经在Spring5中移除的历史的模板引擎JpaTemplate与HibernateTemplate,那么我们是否仍要将JPA异常转换为Spring的DataAccessException呢?

答案是肯定的,我们仍然可以在DAO层使用@Repository注解来开启异常转换功能。@Repository注解将自动获取bean中的PersistenceExceptionTranslator并将其转换为我们熟知的DataAccessException

比如执行以下代码最终将获取DataIntegrityViolationException(该异常是DataAccessException的子类):

    /**
     * 由于Student实体中要求name字段不能为null
     * 所以保存一个name字段为null的默认学生实体时,将会发生DataIntegrityViolationException异常
     */
    @Test
    void givenStudentHaveNoName() {
        Student student = new Student();
        Assertions.assertThrows(DataIntegrityViolationException.class,
                () -> this.studentDAO.save(student));
    }

需要时刻提醒自己的是:和很多注解一样,异常转换是通过代码模式完成的。所以相关的方法绝对不能够声明为final

5. Spring Data JPA仓库配置

使用@EnableJpaRepositories来启用Spring JPA的仓库支持(自动根据接口创建DAO实现),同时在该注解中需要同时指定扫描的基础包:

/**
 * 测试EnableJpaRepositories注解时,请 注释/启用 11,12,13行后
 * 分别查看单元测试EnableJpaRepositoriesTest中获取bean的效果
 */
@SpringBootApplication
@EnableJpaRepositories(
		basePackages = "club.codedemo.repository"
)
public class ThePersistenceLayerWithSpringDataJpaApplication {

使用XML的话,配置如下:

<jpa:repositories base-package="com.baeldung.spring.data.persistence.repository" />

6. JAVA或XML配置

在认识Spring JPA一文中已经对如何在Spring中配置JPA进行了深入了讨论。除了在前文中讨论的内容以外,如果说我们使用XML进行配置,则需要在@ImportResource中指定XML的位置:

@Configuration
@EnableTransactionManagement
@ImportResource("classpath*:*springDataConfig.xml")
public class PersistenceJPAConfig {
    ...
}

7. MAVEN依赖

就像前文中提及的一样,使用JPA还需要加入spring-data-jpa依赖:

<dependency>
   <groupId>org.springframework.data</groupId>
   <artifactId>spring-data-jpa</artifactId>
   <version>2.2.7.RELEASE</version>
</dependency>

8. 使用Spring Boot

我们还可以使用Spring Boot Starter Data JPA依赖,其将会自动的为项目配置数据源。

当然了,自动配置数据源的前提是需要让其能够检测到当前项目使用的数据源,如果在项目中并没有提供任何数据源,则将发生一个异常。比如我们在项目中添加H2内容数据库:

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-jpa</artifactId>
   <version>2.2.6.RELEASE</version>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>1.4.200</version>
</dependency>

则项目在启动时,将自动加载H2数据库做为数据源。

在未进行任何配置的情况下,Spring Boot在启动时,将会按标准的默认配置进行加载。这些默认的配置项可以轻松的通过application.properties文件进行更改:

spring.datasource.url=jdbc:h2:mem:db;DB_CLOSE_DELAY=-1
spring.datasource.username=sa
spring.datasource.password=sa

比如我们通过以代码来修改数据源的地址以及数据库的认证信息。

9. 总结

本文对Spring 5、JPA2、Sptring Data JPA中数据持久化层的配置及实现进行介绍。在介绍的过程中给出了基于xml以及基于java的配置示例。

本文还讨论了自定义高级查询的几种方法、事务机制、异常转换以及如何扫描一个项目外的jpa命名空间。

总之Spring JPA是一种新颖、优雅的数据访问方式,它允许我们在只定义接口及方法名称的前提下,快速的实现对数据库的访问。

分类
Spring Data

Spring Data JPA 条件查询

1. 简介

Spring Data JPA中提供了多种数据查询的方式,比如说定义查询方法或使用自定义的JPQL。但有些时候,上述查询方式并不能够很好的满足我们对复杂综合查询的需要,这时候就需要Criteria APIQueryDSL登场了。

Criteria API提供了一种使用代码创建动态查询语句的功能,相较于编写传统的SQL语句,该方法能够有效的避免一些语法错误。当结合Metamodel API使用时,甚至可以在编译的阶段发现一些字段或类型的错误。

当然,凡是都有两面性,它的缺点是:即使实现一个看似不起眼的查询方法,也需要编写长篇的样板(只所以称为样板是由于每次写的都差不多)代码,这使得阅读代码的时候不是很爽;再者由于需要接受一些新的知识,所以对于初次接触Criteria API的小伙伴们并不友好。

本文将讨论如何使用criteria queries(条件查询)来实现自定义的数据访问逻辑,以及Spring是如果帮助我们减少样板代码的。

2. 需求假设

假设如下图书(Book)实体,我们需要实现的功能是:查询某个作者(author)并且图书名称包含特定的关键字的所有图书。

/**
 * 图书
 */
@Entity
public class Book {
    @Id
    @GeneratedValue
    private Long id;

    /**
     * 名称
     */
    String title;

    /**
     * 作者
     */
    String author;
    // setter and getter
}

本着一事一议、尽量减少干扰项的原则,本文引入Metamodel API。

3. 数据仓库类

按Spring规范,我们使用@Repository 来标识自定义数据仓库。要实现Criteria API还需要在数据仓库中注入EntityManager实例:

/**
 * 图书数据访问对象
 */
@Repository
public class BookDao {

    /**
     * 实体管理器
     */
    @Autowired
    EntityManager em;

    /**
     * 查询图书
     * @param authorName 作者名称(精确查询)
     * @param title 图书名称 模糊查询
     * @return
     */
    List<Book> findBooksByAuthorNameAndTitle(String authorName, String title) {
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<Book> cq = cb.createQuery(Book.class);

        Root<Book> book = cq.from(Book.class);
        Predicate authorNamePredicate = cb.equal(book.get("author"), authorName);
        Predicate titlePredicate = cb.like(book.get("title"), "%" + title + "%");
        cq.where(authorNamePredicate, titlePredicate);

        TypedQuery<Book> query = em.createQuery(cq);
        return query.getResultList();
    }
}

上述代码遵循了标准的Criteria API 工作流:

  • 首先获取了一个CriteriaBuilderCriteriaBuilder是整个查询功能的基础。
  • 使用CriteriaBuilder创建了一个CriteriaQuery<Book>,泛型中的Book指明了我们想获取的数据是Book实体。
  • 使用CriteriaQuery<Book>的from方法来获取了一个Root<Book> book,指定了此次查询的起点为Book实体(对应book) 数据表。
  • 接下来使用CriteriaBuilder创建了两个基于Book实体的查询条件(仅仅是创建了一个查询条件,只有将其放入正式的查询中,该查询条件才会生效)。查询条件一用于查找book表中的author字段与传入authorname完全相同的记录;条询条件二用于查找book表的title字段左右模糊匹配title字段。
  • cp.where将两个查询条件组合到一起,表示:查询满足查询条件一并且同时满足查询条件二的数据。
  • 然后使用CriteriaQuery创建了一个TypedQuery<Book>实例,该实例具备了数据查询的能力。
  • 最后调用了TypedQuery的getResultList()方法,此时将综合查询条件进行数据查询,并且将获取的数据依次填充到CriteriaQuery<Book>上规定的<Book>实体上。

值得提出的一点时,由于我们在当前数据访问对象BookDao使用@Repository注解,所以spring将默认托管该类所产生的异常。

4. 使用自定义的查询的方法扩展数据仓库层

虽然我们可以使用Spring Data中根据方法名称自动创建动态查询的功能来完成一些个性化的查询工作。但当有一些些逻辑处理参杂在查询过程中时(比如对null值进行过滤),这种自动创建查询方法的功能便力不从心了。

当然了,我们完全可以像上一节展示的一样来创建自定义的数据仓库来实现一些复要的查询功能。

但我想说的是,参考Spring Data组合数据仓库一文将一些自定义的方法添加到JPA的@Repository接口机制,使用继承接口的方法来完成数据创建的初始化,会有一种更好开发体验。

比如我们创建如下BookRepositoryCustom接口:

/**
 * 自定义的Book数据仓库
 */
public interface BookRepositoryCustom {
    /**
     * 查询图书
     * @param authorName 作者名(精确)
     * @param title 图书名称(模糊)
     * @return
     */
    List<Book> findBooksByAuthorNameAndTitle(String authorName, String title);
}

然后在创建的数据仓库接口中继承BookRepositoryCustom:

interface BookRepository extends JpaRepository<Book, Long>, BookRepositoryCustom {}

值得注意的是,如果想使用BookDao中的findBooksByAuthorNameAndTitle方法做为BookRepositoryCustom中方法的实现,则必须将BookDao重命名为BookRepositoryImpl,同时实现BookRepositoryCustom接口。

在此,我们保持原BookDao名称不变。并新建BookRepositoryImpl:

/**
 * 图书数据访问对象
 */
@Repository
public class BookRepositoryImpl implements BookRepositoryCustom {

    /**
     * 实体管理器
     */
    @Autowired
    EntityManager em;

    /**
     * 查询图书
     * @param authorName 作者名称(精确查询)
     * @param title 图书名称 模糊查询
     * @return
     */
    @Override
    public List<Book> findBooksByAuthorNameAndTitle(String authorName, String title) {
    }
}

然后使用另外一种方法来实现查询的功能,该功能将对排除掉值为null的传入参数:

@Override
List<Book> findBooksByAuthorNameAndTitle(String authorName, String title) {
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery<Book> cq = cb.createQuery(Book.class);
 
    Root<Book> book = cq.from(Book.class);
    List<Predicate> predicates = new ArrayList<>();
    
    if (authorName != null) {
        predicates.add(cb.equal(book.get("author"), authorName));
    }
    if (title != null) {
        predicates.add(cb.like(book.get("title"), "%" + title + "%"));
    }
    cq.where(predicates.toArray(new Predicate[0]));
 
    return em.createQuery(cq).getResultList();
}

其实无论是哪种方法,都可以发现同一个特点:代码像懒婆娘的裹脚布一样,又臭又长。

接下来,我们继续展示使用JPA Specifications来掀开这又臭又长的裹脚布。

5. JPA Specifications

Spring Data 提供了org.springframework.data.jpa.domain.Specification来简化查询操作时的样板代码:

interface Specification<T> {
    Predicate toPredicate(Root<T> root, CriteriaQuery query, CriteriaBuilder cb);
}

该接口提供了toPredicate方法,在方法中注入了样板代码中需要的root、CriteriaQuery以及CriteriaBuilder。我们可以利用该接口来快速的创建查询条件:

/**
 * 查询作者
 * @param author 作者
 * @return
 */
static Specification<Book> hasAuthor(String author) {
    return (book, cq, cb) -> cb.equal(book.get("author"), author);
}
 
/**
 * 查询图书名
 * @param title 名称
 * @return
 */
static Specification<Book> titleContains(String title) {
    return (book, cq, cb) -> cb.like(book.get("title"), "%" + title + "%");
}

在使用上述查询条件以前,我们还需要在数据仓库上继承org.springframework.data.jpa.repository.JpaSpecificationExecutor<T>

interface BookRepository extends JpaRepository<Book, Long>, JpaSpecificationExecutor<Book> {}

此时,当前数据仓库层便能够处理Spring Data的Specification查询条件了。此时,如若获取某个作者的所有图书,则可以简短的这样写:

bookRepository.findAll(hasAuthor(author));

美中不足的是,JpaSpecificationExecutor提供的findAll仅仅支持传入一个Specification查询条件。幸运的是我们可以使用org.springframework.data.jpa.domain.Specification接口提供的方法来完成查询条件的组合。

比如将作者与图书名两个查询条件组合在一起进行综合查询:

bookRepository.findAll(where(hasAuthor(author)).and(titleContains(title)));

上述代码中,我们使用了Specification接口上的where方法来完成了两个查询条件的组合。

这样以来,得益于伟大的Spring,我们在实现了查询模块化(可以轻易的按某一个或多个条件进行组合查询)的同时,还让代码看起来没有那么臃肿。

当然了,这并不意味着我们构造综合查询时可以完全的脱离样板化的代码。在有些时候,合适样板化代码来完成一些特定的查询功能也是非常有必要的。比如我们在查询中若想使用group进行分组,或是查询A数据表后,将数据填充到B实体上,再或者进行一些子查询。

条条大路通北京。综合查询的实现也是一样,没有最好的方法,只有最合适的方法。在实际的使用过程中,应该根据当前的业务需求,尝试选择出最合适的那个查询方式。

6. 总结

本文中先后介绍了3种不同的综合查询方法:

  • 最简单、直接的创建自定义的DAO
  • @Repository进行扩展,就像访问标准的查询方法一样调用综合查询方法
  • 使用Specification减少样板化的代码

同样,你可以打开文章顶部的github地址来获取一份与本文相同的且可运行的code demo。不止如此,在code demo中我们还提供了用于验证的单元测试代码。希望能对你有所帮助。

分类
persistence

Criteria API IN表达式使用示例

1. 概述

我们在进行查询时,经常会遇到查询实体的某个字段位于某个范围的情况。

本文我们将介绍如何使用Criteria API来解决此类问题。

2. 示例实体

在正式开始以前,我们先准备如下两个实体:Department以及DeptEmployee,这两个实体间的关系为 1:n

DeptEmployee实体如下,该实体中使用@ManyToOne注解声明了多对一的关系。

@Entity
public class DeptEmployee {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private long id;
 
    private String title;
 
    @ManyToOne
    private Department department;
    
    // 以下省略了setter/getter
}

Department 实体如下,使用@OneToMany注解声明了一对多的关系:

@Entity
public class Department {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private long id;
 
    private String name;
 
    @OneToMany(mappedBy="department")
    private List<DeptEmployee> employees;

    // 以下省略了setter/getter
}

3. CriteriaBuilder.In

首先让我们看一下CriteriaBuilder接口。该接口上定义了一个in()方法,此方法接收的参数类型为 Expression,返回值类型为Predicate。将返回的Predicate做为查询条件传入至where()方法中,即可将范围做为某个字段的查询条件:

CriteriaQuery<DeptEmployee> criteriaQuery = 
  criteriaBuilder.createQuery(DeptEmployee.class);
Root<DeptEmployee> root = criteriaQuery.from(DeptEmployee.class);
In<String> inClause = criteriaBuilder.in(root.get("title"));
for (String title : titles) {
    inClause.value(title);
}
criteriaQuery.select(root).where(inClause);

在Spring Boot中,你可以如下获取criteriaBuilder(GITHUB示例代码):

    @Autowired
    EntityManager entityManager;
    ...
    CriteriaBuilder criteriaBuilder = this.entityManager.getCriteriaBuilder();

或者配合仓库层如下使用(GITHUB示例代码):

    @Test
    void spring() {
        String[] titles = {title};
        Specification<DeptEmployee> deptEmployeeSpecification = new Specification<DeptEmployee>() {
            @Override
            public Predicate toPredicate(Root<DeptEmployee> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {

                CriteriaBuilder.In<String> inClause = criteriaBuilder.in(root.get("title"));
                for (String title : titles) {
                    inClause.value(title);
                }
               return inClause;
            }
        };


        assertEquals(10,  this.deptEmployeeCrudRepository.findAll(deptEmployeeSpecification).size());
    }

4. Expression.In

另外,还可以使用Expression接口提供的in()方法:

criteriaQuery.select(root)
  .where(root.get("title")
  .in(titles));

CriteriaBuilder.in()不同的是,Expression.in() 接收的参数类型为Collection。对比上述两处代码可以看出Expression.in() 的写法更简单一些。

5. 在子查询中使用IN表达式

接下来让我们看看如何在子查询中使用IN表达式。

比如有如下需求:查询出所有部门名称中包含某个搜索值的所有部门Department的所有员工DeptEmployee

        Subquery<Department> subquery = criteriaQuery.subquery(Department.class);
        Root<Department> dept = subquery.from(Department.class);
        subquery.select(dept)
                .distinct(true)
                .where(criteriaBuilder.like(dept.get("name"), "%" + departmentName + "%"));

        criteriaQuery.select(employeeRoot)
                                                            .where(criteriaBuilder.in(employeeRoot.get("department")).value(subquery));

如上代码:我们创建一个名为subquery的子查询,并将其做为一个表达式传入了value()方法中。

6. 总结

本文中,我们介绍了几种IN查询的方法,在文章的最后对子查询给出了示例。希望对你能有所帮助。

分类
Series Spring Persistence

Spring 持久化(操作数据库)系列教程