如何创建一个带分页的动态Spring(Boot)JPA查询?

huangapple go评论75阅读模式
英文:

How to make a dynamic Spring (Boot) JPA query with paging?

问题

为了过于简化这个问题:我们有一个类/表格 Wine(表格"wines"),其中包含以下属性之一:

  • name:String
  • description:String
  • origin:Origin

...其中Origin是另一个类(带有表格"origins"),只有region:Stringcountry: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... 你明白的。

我尝试过的方法:

  1. (天真地)在我的Repository(实现了CrudRepository)中构建一个庞大的查询,就像这里一样。

    Page<Wine> findWinesByNameLikeOrDescriptionLikeAndOriginEquals(..., Pageable pageable);

    • 除了非常丑陋(特别是对于更多的属性),这可能不起作用,因为:
      1. 没有定义OR或AND更重要(没有括号)。
      2. 不知道是否可以直接将一个"Origins"对象放进去让它起作用。
      3. 不知道Pageable是否在这种自定义方法中起作用。
  2. 使用"Specifications and Querydsl",如spring.io 这里所建议的。

    • 我太愚笨了,无法理解,尤其是在帖子底部的那些Q类,或者开头的下划线类。这似乎对于它应该做的事情来说过于复杂。
    • 而且没有分页选项。但是,有一个可能的解决方法,就像这里所示,但是由于使用那些下划线和/或Q类太过繁琐,所以我没有尝试。
  3. 此外,这里是我从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(&quot;/search&quot;)
public Wine searchProduct(
		@RequestParam Optional&lt;String&gt; searchTerm,
		@RequestParam Optional&lt;Origin&gt; 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:

  1. (Naively) building a massive query in my repository (implements CrudRepository) like here.

    Page&lt;Wine&gt; findWinesByNameLikeOrDescriptionLikeAndOriginEquals(..., Pageable pageable);

    • Which (apart from being super ugly, especially for more attributes) probably doesn't work because:
      1. It's not defined whether OR or AND are more important (no brackets).
      2. No idea whether I can just shove an "Origins" object in there for it to work.
      3. No idea whether Pageable even works if that's a custom method.
  2. 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.
  3. 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(&quot;SELECT w FROM wines w join w.origin o where &quot; +
            &quot;(   :searchTerm is null or &quot; +
            &quot;    w.name like CONCAT(&#39;%&#39;,:searchTerm ,&#39;%&#39;) or &quot; +
            &quot;    w.description like CONCAT(&#39;%&#39;, :searchTerm ,&#39;%&#39;)&quot; +
            &quot;) &quot; +
            &quot;AND (:origin is null or  o = :origin)&quot;)
    Page&lt;Wine&gt; 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(&quot;search&quot;)
public Page&lt;Wine&gt; searchProducts(
		@RequestParam(name = &quot;text&quot;, 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) &amp;&amp; Objects.isNull(origin)) {
		return wineService.findAll(pageRequest);
	}
	return wineService.searchWines(
			searchTerm,
			origin,
			pageRequest
	);
}

WineService.java

public Page&lt;Wine&gt; searchWines(String searchTerm, Origin origin, Pageable pageable) {
	List&lt;Specification&lt;Wine&gt;&gt; specifications = new LinkedList&lt;&gt;();
	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&lt;Wine&gt; query = Specification.where(specifications.remove(0));
		for (Specification&lt;Wine&gt; 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&lt;Wine&gt; hasSearchStringInNameOrDescription(String input) {
	final String searchTerm = input.toLowerCase();
	return (root, criteriaQuery, criteriaBuilder) -&gt; {
		log.info(&quot;SearchTerm: &quot; + searchTerm);
		Predicate pName = criteriaBuilder.like(
				criteriaBuilder.lower(root.get(Wine_.NAME)),
				&quot;%&quot; + searchTerm + &quot;%&quot;
		);
		Predicate pDescription = criteriaBuilder.like(
				criteriaBuilder.lower(root.get(Wine_.DESCRIPTION)),
				&quot;%&quot; + searchTerm + &quot;%&quot;
		);
		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.

huangapple
  • 本文由 发表于 2020年7月24日 03:02:37
  • 转载请务必保留本文链接:https://go.coder-hub.com/63061380.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定