Spring Boot & JPA:使用可选的范围条件实施搜索查询
Spring Boot & JPA: Implementing search queries with optional, ranged criteria
这是一篇SSCCE,展示了研究成果,不是骗人的,而且切题!!!
Spring 在此处启动 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 端点,它提供了一种真正 flexible/robust 的方式来根据大量条件搜索 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
),我一直在研究如何在 ProfileRepository
:
中编写 JPQL 查询
public interface ProfileRepository extends CrudRepository<Profile,Long> {
@Query("???")
public Set<Profile> searchProfiles();
}
如何实现 @Query(...)
for ProfileRepository#searchProfiles
以启用我的所有搜索条件(给定所有允许的搜索范围和条件值) , 并允许任何条件为 null/optional?
当然,如果有任何漂亮的小库,或者如果 Spring Boot/JPA 已经有解决方案,我洗耳恭听!
查看 spring 数据中的“示例查询”。似乎符合您的需求...
https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#query-by-example
答案很简单,你可以使用spring中的query-by-example。
甚至您不需要在控制器中列出所有 Profile
属性,您只需将 Profile
作为参数,spring 会处理它。
并且因为你要验证请求参数,这里更容易与bean验证器集成,以"givenName"为例。在实体中添加 NotNull
,并在控制器中添加 @Valid
,如果 "givenName" 不在请求参数中,您将得到 "Bad Request" 响应。
以下是工作代码:
@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 方法。
您可以通过 spring 数据中的 JpaSpecificationExecutor
规范实现复杂查询。
存储库接口必须扩展 JpaSpecificationExecutor<T>
接口,以便我们可以通过创建新的 Specification<T>
对象来指定数据库查询的条件。
诀窍在于将规范接口与 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
。此接口定义了通过规范 class.
执行搜索的方法
我们现在要做的是定义我们的规范,该规范将 return Predicate
包含查询的约束(在示例中 PersonSpecification
正在执行查询 select * 来自名字 = ? 或(姓氏 = ? 和年龄 = ?)的人:
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
中的完整示例
您还可以使用规范创建任何复杂的查询
在 Querydsl and Web support Spring 数据扩展的帮助下,Spring 数据 几乎已经实现了您所需要的。
您还应该从 QuerydslPredicateExecutor
扩展您的存储库,如果您使用 Spring Data REST,您可以通过基本过滤、分页和排序支持直接 'from the box' 查询您的存储库数据:
/profiles?isMale=0&heightMeters=1.7&sort=dob,desc&size=10&page=2
要实现更复杂的过滤器,您应该从 QuerydslBinderCustomizer
扩展您的存储库并使用它的 customize
方法(就在您的存储库中)。
例如,您可以为 heightMeters
实施 'between' 过滤器,为 surname
实施 'like' 过滤器:
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 支持实现您自己的休息控制器方法:
@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'类.
这是一篇SSCCE,展示了研究成果,不是骗人的,而且切题!!!
Spring 在此处启动 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 端点,它提供了一种真正 flexible/robust 的方式来根据大量条件搜索 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
),我一直在研究如何在 ProfileRepository
:
public interface ProfileRepository extends CrudRepository<Profile,Long> {
@Query("???")
public Set<Profile> searchProfiles();
}
如何实现 @Query(...)
for ProfileRepository#searchProfiles
以启用我的所有搜索条件(给定所有允许的搜索范围和条件值) , 并允许任何条件为 null/optional?
当然,如果有任何漂亮的小库,或者如果 Spring Boot/JPA 已经有解决方案,我洗耳恭听!
查看 spring 数据中的“示例查询”。似乎符合您的需求...
https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#query-by-example
答案很简单,你可以使用spring中的query-by-example。
甚至您不需要在控制器中列出所有 Profile
属性,您只需将 Profile
作为参数,spring 会处理它。
并且因为你要验证请求参数,这里更容易与bean验证器集成,以"givenName"为例。在实体中添加 NotNull
,并在控制器中添加 @Valid
,如果 "givenName" 不在请求参数中,您将得到 "Bad Request" 响应。
以下是工作代码:
@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 方法。
您可以通过 spring 数据中的 JpaSpecificationExecutor
规范实现复杂查询。
存储库接口必须扩展 JpaSpecificationExecutor<T>
接口,以便我们可以通过创建新的 Specification<T>
对象来指定数据库查询的条件。
诀窍在于将规范接口与 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
。此接口定义了通过规范 class.
我们现在要做的是定义我们的规范,该规范将 return Predicate
包含查询的约束(在示例中 PersonSpecification
正在执行查询 select * 来自名字 = ? 或(姓氏 = ? 和年龄 = ?)的人:
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
中的完整示例您还可以使用规范创建任何复杂的查询
在 Querydsl and Web support Spring 数据扩展的帮助下,Spring 数据 几乎已经实现了您所需要的。
您还应该从 QuerydslPredicateExecutor
扩展您的存储库,如果您使用 Spring Data REST,您可以通过基本过滤、分页和排序支持直接 'from the box' 查询您的存储库数据:
/profiles?isMale=0&heightMeters=1.7&sort=dob,desc&size=10&page=2
要实现更复杂的过滤器,您应该从 QuerydslBinderCustomizer
扩展您的存储库并使用它的 customize
方法(就在您的存储库中)。
例如,您可以为 heightMeters
实施 'between' 过滤器,为 surname
实施 'like' 过滤器:
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 支持实现您自己的休息控制器方法:
@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'类.