Hibernate Generating Unnecessary Queries When Using Nested Closed JPA Projections

Viewed 727

I am running into a strange issue with JPA/Hibernate I hope I can get some help with. My environment:

  • OpenJDK 11.0.2 Spring Boot 2.2.2
  • spring-boot-starter-JPA (default version for above Spring Boot version)
  • Hibernate 5.4.9
  • Maria DB 2.3.0
  • Windows 10

I am using two Entities:

@Entity
@Table(name = "\"accUser\"") 
public class User implements Serializable {

    private static final long serialVersionUID = -5750077342980986498L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "UserID")
    private Long id;

    // NOTE: Below is an embedded object!
    @OneToOne
    @JoinColumn(name = "EmpNum")
    private Employee employee;

    //...other fields are wrapped primitives omitted for brevity
}

@Entity
@Table(name = "\"hrmEmployee\"")
public class Employee implements Serializable {
    private static final long serialVersionUID = 5471137977607643256L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "EmpNum")
    private Long employeeNumber;

    @Column(name = "FirstName")
    private String firstName;

    //...other fields are wrapped primitives omitted for brevity

}

I have the following (closed) Projection interfaces defined, each in their own class file:

@Projection(types={ User.class })
interface UserProjection {
    Long getId();
    EmployeeFirstNameProjection getEmployee();
}

@Projection(types={ Employee.class })
interface EmployeeFirstNameProjection {
    String getFirstName();
}

I am calling this Repository interface query method:

<T> T findUserById(Long id, Class <T> type);

And calling the above Repository method with this Service method:

public UserProjection getUser(Long id) {
    return userRepository.findUserById(id, UserProjection.class);
} 

So at runtime, here is the JSON of the single returned UserProjection:

{"employee":{"firstName":"Matt"},"id":1796}

This is exactly what I want to be returned. However, when the code executes, Hibernate is selecting all fields in both entities in two queries, which I don't want. The whole reason I am using the Projections mechanism is to limit wire traffic with the JSON, and also want to hopefully keep the number of queries low for performance reasons.

What I expect is to see a SINGLE Hibernate generated query.

Why is Hibernate running two respective queries that select every field in each of the entities?

Thanks in advance!

2 Answers

Hibernate and Spring-data are working as they should, when there is a non primitive field in the projection all the columns are included in the query.

It was already explained here https://stackoverflow.com/a/53783270/3449039 and a jira was opened at that moment https://jira.spring.io/browse/DATAJPA-1218 and closed with this answer "Currently, we don't optimize the query for referenced entities by only selecting the required properties".

As workaround you could create a JPQL query with a custom DTO or try something like Blaze-Persitence as was mentioned by @christian-beikov

This is a limitation of Spring-Data Projections and is a perfect use case for Blaze-Persistence Entity Views.

Blaze-Persitence is a query builder on top of JPA which supports many of the advanced DBMS features on top of the JPA model. I created Entity Views on top of it to allow easy mapping between JPA models and custom interface defined models, something like Spring Data Projections on steroids. The idea is that you define your target structure the way you like and map attributes(getters) via JPQL expressions to the entity model. Since the attribute name is used as default mapping, you mostly don't need explicit mappings as 80% of the use cases is to have DTOs that are a subset of the entity model.

A mapping for your model could look as simple as the following

@EntityView(User.class)
interface UserProjection {
    Long getId();
    EmployeeFirstNameProjection getEmployee();
}

@EntityView(Employee.class)
interface EmployeeFirstNameProjection {
    String getFirstName();
}

Querying is a matter of applying the entity view to a query, the simplest being just a query by id.

UserProjection dto = entityViewManager.find(entityManager, UserProjection.class, id);

But the Spring Data integration allows you to use it almost like Spring Data Projections: https://persistence.blazebit.com/documentation/entity-view/manual/en_US/index.html#spring-data-features

It will only fetch the mappings that you tell it to fetch

Related