Inspired by Legacy Hibernate Criteria Queries, while this should be considered deprecated vs JPA APIs,
but it still productive and easily understandable. Build on Spring Data JPA and simplify the dynamic query process.
- Compatible with Spring Data JPA and JPA 2.1 interface.
- Equal/NotEqual/Like/NotLike/In/NotIn support multiple values, Equal/NotEqual support Null value.
- Each specification support join query(left joiner).
- Support custom specification.
- Builder style specification creator.
- Support pagination and sort builder.
English Version:
Chinese Version:
repositories {
jcenter()
}
dependencies {
compile 'com.github.wenhao:jpa-spec:3.2.3'
}
<dependency>
<groupId>com.github.wenhao</groupId>
<artifactId>jpa-spec</artifactId>
<version>3.2.3</version>
</dependency>
- condition: if true(default), apply this specification.
- property: field name.
- values: compare value with model, eq/ne/like support multiple values.
each Repository class should extends from two super class JpaRepository and JpaSpecificationExecutor.
public interface PersonRepository extends JpaRepository<Person, Long>, JpaSpecificationExecutor<Person> {
}
public Page<Person> findAll(SearchRequest request) {
Specification<Person> specification = Specifications.<Person>and()
.eq(StringUtils.isNotBlank(request.getName()), "name", request.getName())
.gt(Objects.nonNull(request.getAge()), "age", 18)
.between("birthday", new Date(), new Date())
.like("nickName", "%og%", "%me")
.build();
return personRepository.findAll(specification, new PageRequest(0, 15));
}
find any person nickName equals to "dog" and name equals to "Jack"/"Eric" or null value, and company is null.
Test: EqualTest.java and NotEqualTest.java
public List<Person> findAll(SearchRequest request) {
Specification<Person> specification = Specifications.<Person>and()
.eq("nickName", "dog")
.eq(StringUtils.isNotBlank(request.getName()), "name", "Jack", "Eric", null)
.eq("company", null) //or eq("company", (Object) null)
.build();
return personRepository.findAll(specification);
}
find any person name in "Jack" or "Eric" and company not in "ThoughtWorks" or "IBM".
Test: InTest.java
public List<Person> findAll(SearchRequest request) {
Specification<Person> specification = Specifications.<Person>and()
.in("name", request.getNames().toArray()) //or in("name", "Jack", "Eric")
.notIn("company", "ThoughtWorks", "IBM")
.build();
return personRepository.findAll(specification);
}
Support any comparison class which implements Comparable interface, find any people age bigger than 18.
Test: GtTest.java
public List<Person> findAll(SearchRequest request) {
Specification<Person> specification = Specifications.<Person>and()
.gt(Objects.nonNull(request.getAge()), "age", 18)
.lt("birthday", new Date())
.build();
return personRepository.findAll(specification);
}
find any person age between 18 and 25, birthday between someday and someday.
Test: BetweenTest.java
public List<Person> findAll(SearchRequest request) {
Specification<Person> specification = Specifications.<Person>and()
.between(Objects.nonNull(request.getAge(), "age", 18, 25)
.between("birthday", new Date(), new Date())
.build();
return personRepository.findAll(specification);
}
find any person name like %ac% or %og%, company not like %ec%.
Test: LikeTest.java and NotLikeTest.java
public Page<Person> findAll(SearchRequest request) {
Specification<Person> specification = Specifications.<Person>and()
.like("name", "ac", "%og%")
.notLike("company", "ec")
.build();
return personRepository.findAll(specification);
}
support or specifications.
Test: OrTest.java
public List<Phone> findAll(SearchRequest request) {
Specification<Person> specification = Specifications.<Person>or()
.like("name", "%ac%")
.gt("age", 19)
.build();
return phoneRepository.findAll(specification);
}
support mixed and
and or
specifications.
Test: AndOrTest.java
public List<Person> findAll(SearchRequest request) {
Specification<Person> specification = Specifications.<Person>and()
.like("name", "%ac%")
.predicate(Specifications.or()
.lt("age", 19)
.gt("age", 25)
.build())
.build();
return personRepository.findAll(specification);
}
each specification support association query as left join.
Test: JoinTest.java
@ManyToOne association query, find person name equals to "Jack" and phone brand equals to "HuaWei".
public List<Phone> findAll(SearchRequest request) {
Specification<Phone> specification = Specifications.<Phone>and()
.eq(StringUtils.isNotBlank(request.getBrand()), "brand", "HuaWei")
.eq(StringUtils.isNotBlank(request.getPersonName()), "person.name", "Jack")
.build();
return phoneRepository.findAll(specification);
}
@ManyToMany association query, find person age between 10 and 35, live in "Chengdu" street.
public List<Phone> findAll(SearchRequest request) {
Specification<Person> specification = Specifications.<Person>and()
.between("age", 10, 35)
.eq(StringUtils.isNotBlank(jack.getName()), "addresses.street", "Chengdu")
.build();
return phoneRepository.findAll(specification);
}
You can custom specification to do the @ManyToOne and @ManyToMany as well.
@ManyToOne association query, find person name equals to "Jack" and phone brand equals to "HuaWei".
Test: AndTest.java
public List<Phone> findAll(SearchRequest request) {
Specification<Phone> specification = Specifications.<Phone>and()
.eq(StringUtils.isNotBlank(request.getBrand()), "brand", "HuaWei")
.predicate(StringUtils.isNotBlank(request.getPersonName()), (root, query, cb) -> {
Path<Person> person = root.get("person");
return cb.equal(person.get("name"), "Jack");
})
.build();
return phoneRepository.findAll(specification);
}
@ManyToMany association query, find person age between 10 and 35, live in "Chengdu" street.
Test: AndTest.java
public List<Phone> findAll(SearchRequest request) {
Specification<Person> specification = Specifications.<Person>and()
.between("age", 10, 35)
.predicate(StringUtils.isNotBlank(jack.getName()), ((root, query, cb) -> {
Join address = root.join("addresses", JoinType.LEFT);
return cb.equal(address.get("street"), "Chengdu");
}))
.build();
return phoneRepository.findAll(specification);
}
Test: SortTest.java
public List<Person> findAll(SearchRequest request) {
Specification<Person> specification = Specifications.<Person>and()
.eq(StringUtils.isNotBlank(request.getName()), "name", request.getName())
.gt("age", 18)
.between("birthday", new Date(), new Date())
.like("nickName", "%og%")
.build();
Sort sort = Sorts.builder()
.desc(StringUtils.isNotBlank(request.getName()), "name")
.asc("birthday")
.build();
return personRepository.findAll(specification, sort);
}
find person by pagination and sort by name desc and birthday asc.
public Page<Person> findAll(SearchRequest request) {
Specification<Person> specification = Specifications.<Person>and()
.eq(StringUtils.isNotBlank(request.getName()), "name", request.getName())
.gt("age", 18)
.between("birthday", new Date(), new Date())
.like("nickName", "%og%")
.build();
Sort sort = Sorts.builder()
.desc(StringUtils.isNotBlank(request.getName()), "name")
.asc("birthday")
.build();
return personRepository.findAll(specification, new PageRequest(0, 15, sort));
}
Using @org.hibernate.annotations.Subselect to define a virtual view if you don't want a database table view.
There is no difference between a view and a database table for a Hibernate mapping.
Test: VirtualViewTest.java
@Entity
@Immutable
@Subselect("SELECT p.id, p.name, p.age, ic.number " +
"FROM person p " +
"LEFT JOIN id_card ic " +
"ON p.id_card_id=ic.id")
public class PersonIdCard {
@Id
private Long id;
private String name;
private Integer age;
private String number;
// Getters and setters are omitted for brevity
}
public List<PersonIdCard> findAll(SearchRequest request) {
Specification<PersonIdCard> specification = Specifications.<PersonIdCard>and()
.gt(Objects.nonNull(request.getAge()), "age", 18)
.build();
return personIdCardRepository.findAll(specification);
}
Spring Data JPA doesn't support Projection(a little but trick), GroupBy and Aggregation,
furthermore, Projection/GroupBy/Aggregation are often used for complex statistics report, it might seem like overkill to use Hibernate/JPA ORM to solve it.
Alternatively, using virtual view and give a readable/significant class name to against your problem domain may be a better option.
Copyright ยฉ 2016-2018 Wen Hao
Licensed under Apache License