Close

Spring Data JPA - Open Projections

[Updated: Aug 29, 2018, Created: Aug 29, 2018]

Closed Projections

In the previous two examples we saw how to use interface based projection where projection interface's getter methods matched properties of the target entity. Such projections are considered Closed Projection.

Open Projections

Open Projections are also interface based projection but the getter methods need not to be the same as entity properties. Such interface's methods use @Value annotation along with SpEL to defined the result to be returned. For example:

public interface EmployeeInfo{
  @Value("#{target.name + ' ' + target.salary}")
  String getEmployeeNameSalary();
}

In above snippet the 'target' variable represents the entity (Employee) instance.

Passing parameter

The methods of an Open projection interface can also use parameters. Such parameters can be accessed in SpEL via 'args' variable. For example:

public interface EmployeeInfo{
  @Value("#{args[0] +': ' +target.name + ' ' + target.salary}")
  String getEmployeeNameSalary(String label);
}

where arg[0] is assigned the 'label' value by Spring.

Example

Entities

@Entity
public class Employee {
  @Id
  @GeneratedValue
  private Integer id;
  private String name;
  @ManyToOne(cascade = CascadeType.ALL)
  private Department department;
  private int salary;
    .............
}
@Entity
public class Department {
  @Id
  @GeneratedValue
  private Integer id;
  private String deptName;
  private String location;
    .............
}

Open Projection interface

public interface EmployeeInfo {

  @Value("#{target.name+' ('+ target.department.deptName+' dept)'}")
  String getDisplayString();

  @Value("#{args[0]+': '+target.name+', '+args[1]+': '+ "
          + "target.department.deptName+', '+target.department.location}")
  String getDetailedString(String labelName, String labelDept);
}

Repository

public interface EmployeeRepository extends CrudRepository<Employee, Long> {
  List<EmployeeInfo> findBy();
}

Example Client

@Component
public class ExampleClient {

  @Autowired
  private EmployeeRepository repo;

  public void run() {
      List<Employee> employees = createEmployees();
      repo.saveAll(employees);

      System.out.println("-- finding all employees --");
      Iterable<Employee> all = repo.findAll();
      all.forEach(System.out::println);

      System.out.println("-- All EmployeeInfo --");
      List<EmployeeInfo> list = repo.findBy();
      System.out.println("-- display strings --");
      list.forEach(employeeInfo -> System.out.println(employeeInfo.getDisplayString()));
      System.out.println("-- detailed display strings --");
      list.forEach(employeeInfo -> {
          String str = employeeInfo.getDetailedString("Employee", "Department");
          System.out.println(str);
      });
  }

  private List<Employee> createEmployees() {
      return Arrays.asList(
              Employee.of("Diana", Department.of("Admin", "NY"), 3000),
              Employee.of("Mike", Department.of("IT", "TX"), 35000),
              Employee.of("Rose", Department.of("Sales", "NC"), 4000),
              Employee.of("Sara", Department.of("Admin", "TX"), 3500),
              Employee.of("Joe", Department.of("IT", "TX"), 3000),
              Employee.of("Charlie", Department.of("IT", "NY"), 4500)
      );
  }
}

Main class

public class ExampleMain {

  public static void main(String[] args) {
      AnnotationConfigApplicationContext context =
              new AnnotationConfigApplicationContext(AppConfig.class);
      ExampleClient exampleClient = context.getBean(ExampleClient.class);
      exampleClient.run();
      EntityManagerFactory emf = context.getBean(EntityManagerFactory.class);
      emf.close();
  }
}
-- finding all employees --
Employee{id=1, name='Diana', department=Department{id=2, deptName='Admin', location='NY'}}
Employee{id=3, name='Mike', department=Department{id=4, deptName='IT', location='TX'}}
Employee{id=5, name='Rose', department=Department{id=6, deptName='Sales', location='NC'}}
Employee{id=7, name='Sara', department=Department{id=8, deptName='Admin', location='TX'}}
Employee{id=9, name='Joe', department=Department{id=10, deptName='IT', location='TX'}}
Employee{id=11, name='Charlie', department=Department{id=12, deptName='IT', location='NY'}}
-- All EmployeeInfo --
-- display strings --
Diana (Admin dept)
Mike (IT dept)
Rose (Sales dept)
Sara (Admin dept)
Joe (IT dept)
Charlie (IT dept)
-- detailed display strings --
Employee: Diana, Department: Admin, NY
Employee: Mike, Department: IT, TX
Employee: Rose, Department: Sales, NC
Employee: Sara, Department: Admin, TX
Employee: Joe, Department: IT, TX
Employee: Charlie, Department: IT, NY

Closed vs Open projections and Query Optimization

The query execution engine creates proxy instances of projection interfaces at runtime. When we use a closed projection, Spring Data can optimize the query execution; that's because all the attributes that needed to be evaluated are known beforehand.

Spring Data cannot apply query execution optimizations in case of Open Projection, that's because the SpEL expression can use any attributes of the target entity. For this reason expression in @Value should not be too complex

Example Project

Dependencies and Technologies Used:

  • spring-data-jpa 2.0.9.RELEASE: Spring Data module for JPA repositories.
    Uses org.springframework:spring-context version 5.0.8.RELEASE
  • hibernate-core 5.3.5.Final: Hibernate's core ORM functionality.
    Implements javax.persistence:javax.persistence-api version 2.2
  • h2 1.4.197: H2 Database Engine.
  • JDK 1.8
  • Maven 3.5.4

Open Projections Example Select All Download
  • spring-data-jpa-open-projections
    • src
      • main
        • java
          • com
            • logicbig
              • example
                • EmployeeInfo.java
          • resources
            • META-INF

    See Also