在 Spring Data JPA 中创建动态查询

1、概览

在使用 Spring Data 开发应用时,我们经常需要根据选择条件构建动态查询,以便从数据库中获取数据。

本文将带你了解在 Spring Data JPA Repository 中创建动态查询的三种方法:Example 查询、Specification 查询和 Querydsl 查询。

2、示例

创建 SchoolStudent 两个实体。这两个实体类之间的关系是一对多,即一个 School 可以有多个 Student

@Entity
@Table
public class School {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column
    private Long id;

    @Column
    private String name;

    @Column
    private String borough;

    @OneToMany(mappedBy = "school")
    private List<Student> studentList;

    // 构造函数、Getter、Setter 省略
}
@Entity
@Table
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column
    private Long id;

    @Column
    private String name;

    @Column
    private Integer age;

    @ManyToOne
    private School school;

    // 构造函数、Getter、Setter 省略
}

除了实体类,还要为 Student 实体定义一个 Spring Data Repository:

public interface StudentRepository extends JpaRepository<Student, Long> {
}

最后,在 School 表中添加一些示例数据:

id name borough
1 University of West London Ealing
2 Kingston University Kingston upon Thames

同样 Student 表也需要一些示例数据。

id name age school_id
1 Emily Smith 20 2
2 James Smith 20 1
3 Maria Johnson 22 1
4 Michael Brown 21 1
5 Sophia Smith 22 1

接下来,我们将通过不同的方法实现如下查询:

  • Student 姓名(name)以 Smith 结尾,且
  • Student 年龄(age)为 20 岁,且
  • Student 所在学校(School)位于伊灵区(Ealing

3、Example 查询

Spring Data 提供了一种使用 Example(示例)查询实体的简单方法。这个想法很简单:创建一个示例实体,并在其中设置要过滤的条件字段。然后,使用该 Example 查找与之匹配的实体。

要采用这种方法,Repository 必须实现 QueryByExampleExecutor 接口。在本例中,JpaRepository 已经继承了该接口,而我们通常会在 Repository 中继承 JpaRepository 接口。因此,没有必要明确地实现它。

现在,创建一个 Student Example,包含我们要过滤的三个条件:

School schoolExample = new School();
schoolExample.setBorough("Ealing");

Student studentExample = new Student();
studentExample.setAge(20);
studentExample.setName("Smith");
studentExample.setSchool(schoolExample);

Example example = Example.of(studentExample);

创建好了 Example 后,调用 Repository 的 findAll(...) 方法来获取结果:

List<Student> studentList = studentRepository.findAll(example);

不过,上面的示例只支持精确匹配。如果我们想获取姓名以 “Smith” 结尾的学生,就需要自定义匹配策略。Example 查询提供了 ExampleMatcher 类来实现这一点。我们只需在 name 字段上创建一个 ExampleMatcher,并将其应用于 Example 实例:

ExampleMatcher customExampleMatcher = ExampleMatcher.matching()
  .withMatcher("name", ExampleMatcher.GenericPropertyMatchers.endsWith().ignoreCase());
Example<Student> example = Example.of(studentExample, customExampleMatcher);

这里使用的 ExampleMatcher 不言自明。它使 name 字段不区分大小写地匹配,并确保值以 Example 中指定的名称结尾。

Example 查询易于理解和实现。不过,它不支持更复杂的查询,如某个字段大于或小于的条件。

4、Specification 查询

Spring Data JPA 中的 Specification 查询功能允许使用 Specification 接口根据一组条件创建动态查询。

与派生查询方法或使用 @Query 的自定义查询等传统方法相比,这种方法更加灵活。它适用于复杂的查询要求,或需要在运行时动态调整查询的情况。

Example 查询类似,我们的 Repository 接口也必须继承一个接口才能启用此功能。这一次,我们需要继承 JpaSpecificationExecutor

public interface StudentRepository extends JpaRepository<Student, Long>, JpaSpecificationExecutor<Student> {
}

接下来,为每个过滤条件定义各自的方法,这主要是为了清晰和使其更易读:

public class StudentSpecification {
    public static Specification<Student> nameEndsWithIgnoreCase(String name) {
        return (root, query, criteriaBuilder) ->
          criteriaBuilder.like(criteriaBuilder.lower(root.get("name")), "%" + name.toLowerCase());
    }

    public static Specification<Student> isAge(int age) {
        return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get("age"), age);
    }

    public static Specification<Student> isSchoolBorough(String borough) {
        return (root, query, criteriaBuilder) -> {
            Join<Student, School> scchoolJoin = root.join("school");
            return criteriaBuilder.equal(scchoolJoin.get("borough"), borough);
        };
    }
}

如上,使用 CriteriaBuilder 来构建过滤条件。CriteriaBuilder 可以帮助我们以编程式的方法在 JPA 中构建动态查询,并为我们提供类似于编写 SQL 查询的灵活性。它允许我们使用 equal(...)like(...) 等方法创建谓词(Predicate)来定义条件。

如果是更复杂的操作,例如 JOIN 查询,可以使用 Root.join(...)Root 作为 FROM 子句的锚(根对象),提供对实体属性和关系的访问。

现在,调用 Repository 方法,按 Specification 获取过滤后的结果:

Specification<Student> studentSpec = Specification
  .where(StudentSpecification.nameEndsWithIgnoreCase("smith"))
  .and(StudentSpecification.isAge(20))
  .and(StudentSpecification.isSchoolBorough("Ealing"));
List<Student> studentList = studentRepository.findAll(studentSpec);

5、QueryDsl 查询

Example 相比,Specification 功能强大,能够处理更复杂的查询。不过,当我们处理包含许多选择条件的复杂查询时,Specification 接口可能会变得冗长,难以阅读。

QueryDSL 尝试用一种更直观的解决方案来解决 Specification 的局限性。它是一个类型安全的框架,用于以直观、可读和强类型的方式创建动态查询。

为了使用 QueryDSL,需要在 pom.xml 中添 Querydsl JPAAPT 支持 的依赖:

<dependency>
    <groupId>com.querydsl</groupId>
    <artifactId>querydsl-jpa</artifactId>
    <version>5.1.0</version>
    <classifier>jakarta</classifier>
</dependency>
<dependency>
    <groupId>com.querydsl</groupId>
    <artifactId>querydsl-apt</artifactId>
    <version>5.1.0</version>
    <classifier>jakarta</classifier>
    <scope>provided</scope>
</dependency>

需要注意的是,在 JPA 3.0 中,JPA 的包名称从 javax.persistence 变为 jakarata.persistence。如果使用 3.0 及以后的版本,还必须在依赖中加入 jakarta classifier。

除了依赖,还必须在 pom.xml 的插件(plugin)部分加入以下注解处理器:

<plugin>
    <groupId>com.mysema.maven</groupId>
    <artifactId>apt-maven-plugin</artifactId>
    <version>1.1.3</version>
    <executions>
        <execution>
            <phase>generate-sources</phase>
            <goals>
                <goal>process</goal>
            </goals>
            <configuration>
                <outputDirectory>target/generated-sources</outputDirectory>
                <processor>com.mysema.query.apt.jpa.JPAAnnotationProcessor</processor>
            </configuration>
        </execution>
    </executions>
</plugin>

该处理器会在编译时为我们的实体类生成元模型类。将这些设置纳入应用并编译后,可以看到 Querydsl 在构建文件夹中生成了两个查询类:QStudentQSchool

同样,Repository 需要继承 QuerydslPredicateExecutor 接口才能通过这些查询类来检索结果:

public interface StudentRepository extends JpaRepository<Student, Long>, QuerydslPredicateExecutor<Student>{
}

接下来,我们要根据这些查询类创建一个动态查询,并在 StudentRepository 中使用它进行查询。这些查询类已经包含了相应实体类的所有属性。因此,我们可以在构建 Predicate 时直接引用所需的字段:

QStudent qStudent = QStudent.student;
BooleanExpression predicate = qStudent.name.endsWithIgnoreCase("smith")
  .and(qStudent.age.eq(20))
  .and(qStudent.school.borough.eq("Ealing"));
List studentList = (List) studentRepository.findAll(predicate);

如上所示,使用查询类定义查询条件既简单又直观。

尽管需要添加额外的依赖并配置插件略显复杂,但它提供了与 Specification 相同的 Fluent 风格调用,更加直观易读。

而且,不需要像在 Specification 类中那样手动明确定义过滤条件。

6、总结

本文介绍了在 Spring Data JPA 中创建动态查询的不同方法。

  • Example 查询最适用于简单的精确匹配查询。
  • 如果我们需要更多类似 SQL 的表达式和比较,Specification 查询对于中等复杂程度的查询非常适合。
  • 通过 QueryDSL 进行查询最适合高度复杂的查询,因为它在使用查询类定义条件时非常简单。

Ref:https://www.baeldung.com/spring-data-jpa-query-arbitrary-and-clauses