Avec
Avec

Reputation: 1771

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?

Upvotes: 2

Views: 3556

Answers (1)

Davide D&#39;Alto
Davide D&#39;Alto

Reputation: 8246

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
    

Upvotes: 3

Related Questions