Search code examples
javaspring-data-jpamapstructlazy-initialization

LazyInitializationException with Mapstruct because of cyclic issue


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?


Solution

  • 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