如何使用Spring Data轻松实现一种“REST API查询语言”来过滤实体?
例如,对于以下
Person
实体:
@Data
@Entity
public class Person {
@Id
@GeneratedValue
private Long id;
private LocalDate dob; // date of birth
private String name;
@Formula("timestampdiff('year', dob, now())")
private Integer age;
public Person(String name, LocalDate dob) {
this.name = name;
this.dob = dob;
}
}
我想通过这样的请求获取它的数据:
GET /people?name=jo&age=18&page=1&sort=name,desc
即:“获取所有
name
包含“jo”(不区分大小写)且 age
等于 18 的人的第一页,按 name
降序排序”。
借助Querydsl Web支持,Web支持Spring Data扩展的部分,我们可以轻松实现一种“REST API查询语言”来过滤我们的实体。
我们需要做的就是执行以下操作:
1)从
QuerydslPredicateExecutor
,扩展我们的存储库
2) 将
Predicate
和注释 @QuerydslPredicate
作为参数添加到我们的 REST 控制器方法
3)在存储库的
findAll
方法中使用此谓词:
public interface PersonRepo extends JpaRepository<Person, Long>, QuerydslPredicateExecutor<Person> {
}
@RequiredArgsConstructor
@RestController
@RequestMapping("/people")
public class PersonController {
private final PersonRepo personRepo;
@GetMapping
public ResponseEntity getFiltered(@QuerydslPredicate(root = Person.class) Predicate predicate, Pageable pageable) {
return ResponseEntity.ok(personRepo.findAll(predicate, pageable)));
}
}
然后我们将能够请求我们的数据:
GET /people?name=John&age=18&page=1&sort=name,desc
接下来我们必须设置不区分大小写的“like”过滤器。为此,我们从
QuerydslBinderCustomizer
扩展我们的存储库并覆盖其 customize
方法(就在存储库中):
public interface PersonRepo extends
JpaRepository<Person, Long>,
QuerydslPredicateExecutor<Person>,
QuerydslBinderCustomizer<QPerson> {
@Override
default void customize(QuerydslBindings bindings, QPerson person) {
// Make case-insensitive 'like' filter for all string properties
bindings.bind(String.class).first((SingleValueBinding<StringPath, String>) StringExpression::containsIgnoreCase);
}
}
为了使其正常工作,我们必须将参数
bindings
添加到控制器方法的 @QuerydslPredicate
中:
@GetMapping
public ResponseEntity getFiltered(
@QuerydslPredicate(root = Person.class, bindings = PersonRepo.class) Predicate predicate,
Pageable pageable
) {
return ResponseEntity.ok(personRepo.findAll(predicate, pageable)));
}
现在我们可以按照问题中的要求请求数据:
GET /people?name=jo&age=18&page=1&sort=name,desc
使用
QuerydslBinderCustomizer
我们可以实现更复杂的过滤器,例如 between
和 greater or equal
过滤器(将此代码添加到 customize
方法中):
bindings.bind(person.age).all((path, value) -> {
Iterator<? extends Integer> it = value.iterator();
Integer from = it.next();
if (value.size() >= 2) {
Integer to = it.next();
return Optional.of(path.between(from, to)); // between
} else {
return Optional.of(path.goe(from)); // greater or equal
}
});
如果我们在请求中指定两个
age
参数,那么我们将获得年龄在这些参数之间之间的所有记录。如果我们只指定一个 age
参数 - 我们会得到年龄大于或等于该值的记录。
GET /people?age=18&age=30
...
获取所有年龄在 18 岁至 30 岁之间的人
GET /people?age=18
...
获取所有年龄大于或等于18岁的人
最后我们可以从过滤器中排除一些不必要的属性,例如实体id
(将此代码添加到
customize
方法中):
bindings.excluding(person.id);
要使用 Querydsl Web 支持,我们必须将这些依赖项和插件添加到我们的 Spring Boot 项目中:
<dependencies>
<!-- ... -->
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- ... -->
<plugin>
<groupId>com.mysema.maven</groupId>
<artifactId>apt-maven-plugin</artifactId>
<version>1.1.3</version>
<executions>
<execution>
<goals>
<goal>process</goal>
</goals>
<configuration>
<outputDirectory>target/generated-sources/annotations</outputDirectory>
<processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
然后,重要的是,
编译项目以构建我们实体的“Q 类”。
您可以在我的存储库中找到完整的示例演示:sb-querydsl-sd-demo,以及此演示的Postman API 文档 - 这里:带有 Querydsl 和 Spring Data 的 REST 查询语言。
rsql-querydsl
,它在支持的运算符和其他一些优势方面提供更多自由。首先将库添加到你的POM中:
<dependency>
<groupId>io.github.apulbere</groupId>
<artifactId>rsql-querydsl</artifactId>
<version>1.0</version>
</dependency>
然后定义一个代表您的搜索条件的 DTO。请注意,这是 DTO,而不是实体本身,这有助于将持久层与视图分开:
@Setter
@Getter
public class PersonCriteria {
StringCriteria name = StringCriteria.empty();
LongCriteria age = LongCriteria.empty();
}
最后构建谓词。请注意,您可以根据需要在搜索 DTO 和 Q 模型之间进行映射,这为 REST API 的外观提供了更大的灵活性:
@GetMapping("/people")
List<PersonDTO> search(PersonCriteria criteria, Pageable page) {
var predicate = criteria.age.match(QPerson.age)
.and(criteria.name.match(QPerson.name));
return peopleRepository.findAll(predicate, page)
.stream()
.map(personMapper::map)
.toList();
}