This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

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:

    SpecSupported field typeSampleJPQL snippet
    EqualsAny@Spec(Equals.class) String firstname;... where x.firstname = ?
    NotEqualsAny@Spec(NotEquals.class) String firstname;... where x.firstname <> ?
    BetweenIterable of Comparable
    (Expected exact 2 elements in Iterable)
    @Spec(Between.class) List<Integer> age;... where x.age between ? and ?
    LessThanComparable@Spec(LessThan.class) Integer age;... where x.age < ?
    LessThanEqualComparable@Spec(LessThanEqual.class) Integer age;... where x.age <= ?
    GreaterThanComparable@Spec(GreaterThan.class) Integer age;... where x.age > ?
    GreaterThanEqualComparable@Spec(GreaterThanEqual.class) Integer age;... where x.age >= ?
    AfterComparable@Spec(After.class) LocalDate startDate;... where x.startDate > ?
    BeforeComparable@Spec(Before.class) LocalDate startDate;... where x.startDate < ?
    IsNullBoolean@Spec(IsNull.class) Boolean age;... where x.age is null (if true)
    ... where x.age not null (if false)
    NotNullBoolean@Spec(NotNull .class) Boolean age;... where x.age not null (if true)
    ... where x.age is null (if false)
    LikeString@Spec(Like.class) String firstname;... where x.firstname like %?%
    NotLikeString@Spec(NotLike.class) String firstname;... where x.firstname not like %?%
    StartingWithString@Spec(StartingWith.class) String firstname;... where x.firstname like ?%
    EndingWithString@Spec(EndingWith.class) String firstname;... where x.firstname like %?
    InIterable of Any@Spec(In.class) Set<String> firstname;... where x.firstname in (?, ?, ...)
    NotInIterable of Any@Spec(NotIn.class) Set<String> firstname;... where x.firstname not in (?, ?, ...)
    TrueBoolean@Spec(True.class) Boolean active;... where x.active = true (if true)
    ... where x.active = false (if false)
    FalseBoolean@Spec(False.class) Boolean active;... where x.active = false (if true)
    ... where x.active = true (if false)
    HasLengthBoolean@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)
    HasTextBoolean@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
    )