Simple Specs
You can use @Spec
on fields to define the implementation of the Specification
, Equals
spec is the default:
@Spec // Equivalent to @Spec(Equals.class)
String firstname;
The corresponding entity path will default to the field name, but you can also set @Spec#path
to change it:
@Spec(path = "...") // Takes precedence if defined
String firstname; // Defaults to the field name
Built-in @Spec
Here is a list of the built-in types for @Spec
:
Spec | Supported field type | Sample | JPQL snippet |
---|---|---|---|
Equals | Any | @Spec(Equals.class) String firstname; | ... where x.firstname = ? |
NotEquals | Any | @Spec(NotEquals.class) String firstname; | ... where x.firstname <> ? |
Between | Iterable of Comparable (Expected exact 2 elements in Iterable) | @Spec(Between.class) List<Integer> age; | ... where x.age between ? and ? |
LessThan | Comparable | @Spec(LessThan.class) Integer age; | ... where x.age < ? |
LessThanEqual | Comparable | @Spec(LessThanEqual.class) Integer age; | ... where x.age <= ? |
GreaterThan | Comparable | @Spec(GreaterThan.class) Integer age; | ... where x.age > ? |
GreaterThanEqual | Comparable | @Spec(GreaterThanEqual.class) Integer age; | ... where x.age >= ? |
After | Comparable | @Spec(After.class) LocalDate startDate; | ... where x.startDate > ? |
Before | Comparable | @Spec(Before.class) LocalDate startDate; | ... where x.startDate < ? |
IsNull | Boolean | @Spec(IsNull.class) Boolean age; | ... where x.age is null (if true)... where x.age not null (if false) |
NotNull | Boolean | @Spec(NotNull .class) Boolean age; | ... where x.age not null (if true)... where x.age is null (if false) |
Like | String | @Spec(Like.class) String firstname; | ... where x.firstname like %?% |
NotLike | String | @Spec(NotLike.class) String firstname; | ... where x.firstname not like %?% |
StartingWith | String | @Spec(StartingWith.class) String firstname; | ... where x.firstname like ?% |
EndingWith | String | @Spec(EndingWith.class) String firstname; | ... where x.firstname like %? |
In | Iterable of Any | @Spec(In.class) Set<String> firstname; | ... where x.firstname in (?, ?, ...) |
NotIn | Iterable of Any | @Spec(NotIn.class) Set<String> firstname; | ... where x.firstname not in (?, ?, ...) |
True | Boolean | @Spec(True.class) Boolean active; | ... where x.active = true (if true)... where x.active = false (if false) |
False | Boolean | @Spec(False.class) Boolean active; | ... where x.active = false (if true)... where x.active = true (if false) |
HasLength | Boolean | @Spec(HasLength.class) Boolean firstname; | ... where x.firstname is not null and character_length(x.firstname)>0 (if true)... where not(x.firstname is not null and character_length(x.firstname)>0) (if false) |
HasText | Boolean | @Spec(HasText.class) Boolean firstname; | ... where x.firstname is not null and character_length(trim(BOTH from x.firstname))>0 (if true)... where not(where x.firstname is not null and character_length(trim(BOTH from x.firstname))>0) (if false) |
In order to facilitate the usage for those who are already familiar with Spring Data JPA, the specs are named as closely as possible with Query Methods
Negates @Spec
You can use @Spec#not
to indicate the inverse condition. By default, it is set to false
, but if you set it to true
, the result will be inverted.
For example, if you want to use Between
to find data outside of a certain range, the example would be as follows:
@Spec(value = Between.class, not = true)
Collection<Integer> age;
The executed SQL will be like:
... where x.age not between ? and ?
Extending @Spec
@Spec
can be easily extended by implementing SimpleSpecification<T>
and providing the required constructor. These classes can then be used in @Spec
annotations.
For example, let’s say we have a Customer
entity with the following fields:
firstname
(String): Name of the person (can be duplicated)createdTime
(LocalDateTime): Creation time (unique)
We want to retrieve the data for each unique name with the latest creation time. To achieve this, we plan to write a subquery. The complete example is as follows:
First, we implement SimpleSpecification<T>
and provide the required constructor:
public class MaxCustomerCreatedTime extends SimpleSpecification<Customer> {
// This is the required constructor, the modifier can be public, protected, default, or private
protected MaxCustomerCreatedTime(Context context, String path, Object value) {
super(context, path, value);
}
@Override
public Predicate toPredicate(Root<Customer> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
// The following provides an example implementation for the subquery
var subquery = query.subquery(LocalDateTime.class);
var subroot = subquery.from(Customer.class);
subquery.select(builder.greatest(subroot.get("createdTime").as(LocalDateTime.class)))
.where(builder.equal(root.get((String) value), subroot.get((String) value)));
return builder.equal(root.get("createdTime"), subquery);
}
}
The MaxCustomerCreatedTime
class we implemented above can now be used in @Spec
. Next, we define the POJO and convert it to a Specification
:
@Data
public class CustomerCriteria {
@Spec(MaxCustomerCreatedTime.class)
String maxBy;
}
var criteria = new CustomerCriteria();
criteria.setMaxBy("firstname");
var spec = mapper.toSpec(criteria, Customer.class);
repository.findAll(spec);
The executed SQL will be like:
... where customer0_.created_time=(
select max(customer1_.created_time) from customer customer1_
where customer0_.firstname=customer1_.firstname
)