英文:
How to make a dynamic Spring (Boot) JPA query with paging?
问题
为了过于简化这个问题:我们有一个类/表格 Wine(表格"wines"),其中包含以下属性之一:
- name:String
- description:String
- origin:Origin
...其中Origin是另一个类(带有表格"origins"),只有region:String
和country:String
。
我正在尝试创建一个在Repository中供RestController使用的搜索方法。
在RestController中的方法声明如下:
@GetMapping("/search")
public Wine searchProduct(
@RequestParam Optional<String> searchTerm,
@RequestParam Optional<Origin> origin) {
// ???
}
我现在要做的是:为数据库创建一个查询,如果给定了searchTerm,则使用它,origin也是同理。查询还应该支持分页。
示例:
SELECT * FROM wines JOIN origins ON wines.origin_id = origins.id
WHERE (name LIKE $searchTerm OR description LIKE $searchTerm) AND (/*origin check*/)
如果没有提供搜索条件,查询中就不应该有整个"() AND
"部分。如果没有提供Origin... 你明白的。
我尝试过的方法:
-
(天真地)在我的Repository(实现了CrudRepository)中构建一个庞大的查询,就像这里一样。
Page<Wine> findWinesByNameLikeOrDescriptionLikeAndOriginEquals(..., Pageable pageable);
- 除了非常丑陋(特别是对于更多的属性),这可能不起作用,因为:
- 没有定义OR或AND更重要(没有括号)。
- 不知道是否可以直接将一个"Origins"对象放进去让它起作用。
- 不知道Pageable是否在这种自定义方法中起作用。
- 除了非常丑陋(特别是对于更多的属性),这可能不起作用,因为:
-
使用"Specifications and Querydsl",如spring.io 这里所建议的。
- 我太愚笨了,无法理解,尤其是在帖子底部的那些Q类,或者开头的下划线类。这似乎对于它应该做的事情来说过于复杂。
- 而且没有分页选项。但是,有一个可能的解决方法,就像这里所示,但是由于使用那些下划线和/或Q类太过繁琐,所以我没有尝试。
-
此外,这里是我从2013年找到的一个示例,我甚至不能从总体上理解,但看起来似乎非常合适。
英文:
To oversimplify the problem: we have a class/table Wine (table "wines") which has, among other attributes:
- name: String
- description: String
- origin: Origin
... where Origin is another class (with table "origins") with just region: String
and country: String
.
What I'm trying to make is a search method in my repository for the RestController to use.
The method declaration in the RestController is like this:
@GetMapping("/search")
public Wine searchProduct(
@RequestParam Optional<String> searchTerm,
@RequestParam Optional<Origin> origin) {
// ???
}
What I'm trying to do now is the following: creating a query for the database where the searchTerm is used if given, same with origin. And it should be pageable.
Example:
SELECT * FROM wines JOIN origins ON wines.origin_id = origins.id
WHERE (name LIKE $searchTerm OR description LIKE $searchTerm) AND (/*origin check*/)
If no search term is given, the whole "() AND" part should not be in the query. If no Origin is given... you get it.
Things I've tried:
-
(Naively) building a massive query in my repository (implements CrudRepository) like here.
Page<Wine> findWinesByNameLikeOrDescriptionLikeAndOriginEquals(..., Pageable pageable);
- Which (apart from being super ugly, especially for more attributes) probably doesn't work because:
- It's not defined whether OR or AND are more important (no brackets).
- No idea whether I can just shove an "Origins" object in there for it to work.
- No idea whether Pageable even works if that's a custom method.
- Which (apart from being super ugly, especially for more attributes) probably doesn't work because:
-
Using "Specifications and Querydsl" as suggested by spring.io here.
- Which I'm just to stupid to understand, especially with those Q classes at the bottom of the post or the _ classes in the beginning. It just seems way too compicated for what it should do.
- Also there are no pagination options. There is, however, a possible fix like here but I didn't get to it since using those _ and/or Q classes was to much of an overhead to start with.
-
Also this is just an example I found from 2013 which I don't even understand in general but kinda looks like it would be very fitting.
答案1
得分: 3
如果你发现很难理解规格说明
,只需使用这两个搜索参数,并假设Wine
实体与Origin
有@ManyToOne
关系,你可以进行以下操作:
@Query("SELECT w FROM wines w join w.origin o where " +
"( :searchTerm is null or " +
" w.name like CONCAT('%',:searchTerm ,'%') or " +
" w.description like CONCAT('%', :searchTerm ,'%')) " +
"AND (:origin is null or o = :origin)")
Page<Wine> searchWines(String searchTerm, Origin origin, Pageable p);
当你调用它时,
wineRepository.searchWines(searchTerm.orElse(null), origin.orElse(null));
英文:
If you find it difficult to understand Specifications
, only have those two search parameters and assuming Wine
entity has a @ManyToOne
to Origin
, you can do the following:
@Query("SELECT w FROM wines w join w.origin o where " +
"( :searchTerm is null or " +
" w.name like CONCAT('%',:searchTerm ,'%') or " +
" w.description like CONCAT('%', :searchTerm ,'%')" +
") " +
"AND (:origin is null or o = :origin)")
Page<Wine> searchWines(String searchTerm, Origin origin, Pageable p);
And when you call it,
wineRepository.searchWines(searchTerm.orElse(null), origin.orElse(null));
答案2
得分: 2
我实际上已经有一个可行的解决方案了!对于好奇的人,以下是我是如何做到的:
ProductController.java
@GetMapping("search")
public Page<Wine> searchProducts(
@RequestParam(name = "text", required = false) String searchTerm,
@RequestParam(required = false) Origin origin,
@RequestParam(required = false) Integer page) {
// 根据传入的 Integer page 生成 PageRequest:
Pageable pageRequest = PageRequest.of(page != null ? page : 0, 10);
if(Objects.isNull(searchTerm) && Objects.isNull(origin)) {
return wineService.findAll(pageRequest);
}
return wineService.searchWines(
searchTerm,
origin,
pageRequest
);
}
WineService.java
public Page<Wine> searchWines(String searchTerm, Origin origin, Pageable pageable) {
List<Specification<Wine>> specifications = new LinkedList<>();
if (searchTerm != null) {
specifications.add(ProductSpecification.hasSearchStringInNameOrDescription(searchTerm));
}
if (origin != null) {
specifications.add(ProductSpecification.hasOrigin(origin));
}
if (specifications.isEmpty()) {
return wineRepository.findAll(pageable);
} else {
Specification<Wine> query = Specification.where(specifications.remove(0));
for (Specification<Wine> wineSpecification : specifications) {
query = query.and(wineSpecification);
}
return wineRepository.findAll(query, pageable);
}
}
以名称/描述字符串为例,以下是规范:
ProductSpecification.java
public static Specification<Wine> hasSearchStringInNameOrDescription(String input) {
final String searchTerm = input.toLowerCase();
return (root, criteriaQuery, criteriaBuilder) -> {
log.info("SearchTerm: " + searchTerm);
Predicate pName = criteriaBuilder.like(
criteriaBuilder.lower(root.get(Wine_.NAME)),
"%" + searchTerm + "%"
);
Predicate pDescription = criteriaBuilder.like(
criteriaBuilder.lower(root.get(Wine_.DESCRIPTION)),
"%" + searchTerm + "%"
);
return criteriaBuilder.or(pName, pDescription);
};
}
生成那些 Wine_ 类是使用 Maven 插件自动生成的。参见 这里。此外,要将请求体片段转换为像 Origin 这样的复杂对象,需要包含一个转换器类,类似于 这里的示例。
英文:
So I actually got a solution working! For curious people, here's how I did it:
ProductController.java
@GetMapping("search")
public Page<Wine> searchProducts(
@RequestParam(name = "text", required = false) String searchTerm,
@RequestParam(required = false) Origin origin,
@RequestParam(required = false) Integer page) {
// generate PageRequest based on Integer page if given:
Pageable pageRequest = PageRequest.of(page != null ? page : 0, 10);
if(Objects.isNull(searchTerm) && Objects.isNull(origin)) {
return wineService.findAll(pageRequest);
}
return wineService.searchWines(
searchTerm,
origin,
pageRequest
);
}
WineService.java
public Page<Wine> searchWines(String searchTerm, Origin origin, Pageable pageable) {
List<Specification<Wine>> specifications = new LinkedList<>();
if (searchTerm != null) {
specifications.add(ProductSpecification.hasSearchStringInNameOrDescription(searchTerm));
}
if (origin != null) {
specifications.add(ProductSpecification.hasOrigin(origin));
}
if (specifications.isEmpty()) {
return wineRepository.findAll(pageable);
} else {
Specification<Wine> query = Specification.where(specifications.remove(0));
for (Specification<Wine> wineSpecification : specifications) {
query = query.and(wineSpecification);
}
return wineRepository.findAll(query, pageable);
}
}
as an example, here's the specification for the name/description string:
ProductSpecification.java
public static Specification<Wine> hasSearchStringInNameOrDescription(String input) {
final String searchTerm = input.toLowerCase();
return (root, criteriaQuery, criteriaBuilder) -> {
log.info("SearchTerm: " + searchTerm);
Predicate pName = criteriaBuilder.like(
criteriaBuilder.lower(root.get(Wine_.NAME)),
"%" + searchTerm + "%"
);
Predicate pDescription = criteriaBuilder.like(
criteriaBuilder.lower(root.get(Wine_.DESCRIPTION)),
"%" + searchTerm + "%"
);
return criteriaBuilder.or(pName, pDescription);
};
}
The generation of those Wine_ classes is automatic with a Maven plugin. See this.
Additionally, for the conversion of request body pieces to complex objects like Origin, you need to include a converter class like here.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论