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

Return to the regular view of this page.

Customize Specs

    Continuing from the example in the section Extending @Spec, let’s take it a step further. Now, we want to make the entity class configurable so that it can be used for entities other than Customer.

    To fulfill this requirement, we need to define additional parameters in the annotation. As a result, the Simple @Spec approach is no longer suitable. Instead, we need to define a new annotation. Here’s the complete code:

    First, we define the annotation:

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ ElementType.FIELD })
    public @interface MaxCreatedTime {
    
      Class<?> from();
    }
    

    Next, we need to implement the logic responsible for handling @MaxCreatedTime. We can extend it by implementing the SpecificationResolver interface:

    public class MaxCreatedTimeSpecificationResolver implements SpecificationResolver {
    
      @Override
      public boolean supports(Databind databind) { 
        // Here, we tell the SpecMapper when to use this resolver
        return databind.getField().isAnnotationPresent(MaxCreatedTime.class);
      }
    
      @Override
      public Specification<Object> buildSpecification(Context context, Databind databind) {
        var def = databind.getField().getAnnotation(MaxCreatedTime.class);
        return databind.getFieldValue()
            .map(value -> subquery(def.from(), value.toString()))
            .orElse(null);
      }
    
      Specification<Object> subquery(Class<?> entityClass, String by) {
        // Here, we provide an example implementation for the subquery
        return (root, query, builder) -> {
          var subquery = query.subquery(LocalDateTime.class);
          var subroot = subquery.from(entityClass);
          subquery.select(
            builder.greatest(subroot.get("createdTime").as(LocalDateTime.class))
          ).where(builder.equal(root.get(by), subroot.get(by)));
          return builder.equal(root.get("createdTime"), subquery);
        };
      }
    }
    

    Next, we add this resolver to the SpecMapper during its construction:

    var mapper = SpecMapper.builder()
          .defaultResolvers()
          .resolver(new MaxCreatedTimeSpecificationResolver())
          .build();
    

    Finally, we define the POJO and convert it to a Specification:

    @Data
    public class CustomerCriteria {
    
      @MaxCreatedTime(from = Customer.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
    )