19

这是一个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);
    }
}

比如说,我的想法BigDecimalCriteria是:

// 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),所以我被困在如何在以下位置编写 JPQL 查询ProfileRepository

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

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

当然,如果有任何漂亮的小库或者 Spring Boot/JPA 已经有解决方案,我全都听好了!

4

4 回答 4

32

JpaSpecificationExecutor您可以通过在 spring data中实现具有规范的复杂查询。存储库接口必须扩展JpaSpecificationExecutor<T>接口,以便我们可以通过创建新Specification<T>对象来指定数据库查询的条件。

诀窍在于将 Specification 接口与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 = ? and age = ?) ):

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

是github中的完整示例

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

于 2018-02-02T06:22:32.090 回答
7

在QuerydslWeb 支持Spring Data 扩展的帮助下,几乎您所需要的已经在Spring Data中实现了。

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

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

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

例如,您可以为 'between' 过滤器heightMeters和 'like' 过滤器实现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”类。

于 2018-02-03T10:06:31.260 回答
4

答案非常简单,您可以在 spring中使用示例查询。

甚至更多你不需要列出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);
    }
}

配置文件库

public interface ProfileRepository extends JpaRepository<Profile, Long> {
}

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

于 2018-01-30T10:08:33.720 回答
3

查看 spring data 中的“query by example”。似乎符合您的需求...

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

于 2018-01-20T02:18:57.550 回答