Data JPA and JpaSpecificationExecutor

(Spring Boot)

Introduction

Flexible and clean searching solution in Spring Boot.

Reference

https://www.logicbig.com/tutorials/spring-framework/spring-data/specifications.html

https://reflectoring.io/spring-data-specifications/

https://docs.jboss.org/hibernate/orm/current/topical/html_single/metamodelgen/MetamodelGenerator.html

https://javadoc.io/doc/org.eclipse.persistence/javax.persistence/latest/javax/persistence/criteria/CriteriaBuilder.html


Specification interface

Spring Data provides Specification interface which can be used to execute JPA criteria queries:

package org.springframework.data.jpa.domain;

 ....

public interface Specification<T> extends Serializable {

//Negates the given spec

static <T> Specification<T> not(Specification<T> spec) { 

return Specifications.negated(spec);

}

//Applies Where clause the give spec

static <T> Specification<T> where(Specification<T> spec) {

return Specifications.where(spec);

}

//Applies AND condition to the give spec

default Specification<T> and(Specification<T> other) {

return Specifications.composed(this, other, AND);

}

//Applies OR condition to the give spec

default Specification<T> or(Specification<T> other) {

return Specifications.composed(this, other, OR);

}

//Creates a Predicate

@Nullable

Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, 

                                          CriteriaBuilder criteriaBuilder);

}


There's only one abstract method 'toPredicate()' which returns javax.persistence.criteria.Predicate. We only have to implement 'toPredicate()' method and return the Where Clause predicate, the rest of the stuff i.e. creating CriteriaBuilder, Root etc is automatically provided to us by Spring.

All other methods are helper methods to create composite Specifications.

JpaSpecificationExecutor interface

To use Specifications we also have to extend our repository interface with JpaSpecificationExecutor interface. This interface provides methods to execute Specifications. Here's this interface snippet:

package org.springframework.data.jpa.repository;

 ......

public interface JpaSpecificationExecutor<T> {

Optional<T> findOne(@Nullable Specification<T> spec);

List<T> findAll(@Nullable Specification<T> spec);

Page<T> findAll(@Nullable Specification<T> spec, Pageable pageable);

List<T> findAll(@Nullable Specification<T> spec, Sort sort);

long count(@Nullable Specification<T> spec);

}

pom.xml

Hibernate Static Metamodel Generator is an annotation processor based on JSR_269 with the task of creating JPA 2 static metamodel classes. Eg: For the entity class Employee, it creates the abstract class Employee_.

 As detailed in  the reference 'JPA Static Metamodel Generator', one way of configuring the generation of the metamodel is configuring the annotationProcessorPath of the maven-compiler-plugin, eg:

<plugin>

<groupId>org.apache.maven.plugins</groupId>

<artifactId>maven-compiler-plugin</artifactId>

<configuration>

<annotationProcessorPaths>

<path>

<groupId>org.projectlombok</groupId>

<artifactId>lombok</artifactId>

<version>${lombok.version}</version>

</path>

                        <-- jpamodelgen not available w/ sb3 (hibernate 6.1.5) -->

            <path>

                <groupId>org.hibernate</groupId>

                <artifactId>hibernate-jpamodelgen</artifactId>

                <version>${hibernate.version}</version>

            </path>

</annotationProcessorPaths>

<encoding>${project.build.sourceEncoding}</encoding>

             <parameters>true</parameters>

</configuration>

<dependencies>

<!-- Java bytecode manipulation framework -->

<dependency>

<groupId>org.ow2.asm</groupId>

<artifactId>asm</artifactId>

<version>9.1</version>

</dependency>

</dependencies>

</plugin>

Tips

Path of the attribute of a nested object

javax.persistence.criteria.Predicate filter1 = criteriaBuilder.equal(root.get(MyEntity_.id).get(MyEntityId_.attrA), objectA);


Sample entities

@Entity

public class Employee {

  @Id

  @GeneratedValue

  private long id;

  private String nickname;

  private String firstName;

  private String lastName;

  @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)

  private List<Phone> phones;

}


@Entity

public class Phone {

  @Id

  @GeneratedValue

  private long id;

  private PhoneType type;

  private String number;

}


public enum PhoneType {

  Home,

  Cell,

  Work

}


Sample repository

It extends JpaSpecificationExecutor:

public interface EmployeeRepository extends JpaRepository<Employee, Long>, JpaSpecificationExecutor<Employee> {

}

Sample specifications

public final class EmployeeSpecs {

    //Employees by nickname

    public static Specification<Employee> getEmployeesByNicknameSpec(String nickname) {

        return (root, query, criteriaBuilder) -> {

            return criteriaBuilder.equal(root.get(Employee_.nickname), nickname);

        };

    }

    //Employees by firstName

    public static Specification<Employee> byFirstNameSpec(String firstName) {

        return (root, query, criteriaBuilder) 

           -> criteriaBuilder.equal(root.get(Employee_.firstName), firstName);

    }


    //Employees by lastName

    public static Specification<Employee> byLastNameSpec(String lastName) {

        return (root, query, criteriaBuilder) 

           -> criteriaBuilder.equal(root.get(Employee_.lastName), lastName);

    }


    //Employees by phone type (join)

    public static Specification<Employee> getEmployeesByPhoneTypeSpec(PhoneType phoneType) {

        return (root, query, criteriaBuilder) -> {

            ListJoin<Employee, Phone> phoneJoin = root.join(Employee_.phones);

            return criteriaBuilder.equal(phoneJoin.get(Phone_.type), phoneType);

        };

    }



    //Employees by first or last name (would be possible to chain the ".equal" with ".and" or ".notNull")

    public static Specification<Employee> getEmployeesByFirstNameOrLastName(String firstName, String lastName) {

        return (root, query, criteriaBuilder) -> {

           return criteriaBuilder.or(

               criteriaBuilder.equal(root.get(Employee_.firstName), firstName),

               criteriaBuilder.equal(root.get(Employee_.lastName), lastName)

           );

        };

    }


    //Employees by nickname like %nick%

    public static Specification<Employee> getEmployeesByNicknameLike(String nick) {

        return (root, query, criteriaBuilder) -> return criteriaBuilder.like(root.get(Employee_.nickname), "%"+nick+"%");

    }


    private Specification<Product> belongsToCategory(List<Category> categories){

      return (root, query, criteriaBuilder)-> 

          criteriaBuilder.in(root.get(Product_.CATEGORY)).value(categories);

    }


    private Specification<Product> isPremium() {

      return (root, query, criteriaBuilder) ->

          criteriaBuilder.and(

              criteriaBuilder.equal(

                  root.get(Product_.MANUFACTURING_PLACE).get(Address_.STATE), STATE.CALIFORNIA),

              criteriaBuilder.greaterThanOrEqualTo(

                  root.get(Product_.PRICE), PREMIUM_PRICE));

    }


}

Sample service

@Service

public class ExampleService {


  @Inject

  private EmployeeRepository repo;



  public List<Employee> findEmployeesByName() {

      System.out.println("-- finding employees with nickname Tim --");

      //calling JpaSpecificationExecutor#findAll(Specification)

      List<Employee> list = repo.findAll(EmployeeSpecs.getEmployeesByNicknameSpec("Tim"));

      return list;

  }


  public List<Employee> findEmployeesByPhoneType() {

      System.out.println("-- finding employees by phone type Cell --");

      //calling JpaSpecificationExecutor#findAll(Specification)

      List<Employee> list = repo.findAll(EmployeeSpecs.getEmployeesByPhoneTypeSpec(PhoneType.Cell));

      return list;

  }


  //where() allows to combine multiple specifications

  public List<Product> getPremiumProducts(String name, List<Category> categories) {

     return productRepository.findAll( //

        where(belongsToCategory(categories)) //

          .and(nameLike(name)) //

          .and(isPremium()) //

        );

   }


  /* DOC. null values are filtered out, so if we return null instead of our actual

   * specification, we can properly filter these depending on the input values. This

   * means that if lastName is null, we won’t filter by byLastNameSpec().

   */

  public List<Employee> getByOptCriteria(String fn, @Nullable String ln) {

    Specification<Employee> spec = Specification //

        .where(EmployeeSpecs.byFirstNameSpec(fn))//

        .and(lastName== null ? null

            : EmployeeSpecs.byLastNameSpec(ln))//

        ;

    return employeeRepo.findAll(spec);

   }


}