2

I have a development project using Spring Data JPA and MapStruct to map between Entities and DTOs. Last week I decided it was time to address the FetchType.EAGER vs LAZY issue I have postponed for some time. I choose to use @NamedEntityGraph and @EntityGraph to load properties when needed. However I am stuck with this LazyInitializationExeption problem when doing the mapping from entity to dto. I think I know where this happens but I do not know how to get passed it.

The code

@NamedEntityGraph(name="Employee.full", ...)
@Entity
public class Employee {
  private Set<Role> roles = new HashSet<>();
}

@Entity
public class Role {
  private Set<Employee> employees = new HashSet<>();
}

public interface EmployeeRepository extends JpaRepository<Employee, Long> {
  @EntityGraph(value = "Employee.full")
  @Override
  Page<Employee> findAll(Pageable pageable);
}

@Service
public class EmployeeService {
  public Page<EmployeeDTO> findAll(PageRequest pageRequest) {
    Page<Employee> employees = repository.findAll(pageRequest); // ok
    Page<EmployeeDTO> dtos = employees.map(emp -> mapper.toDTO(emp, new CycleAvoidMappingContext()); // this is where the exception happens
    return dtos;
  }
}

// also there is EmployeeDTO and RoleDTO classes mirroring the entity classes 
// and there is a simple interface EmployeeMapper loaded as a spring component 
// without any special mappings. However CycleAvoidingMappingContext is used.

I have tracked down the LazyInitializationException to happen when the mapper tries to map the roles dependency. The Role object do have Set<Employee> and therefore there is a cyclic reference.

When using FetchType.EAGER new CycleAvoidingMappingContext() solved this problem, but with LAZY this no longer works.

Does anybody know how I can avoid the exception and at the same time get my DTOs mapped correctly?

Avec
  • 1,626
  • 21
  • 31

1 Answers1

3

The problem is that when the code returns from findAll the entities are not managed anymore. So you have a LazyInitializationException because you are trying, outside of the scope of the session, to access a collection that hasn't been initialized already.

Adding eager make it works because it makes sure that the collection has been already initialized.

You have two alternatives:

  1. Using an EAGER fetch;
  2. Make sure that the entities are still managed when you return from the findAll. Adding a @Transactional to the method should work:
    @Service
    public class EmployeeService {
    
        @Transactional
        public Page<EmployeeDTO> findAll(PageRequest pageRequest) {
            Page<Employee> employees = repository.findAll(pageRequest);
            Page<EmployeeDTO> dtos = employees.map(emp -> mapper.toDTO(emp, new CycleAvoidMappingContext());
            return dtos;
        }
     }
    

I would say that if you need the collection initialized, fetching it eagerly (with an entity graph or a query) makes sense.

Check this article for more details on entities states in Hibernate ORM.

UPDATE: It seems that this error happens because Mapstruct is converting the collection even if you don't need it in the DTO. In this case, you have different options:

  1. Remove the field roles from the DTO. Mapstruct will ignore the field in the entity because the DTO doesn't have a field with the same name;
  2. Create a different DTO class for this specific case without the field roles;
  3. Use the @Mapping annotation to ignore the field in the entity:
    @Mapping(target = "roles", ignore = true)
    void toDTO(...)
    
    or, if you need the toDTO method sometimes
    @Mapping(target = "roles", ignore = true)
    void toSkipRolesDTO(...) // same signature as toDTO
    
Davide D'Alto
  • 7,421
  • 2
  • 16
  • 30
  • Thanks it worked. Adding @Transactional makes sense since I have to do the mapping after initial fetch. However I do not need to populate the Role bidirectional dependency so it could be skipped at the mapping level if I only knew how. That way the exception would never happen. Unless I receive a answer addressing the mapping that solves this issue I will credit your answer within the next couple of days. PS! The link is missing. – Avec Apr 12 '21 at 09:48
  • Link fixed. I don't think I understand your issue with the roles. It seems like you need them, otherwise you wouldn't have the exception. Maybe you are accessing the roles collection somewhere but didn't mean to? – Davide D'Alto Apr 12 '21 at 10:35
  • TO clarify, it seems more the case that something might be wrong in `CycleAvoidMappingContext` – Davide D'Alto Apr 12 '21 at 11:43
  • Maybe if you show the part of the code in it that throw the lazy initialization it can help. – Davide D'Alto Apr 12 '21 at 11:49
  • I do not need it (the call that failes). Mapstruct does the mapping with mapper.toDTO(...). The implementation is generated. I think this is more a question for somebody that knows Mapstruct. Your solution gives me an option to buypass the problem. The problem is this: 1) employee -> 2) has set -> 3) has set. When mapper.toDTO(...) is called number 3 is null. The cycle has to stop somewhere I guess and when the mapping starts the role.getEmployees() is called outside the transaction. I do not need this call since I already have the employee for hand. – Avec Apr 12 '21 at 12:04
  • CycleAvoidMappingContext was introdused when everything was EAGER to remedy the same cyclic problem I would think. Might not even need it anymore now when everything is LAZY or maybe I have to do it in another way. I do not know. The cyclic issue is still a factor that has to be solved at the mapping level. – Avec Apr 12 '21 at 12:07
  • I think this answer your question: https://stackoverflow.com/questions/42787031/mapstruct-ignore-specific-field-only-for-collection-mapping – Davide D'Alto Apr 12 '21 at 12:13