'Understanding how spring-data handles @EntityGraph

(I made a SSCCE for this question.)

I have 2 simple entities : Employee and Company. Employee has a @ManyToOne relationship with Company with default fetch strategy (eager).

I want to be able to load the Employee without the Companywithout changing the fetch strategy defined in the Employee because I need to do that for only one use case.

JPA's entity graph seems to be intended for this purpose.

So I defined a @NamedEntityGraphon the class Employee:

@Entity
@NamedEntityGraph(name = "employeeOnly")
public class Employee {

  @Id
  private Integer id;
  private String name;
  private String surname;
  @ManyToOne
  private Company company;

  //Getters & Setters

And a EmployeeRepository like this :

public interface EmployeeRepository extends CrudRepository<Employee, Integer> {

  @EntityGraph(value = "employeeOnly", type = EntityGraph.EntityGraphType.FETCH)
  List<Employee> findByCompanyId(Integer companyId);

}

Despite the use of @EntityGraph, I can see in the logs that the Company is still loaded by hibernate :

2016-11-07 23:16:08.738 DEBUG 1029 --- [nio-8080-exec-2] org.hibernate.SQL                        : select employee0_.id as id1_1_, employee0_.company_id as company_4_1_, employee0_.name as name2_1_, employee0_.surname as surname3_1_ from employee employee0_ left outer join company company1_ on employee0_.company_id=company1_.id where company1_.id=?
2016-11-07 23:16:08.744 DEBUG 1029 --- [nio-8080-exec-2] org.hibernate.SQL                        : select company0_.id as id1_0_0_, company0_.name as name2_0_0_ from company company0_ where company0_.id=?

Why? How to avoid that?



Solution 1:[1]

Modified Answer

per-specification, the fetch type for @ManyToOne is EAGER by default. But even through we set:

@ManyToOne(fetch = FetchType.LAZY)
private Company company;

You will get the same result. The problem because the way spring-data-jpa create HQL/JPQL for you. So adding @ManyToOne(fetch = FetchType.LAZY) won't work is not enough. To solve this, use@ManyToOne(fetch = FetchType.LAZY) and @Query annotation in your repository:

Employee.java :

@ManyToOne(fetch = FetchType.LAZY)
private Company company;

EmployeeRepository.java

@Query("from Employee e where e.company.id = :companyId")
List<Employee> findByCompanyIdUsingQuery(@Param("companyId") Integer companyId);

In the test, this is SQL that generated by your loadByCompanyId() (which is generate left outer join):

select employee0_.id as id1_1_, employee0_.company_id as company_4_1_, employee0_.name as name2_1_, employee0_.surname as surname3_1_ from employee employee0_ left outer join company company1_ on employee0_.company_id=company1_.id where company1_.id=?

And this is SQL generated by method that use @Query annotation:

select employee0_.id as id1_1_, employee0_.company_id as company_4_1_, employee0_.name as name2_1_, employee0_.surname as surname3_1_ from employee employee0_ where employee0_.company_id=?

You could check the latest code in my repository.

HTH.

Solution 2:[2]

Seems a bug in Hibernate.

@Dragan Bozanovic you are right. I only see one workaround in this case.

Set fetch = lazy

@Entity
@NamedEntityGraph(name = "Employee.withCompany" , attributeNodes = @NamedAttributeNode("company"))
public class Employee {

  @Id
  private Integer id;
  private String name;
  private String surname;
  @ManyToOne(fetch = FetchType.LAZY)
  private Company company;

Introduce new method for loading company eagerly

public interface EmployeeRepository extends CrudRepository<Employee, Integer> {


  List<Employee> findByCompanyId(Integer companyId);

  @Query("select e from Employee e left join e.company c where c.id = :companyId")
  @EntityGraph(value = "Employee.withCompany", type = EntityGraph.EntityGraphType.FETCH)
  List<Employee> findByCompanyIdFetchingCompany(@Param("companyId") Integer companyId);

}

And use following two interchangeably where required

  @RequestMapping(value = "/by-company/{id}")
  public void loadByCompanyId(@PathVariable Integer id) {
    employeeService.loadByCompanyId(id);
  }

  @RequestMapping(value = "/by-company/eager-company/{id}")
  public void loadByCompanyIdFetchingCompany(@PathVariable Integer id) {
    employeeService.loadByCompanyIdFetchingCompany(id);
  }

First one (for lazy loading) http://localhost:8080/employees/by-company/42

select employee0_.id as id1_1_, employee0_.company_id as company_4_1_, employee0_.name as name2_1_, employee0_.surname as surname3_1_ from employee employee0_ left outer join company company1_ on employee0_.company_id=company1_.id where company1_.id=?

Second (eager loading) http://localhost:8080/employees/by-company/eager-company/42

select employee0_.id as id1_1_0_, company1_.id as id1_0_1_, employee0_.company_id as company_4_1_0_, employee0_.name as name2_1_0_, employee0_.surname as surname3_1_0_, company1_.name as name2_0_1_ from employee employee0_ left outer join company company1_ on employee0_.company_id=company1_.id where company1_.id=?

Solution 3:[3]

I was under impression you have to specify fields within your Graph definition.

https://docs.oracle.com/javaee/7/tutorial/persistence-entitygraphs001.htm (43.1.2.1 Fetch Graphs)

A fetch graph consists of only the fields explicitly specified in the EntityGraph instance, and ignores the default entity graph settings.

Solution 4:[4]

Maybe not so cool solution is using a native query.

 @Query(value = "select * from employee where company_id= ?1", nativeQuery = true)
  List<Employee> findByCompanyId(Integer companyId);

I tested it and it was giving expected results, without being forced to set fetch = FetchType.LAZY

Solution 5:[5]

though the association is lazy by specifying (fetch=FetchType.Lazy) the company is exposed with a separate query call because you are exposing the Entity Employee which has property company. If you don't want the company to be exposed then either use @JsonIgnore, which is not very much recommended. And here comes one of the reasons why we make used of DTO so that we can expose only the required fields. You can create an EmployeeDTO with employee specific fields keep the association lazy which will ensure that separate query is not executed to fetch company details and map the entity to DTO either explicitly or you can make use of MapSturct Api

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 slavik
Solution 3 Evgeny M
Solution 4 wmlynarski
Solution 5 unknown