在 Spring Data JPA 中创建动态查询
1、概览
在使用 Spring Data 开发应用时,我们经常需要根据选择条件构建动态查询,以便从数据库中获取数据。
本文将带你了解在 Spring Data JPA Repository 中创建动态查询的三种方法:Example
查询、Specification
查询和 Querydsl 查询。
2、示例
创建 School
和 Student
两个实体。这两个实体类之间的关系是一对多,即一个 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 JPA 和 APT 支持 的依赖:
<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
在构建文件夹中生成了两个查询类:QStudent
和 QSchool
。
同样,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