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?
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:
EAGER
fetch;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:
roles
from the DTO. Mapstruct will ignore the field in the entity because the DTO doesn't have a field with the same name;roles
;@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