Spring Boot 和 JPA:使用可选的范围条件实现搜索查询

Posted

技术标签:

【中文标题】Spring Boot 和 JPA:使用可选的范围条件实现搜索查询【英文标题】:Spring Boot & JPA: Implementing search queries with optional, ranged criteria 【发布时间】:2018-06-29 00:05:47 【问题描述】:

这是SSCCE,展示的是研究成果,不是骗子,而且是主题!!!


这里是 Spring Boot REST 服务和 mysql。我有以下Profile 实体:

@Entity
@Table(name = "profiles")
public class Profile extends BaseEntity 
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "profile_given_name")
    private String givenName;

    @Column(name = "profile_surname")
    private String surname;

    @Column(name = "profile_is_male")
    private Integer isMale;

    @Column(name = "profile_height_meters", columnDefinition = "DOUBLE")
    private BigDecimal heightMeters;

    @Column(name = "profile_weight_kilos", columnDefinition = "DOUBLE")
    private BigDecimal weightKilos;

    @Column(name = "profile_dob")
    private Date dob;

    // Getters, setters & ctor down here

我还有一个 ProfileController,我想公开一个 GET 端点,它提供了一种非常灵活/稳健的方式来根据大量条件搜索 Profiles

# Search for women between 1.2 and 1.8 meters tall.
GET /v1/profiles?isMale=0&heightMeters="gt": 1.2, "lt": 1.8

# Search for men born after Jan 1, 1990 who weigh less than 100 kg.
GET /v1/profiles?isMale=1&dob="gt" : "1990-01-01 00:00:00"&weightKilos="lt": 100.0

等等

这是我的控制器:

@RestController
@RequestMapping("/v1/profiles")
public class ProfileResource 
  @Autowired
  ProfileRepository profileRepository;

  @GetMapping
  public ResponseEntity<Set<Profile>> searchProfiles(@RequestParam(value = "isMale", required = false) String isMaleVal,
                                              @RequestParam(value = "heightMeters", required = false) String heightMetersVal,
                                              @RequestParam(value = "weightKilos", required = false) String weightKilosVal,
                                              @RequestParam(value = "dob", required = false) String dobVal) 

      Integer isMaleVal;
      BooleanCriteria isMaleCriteria;
      if(isMaleVal != null) 
        // Parse the value which could either be "0" for female, "1" for male or something like
        // ?isMale=0,1 to indicate

        // BooleanCriteria would store which values male, female or both) to include in the search
      

      BigDecimal heighMeters;
      BigDecimalCriteria heightCriteria;
      if(heightMetersVal != null) 
        // Parse the value which like in the examples could be something like:
        // ?heightMeters="gt" : "1.0"

        // BigDecimalCriteria stores range information
      

      BigDecimal heighMeters;
      BigDecimalCriteria weightCriteria;
      if(weightKilosVal != null) 
        // Parse the value which like in the examples could be something like:
        // ?weightKilos="eq" : "100.5"

        // BigDecimalCriteria stores range information
      

      // Ditto for DOB and DateCriteria

      // TODO: How to pack all of these "criteria" POJOs into a
      // CrudRepository/JPQL query against the "profiles" table?
      Set<Profile> profiles = profileRepository.searchProfiles(
        isMaleCriteria, heightCriteria, weightCriteria, dobCriteria);
    

我对@9​​87654333@ 的想法是这样的:

// Basically it just stores the (validated) search criteria that comes in over the wire
// on the controller method
public class BigDecimalCriteria 
  private BigDecimal lowerBound;
  private Boolean lowerBoundInclusive;
  private BigDecimal upperBound;
  private Boolean upperBoundInclusive;

  // Getters, setters, ctors, etc.

由于所有这些搜索条件都是可选的(因此可以是 null),我一直在纠结如何在 ProfileRepository 中编写 JPQL 查询:

public interface ProfileRepository extends CrudRepository<Profile,Long> 
  @Query("???")
  public Set<Profile> searchProfiles();

如何实现@Query(...)ProfileRepository#searchProfiles 以启用我的所有搜索条件(给定所有允许的范围和要搜索的条件值),并允许任何条件为空/可选?

当然,如果有任何漂亮的小库,或者如果 Spring Boot/JPA 已经有解决方案,我会全力以赴!

【问题讨论】:

你看过querydsl querydsl.com 处理具有复杂条件条件的查询的最简单方法是使用CriteriaQuery。可以使用Specification 将条件查询顺利集成到 Spring Data 存储库中(参见此处的示例:spring.io/blog/2011/04/26/…) 为什么投反对票?这是SSCCE,展示了研究成果,不是骗子,而且是主题! 这是个骗子,没有任何研究,还包含大量无关紧要的东西。 ***.com/questions/32728843/…***.com/questions/45716923/… 以及更多处理此主题的变体。 @JensSchauder 更多你太大了!听起来 Spring Data 没有能力解决这类问题,有人 对它被叫出来感到不安.... 【参考方案1】:

可以通过spring data中JpaSpecificationExecutor的规范实现复杂查询。 Repository 接口必须扩展JpaSpecificationExecutor&lt;T&gt; 接口,以便我们可以通过创建新的Specification&lt;T&gt; 对象来指定数据库查询的条件。

诀窍在于将规范接口与JpaSpecificationExecutor 结合使用。 这是一个例子:

@Entity
@Table(name = "person")
public class Person 

 @Id
 @GeneratedValue(strategy = GenerationType.AUTO)
 private Long id;

 @Column(name = "name")
 private String name;

 @Column(name = "surname")
 private String surname;

 @Column(name = "city")
 private String city;

 @Column(name = "age")
 private Integer age;

        ....


然后我们定义我们的存储库:

public interface PersonRepository extends JpaRepository<Person, Long>, JpaSpecificationExecutor<Person> 


如您所见,我们扩展了另一个接口JpaSpecificationExecutor。该接口定义了通过规范类执行搜索的方法。

我们现在要做的是定义我们的规范,它将返回包含查询约束的Predicate(在示例中,PersonSpecification 正在执行查询 select * from person where name = ? or (surname = ? 和年龄 = ?)):

public class PersonSpecification implements Specification<Person> 

    private Person filter;

    public PersonSpecification(Person filter) 
        super();
        this.filter = filter;
    

    public Predicate toPredicate(Root<Person> root, CriteriaQuery<?> cq,
            CriteriaBuilder cb) 

        Predicate p = cb.disjunction();

        if (filter.getName() != null) 
            p.getExpressions()
                    .add(cb.equal(root.get("name"), filter.getName()));
        

        if (filter.getSurname() != null && filter.getAge() != null) 
            p.getExpressions().add(
                    cb.and(cb.equal(root.get("surname"), filter.getSurname()),
                            cb.equal(root.get("age"), filter.getAge())));
        

        return p;
    

现在是时候使用它了。以下代码片段展示了如何使用我们刚刚创建的规范:

...

Person filter = new Person();
filter.setName("Mario");
filter.setSurname("Verdi");
filter.setAge(25);

Specification<Person> spec = new PersonSpecification(filter);

List<Person> result = repository.findAll(spec);

Here 是 github 中的完整示例

您还可以使用规范创建任何复杂的查询

【讨论】:

很好的答案,假设我想根据 3 个参数过滤记录,并且每个参数都是可选的以传递给过滤记录,那么我需要在toPredicate 中写 6 个案例吗? 如果你只需要3,应该是3,意味着每个都有单独的条件,不需要与其他人分组,如果你的参数上有基于组的条件,那么你可以创建带有谓词的任意数量的案例。 是的,因为参数是可选的,所以可能存在组条件,如果只传递一个参数,则可能没有。所以我必须检查所有条件并为 6 个条件编写案例,因为我有 3 个参数 谢谢@bhushan 的时间,还有一个疑问,我必须过滤具有多对多关系的实体,我该如何传递参数。你能建议一下吗 你可以加入你的实体 "Join join = root.join("targetEntiiesVaribaleNameInSourceClass");"并添加谓词【参考方案2】:

在Querydsl 和Web support Spring Data 扩展的帮助下,Spring Data 中几乎已经实现了您所需要的。

您还应该从 QuerydslPredicateExecutor 扩展您的 repo,如果您使用的是 Spring Data REST,您可以通过基本过滤、分页和排序支持直接“从盒子”查询您的 repo 数据:

/profiles?isMale=0&heightMeters=1.7&sort=dob,desc&size=10&page=2

要实现更复杂的过滤器,您应该从 QuerydslBinderCustomizer 扩展您的 repo 并使用它的 customize 方法(就在您的 repo 中)。

例如,您可以为heightMeters 实现“介于”过滤器,为surname 实现“喜欢”过滤器:

public interface ProfileRepository extends JpaRepository<Profile, Long>, QuerydslPredicateExecutor<Profile>, QuerydslBinderCustomizer<QProfile> 

    @Override
    default void customize(QuerydslBindings bindings, QProfile profile) 

      bindings.excluding( // used to exclude unnecessary fields from the filter
          profile.id,
          profile.version,
          // ...
      );

      bindings.bind(profile.heightMeters).all((path, value) -> 

          Iterator<? extends BigDecimal> it = value.iterator();
          BigDecimal from = it.next();
          if (value.size() >= 2) 
              BigDecimal to = it.next();
              return path.between(from, to)); // between - if you specify heightMeters two times
           else 
              return path.goe(from); // or greter than - if you specify heightMeters one time
          
      );

      bindings.bind(profile.surname).first(StringExpression::containsIgnoreCase);        
    

然后您可以查询您的个人资料:

/profiles?isMale=0&heightMeters=1.4&heightMeters=1.6&surename=doe

即- 找出所有身高在 1.4 到 1.6 米之间且surename 包含“doe”的女性。

如果您不使用 Spring Data REST,您可以使用 QueryDSL 支持实现自己的 REST 控制器方法:

@RestController
@RequestMapping("/profiles")
public class ProfileController 

    @Autowired private ProfileRepository profileRepo;

    @GetMapping
    public ResponseEntity<?> getAll(@QuerydslPredicate(root = Profile.class, bindings = ProfileRepository.class) Predicate predicate, Pageable pageable) 

        Page<Profile> profiles = profileRepo.findAll(predicate, pageable);
        return ResponseEntity.ok(profiles);
    

注意:不要忘记为你的项目添加 QueryDSL 依赖:

<dependency>
    <groupId>com.querydsl</groupId>
    <artifactId>querydsl-jpa</artifactId>
</dependency>

<dependency>
    <groupId>com.querydsl</groupId>
    <artifactId>querydsl-apt</artifactId>
    <scope>provided</scope>
</dependency>

<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>

然后编译您的项目(例如mvn compile),让它生成“Q”类。

【讨论】:

能否也给查询 dsl 的 gradle 依赖项? @JagratiGogia 不,但我希望这些帖子可以帮助你:discuss.gradle.org/t/…, ***.com/questions/22773639/… 小提示:在您的customize 方法中,两个返回语句都缺少Optional.of( @Graslandpinguin 是的,自 Spring Boot 2 以来已经更改(参见我的相关@​​987654326@)【参考方案3】:

答案很简单,你可以在春天使用query-by-example。

甚至你不需要在你的控制器中列出所有的Profile属性,你只需将Profile作为参数,spring会处理它。

由于你想验证请求参数,这里更容易与 bean 验证器集成,以“givenName”为例。在实体中添加NotNull,在控制器中添加@Valid,如果“givenName”不在请求参数中,您将得到“错误请求”响应。

以下是工作代码:

@Entity
@Table(name = "profiles")
public class Profile 
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "profile_given_name")
    @NotNull
    private String givenName;

    @Column(name = "profile_surname")
    private String surname;

    @Column(name = "profile_is_male")
    private Integer isMale;

    @Column(name = "profile_height_meters", columnDefinition = "DOUBLE")
    private BigDecimal heightMeters;

    @Column(name = "profile_weight_kilos", columnDefinition = "DOUBLE")
    private BigDecimal weightKilos;

    @Column(name = "profile_dob")
    private Date dob;

个人资料资源

@RestController
@RequestMapping("/v1/profiles")
public class ProfileResource 
    @Autowired
    ProfileRepository profileRepository;

    @GetMapping
    public ResponseEntity<List<Profile>> searchProfiles(@Valid Profile profile) 
        List<Profile> all = profileRepository.findAll(Example.of(profile));
        return ResponseEntity.ok(all);
    

ProfileRepository

public interface ProfileRepository extends JpaRepository<Profile, Long> 

然后根据需要发送GET /v1/profiles?isMale=0 HTTP 方法。

【讨论】:

感谢@Liping (+1) - 如果您不介意的话,还有几个后续问题:(1) 这在任何地方都有记录吗? (2) 这是一个 GET 端点,它在控制器方法中接受一个参数...用户是否必须在请求实体/正文中发送任何内容? (3) 您的示例显示了isMale=0,支持的比较运算符有哪些不同类型?例如,我如何使用heightMeters &gt; 1.5 AND heightMeters &lt;= 2.0 搜索Profiles @smeeb for (1) ,是的,你可以参考链接。对于 (2) 我只是更新答案以添加 bean 验证器示例,您不需要使用 GET 方法列出所有 Profile 属性。对于(3)spring会将path vars转换为对应的Object,可以参考spring type convestion部分。 再次感谢@Liping!!!对于上面的#3,我只是查看了您的链接并搜索了“type conversion”,但没有出现任何结果。您是否有机会修改您的示例以向我展示如何使用1.5 &lt; heightMeters &lt;= 2.0 搜索Profiles(作为示例)?再次感谢! @smeeb 抱歉,对于 (3) 我的错误,您可以使用 bean 验证器来实现它,在您的实体中添加 @Range(min=, max=) @smeeb 这是 bean 验证器的一个很好的例子 - baeldung.com/javax-validation【参考方案4】:

查看 spring 数据中的“示例查询”。似乎符合您的需求...

https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#query-by-example

【讨论】:

以上是关于Spring Boot 和 JPA:使用可选的范围条件实现搜索查询的主要内容,如果未能解决你的问题,请参考以下文章

带有查询 Spring-Boot jpa 1.5 的可选参数

Spring Boot事务管理(下)

JPA 继承 @EntityGraph 包括可选的子类关联

如何使用 BooleanBuilder (QueryDSL) 为可选的 OnetoOne JPA/Hibernate 关系建模谓词?

您将如何使用 Spring Boot 处理仅具有可选查询参数的 REST API?

具有任意 AND 子句的动态 spring 数据 jpa 存储库查询