Spring Data JPA Projections – 5 ways to return custom response

Spring-Data-JPA-Projections


In this article, we are going to see Projections in Spring Data JPA in detail. We will see what are different types of projections and what are use cases to use them.

Advertisements

Table of Contents

What is Projection?

If you already know about Spring Data JPA, you must be knowing that all the query methods of Repository classes will return an entity object. There may be cases where we do not want an entire entity from the query method. We may be interested only in few attributes of that entity or subset of that entity with some manipulation in it. In those cases, we will be using Projection which projects us only the required data out of entire entity class.

Broadly there are two types of projections, Interface-based Projections, and Class-based Projections. Let’s see them in detail.

Interface-based Projections

As the name implies we are going to use an interface here. In this type, we create an interface with only getter methods of properties we want from an entity class. This interface will be the return type of query method we write in Spring Data JPA’s Repository interface. It has the following three types:

Close Projection

In Close Projection, the getter methods of interface match exactly with the getter methods of Entity’s properties. For example, consider you have an entity of Employee table as follows:

@Entity
@Table(name = "employee")
public class Employee implements Serializable {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name = "id", unique = true, nullable = false)
	private Long id;

	@Column(name = "firstName", length = 50)
	private String firstName;

	@Column(name = "lastName", length = 50)
	private String lastName;

	@Column(name = "designation", length = 20)
	private String designation;

	@Column(name = "salary")
	private Integer salary;

	@ManyToOne(fetch = FetchType.LAZY)
	@JoinColumn(name = "departmentId", updatable = false, insertable = false)
	private Department department;

	@Column(name = "departmentId")
	private Long departmentId;

	public Long getId() {
		return id;
	}

	public String getFirstName() {
		return firstName;
	}

	public String getLastName() {
		return lastName;
	}

	public String getDesignation() {
		return designation;
	}

	public Integer getSalary() {
		return salary;
	}

	public Department getDepartment() {
		return department;
	}

	public Long getDepartmentId() {
		return departmentId;
	}
	
	//setters
}

Now if we are interested only in Id, First Name, Last Name and Department Name of Employee, we will create an Interface as follows:

public interface EmployeeByCloseProjectionRs {
    Long getId();

    String getFirstName();

    String getLastName();

    DepartmentRs getDepartment();

    interface DepartmentRs {
        String getName();
    }
}

To make use of it, use this interface as a return type of query method in your Repository interface:

@Repository
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
    List<EmployeeByCloseProjectionRs> findByFirstName(String firstName);
}

Open Projection

In Open Projection we will create an interface with getter methods of selective properties only but in addition to that, we also use SpEL expression. The SpEL expression will help us to define a new property from existing properties. Like in the above case if we want a property “fullName” then we can define it using SpEL expression and @Value  annotation. Likewise, we can also define a new property from nested property of entity class. In our case, we can define department name property from the hierarchy of Employee and Department entity.

public interface EmployeeByOpenProjectionRs {
    Long getId();

    @Value("#{target.firstName} #{target.lastName}")
    String getFullName();

    @Value("#{target.department.name}")
    String getDepartmentName();
}
@Repository
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
    List<EmployeeByOpenProjectionRs> findByLastName(String lastName);
}

If you don’t want to use SpEL expression, you can define default methods in the projection type interface to achieve the same thing.

Dynamic Projection

It is possible to define what projection type to return from the query method at runtime. To achieve this, just include a new argument to query method of type Class and set it with your projection type as follows:

@Repository
public interface EmployeeRepository extends JpaRepository<Employee, Long> {

    // dynamic projection can return EmployeeByOpenProjectionRs or EmployeeByCloseProjectionRs
    <T> List<T> findByFirstName(String firstName, Class<T> tClass);
}

From the Service class, invoke the repository method as follow:

@Override
public List<EmployeeByOpenProjectionRs> getAllByDynamicProjection() {
    return employeeRepository.findByFirstName("A", EmployeeByOpenProjectionRs.class);
}

Class-based Projections

In Interface-based Projection, we were creating interfaces with getter methods. If you were thinking about Classes in Class-based Projections, then you are absolutely correct. We create Projection class which contains only those properties we were interested in the Entity class and its class hierarchy. You can create the instance of your custom projection class using argument constructor or a Map.

Projection with argument constructor

In argument constructor way, we create the instance of projection class in the JPQL itself. Consider your projection class is as follows:

public class CustomEmployeeRs implements Serializable {

    private String firstName;
    private String lastName;
    private String departmentName;

    public CustomEmployeeRs(String firstName, String lastName, String departmentName) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.departmentName = departmentName;
    }
}

In JPA Repository we will use above constructor in JPQL to return its object as a response to repository method:

@Repository
public interface EmployeeRepository extends JpaRepository<Employee, Long> {

    @Query("select 
new com.bytestree.restful.dto.CustomEmployeeRs(e.firstName, e.lastName, e.department.name) " +
           "from Employee e")
    List<CustomEmployeeRs> findAllWithCustomObject();

}

Result in Map format

In some cases, our Projection class contains a large number of properties and we may want to use the same class for different requirements which requires different sets of properties. Creating a new constructor with a number of arguments for all such requirements may not be a good practice. In such situations, we add a constructor in our Projection class with a Map. The Map contains alias of selected property as a key(string) and its value. Though we use the same projection class, only interested properties will be set here.

public class CustomEmployeeRs implements Serializable {

    public static final String FIRST_NAME = "firstName";
    public static final String LAST_NAME = "lastName";
    public static final String DEPARTMENT_NAME = "departmentName";

    private String firstName;
    private String lastName;
    private String departmentName;

    public CustomEmployeeRs(Map<String, Object> values) {
	this.firstName = values.get(FIRST_NAME) != null ? (String) values.get(FIRST_NAME): null;
	this.lastName = values.get(LAST_NAME) != null ? (String) values.get(LAST_NAME) : null;
	this.departmentName = values.get(DEPARTMENT_NAME) != null ? (String) values.get(DEPARTMENT_NAME) : null;
   }
}

In JPQL of repository method, use the alias while selecting a property of Entity and define the List of Map as return type.

@Repository
public interface EmployeeRepository extends JpaRepository<Employee, Long> {

    @Query("select e.firstName as "+ CustomEmployeeRs.FIRST_NAME +"," +
            " e.lastName as " + CustomEmployeeRs.LAST_NAME + ", " +
            " d.name as " + CustomEmployeeRs.DEPARTMENT_NAME +
            " from Employee e join e.department d")
    List<Map<String, Object>> findAllWithMapResult();

}

The service method will get the projection class’s objects from the result of a repository method using a constructor with Map as follows:

    public List<CustomEmployeeRs> getAllWithMapResult() {
        List<Map<String, Object>> results = employeeRepository.findAllWithMapResult();
        return results.stream().map(result -> new CustomEmployeeRs(result))
		.collect(Collectors.toList());
    }

That’s all about Projections in Spring Data JPA.

Advertisements
Advertisements

Source Code

The source code of examples shown in this article is available on Github.