How DTO Projections can make your #Java #JPA app faster, better, stronger

How DTO Projections can make your #Java #JPA app faster, better, stronger

DTO Projections in JPA are one of the best ways to improve your app performance, ensure stability and keep some errors away

Standard JPQL Queries fetch entities. This means all basic attributes and FetchType.EAGER associations will be fetched. So as entities grow over time the generated SQL queries also will grow. A simple @Query like this:

  @Query("select e from Employee e where e.lastName = :lastName")
  List<User> findByLastname(String lastName);

will over time fetch more and more columns while seemingly staying the same. Additional columns will be fetched from the database which are probably not necessary in all cases. Selecting additional columns will require more CPU, RAM and network bandwidth slowing the whole system down.

Imagine having and Employee entity and after a while adding a column storing a photo of each employee, which now would be loaded as well with this @Query.

@Entity
@Table(name='EMPLOYEE')
public class Employee extends AbstractPersistable{

    private String firstName;

    private String lastName;

    private byte[] photo;

}

Here come Projections into play that can help you with that.

With a DTO like this (I'm using a Java Record, but you can use a regular Java class as well):

package com.example.demo.employee;

public record EmployeeBasicDataDTO(String firstName, String lastName) {}

you can use it in a Spring Data JPA Repository using a so-called JPQL Constructor Expression. That means you have to provide the package and the name of the class. Or more precisely: the constructor name must be fully qualified. Like this:

public interface EmployeeRepository extends JpaRepository<Employee, Long> {

    @Query("""
        SELECT new com.example.demo.employee.EmployeeBasicDataDTO(emp.firstName, emp.lastName) 
        FROM Employee emp
        """)
    List<EmployeeBasicDataDTO> findEmployeesBasicData();

}

Hibernate will now generate this SQL Query:

    select
        e1_0.first_name,
        e1_0.last_name
    from
        employee e1_0

Now no matter how many fields and relationships you add to your Entity the generated SQL Query will stay the same. Your DTO is not an Entity so it will not be tracked by Hibernate improving performance further. Also, you are safe from LazyInitializationException's because everything will be loaded in one SQL query right away.

You can also enrich your DTO by providing custom getters or manipulating data in the constructor. Sometimes it's easier to do data manipulation like concatenating an address in Java rather than in SQL.

public record EmployeeBasicDataDTO(String firstName, String lastName) {
    public String getFullName(){
        return this.firstName + " " + this.lastName;
    }
}

Summary

DTO Projections ...

  • can speed up your existing JPQL queries

  • stabilize query performance amid changes in Entities

  • are more memory-efficient because they don't need to be tracked for changes (dirty-checking)

  • save you from LazyInitializationException's

  • allow you to do data manipulation in Java Code