Reputation: 1771
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
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:
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
Upvotes: 3