MapStruct and Hibernate/JPA Lazy Loading

Dominik Seichter
Cloud Workers
Published in
4 min readOct 19, 2022

Mapping from one Java object to another is a task regularly encountered by developers. E.g. at the interface between the business- and the persistence layer. Such mappings are well-supported by third-party libraries, but developers must consider some non-trivial details. In this article we explain, how we configure the mapping framework MapStruct to pass objects from the persistence to the business layer without loading the entire object hierarchy (i.e. Lazy loading in JPA terms).

— written with Jonas Ebel as co-author.

MapStruct is our new preferred mapping framework at Accenture to map between Java beans. Even alternative mapper frameworks like Dozer Mapper recommend MapStruct as the tool to use nowadays. MapStruct can be included as a compiler plugin in your build and will automatically generate mappers (during compile time, not during build time, as Dozer does) for bean types. An important case to consider, when using mapping frameworks together with Hibernate/JPA entities, is the case of lazy loading. Assume, the following class hierarchy:

UML Class hierarchy with Entity Beans and DTOs for a Person

We see a hierarchy of Hibernate/JPA entity beans, which are perhaps used in a persistence layer, and a hierarchy of DTOs with the same structure for a business layer. By default, child collections such as Address or BankAccount are supposed to be fetched “lazy” in Hibernate. “Eager” fetching would always cause large amounts of data to be read — especially in larger class hierarchies — and we want to avoid this. Only necessary data should be fetched from the database and there are often cases, where only the main entity is required (e.g. if one wants to check the object’s state).

When using a regular MapStruct mapper, the mapper would not only map the fields of PersonEntity to PersonDTO, but also access sub-collections such as addresses and bank accounts and map those as well. Accessing these sub-collections will trigger the Hibernate/JPA lazy loading mechanism and do a lazy load of these collections. This has the disadvantage to cause many separate SQL Select statements to load additional data of sub-collection. This is also known as “Hibernate N+1 Problem” (e.g., see https://hackernoon.com/3-ways-to-deal-with-hibernate-n1-problem).

There are two possible solutions to address this problem:

  1. Load all the required data beforehand using a JPA query, e.g. “Select e from PersonEntity e Left join fetch e.addresses Left join fetch e.bankAccounts where e.id = :id”. Now, the data will already be present in the entity and MapStruct can map it, without triggering lazy loading.
  2. We instruct MapStruct to skip lazy collections in Hibernate/JPA.

We want to follow the second approach in this article. A precondition to our approach is the 1.5.3 release, with which the MapStruct team has fixed an important bug (Issue 2937) related to the handling of conditions. Based on this fix, we can implement an interface to handle not-initialized collections in Hibernate/JPA:

public interface LazyLoadingAwareMapper {
default boolean isNotLazyLoaded(Collection<?> sourceCollection){
// Case: Source field in domain object is lazy: Skip mapping
if (Hibernate.isInitialized(sourceCollection)) {
// Continue Mapping
return true;
}

// Skip mapping
return false;
}
}

Now, we can implement this interface in our mapper:

@Mapper(uses = { AddressMapper.class, BankAccountMapper.class }, config = MapStructConfiguration.class)
public interface PersonMapper extends LazyLoadingAwareMapper {

PersonDTO toDTO(final PersonEntity entity);
PersonEntity toEntity(final PersonDTO dto);
@Condition
default boolean isNotLazyLoadedBankAccount(
Collection<BankAccount> sourceCollection) {
return isNotLazyLoaded(sourceCollection);
}
@Condition
default boolean isNotLazyLoadedAddress(
Collection<Address> sourceCollection) {
return isNotLazyLoaded(sourceCollection);
}
}

I.e., we have defined additional conditions for both collections where we want to avoid triggering lazy loading. The generated mapper for Person correctly implements the check:

@Override
public PersonDTO toDTO(PersonEntity entity) {
if ( entity == null ) {
return null;
}
PersonDTO personDTO = new PersonDTO();
personDTO.setFirstname( entity.getFirstname() );
personDTO.setLastname( entity.getLastname() );
personDTO.setBirthday( entity.getBirthday() );
if ( isNotLazyLoadedAddress( entity.getAddresses() ) ) {
for ( AddressEntity address : entity.getAddresses() ) {
personDTO.addAddress( addressMapper.toDTO( address ) );
}
}
if ( isNotLazyLoadedBankAccount( entity.getBankAccounts() ) ) {
for ( BankAccountEntity bankAccount :
entity.getBankAccounts() ) {
personDTO.addBankAccount(
bankAccountMapper.toDTO( bankAccount ) );
}
}
return personDTO;
}

Finally, we end up with a class hierarchy like this:

UML Class hierarchie with generated mapper classes.

One could think of a simplification, to put @Condition directly on the “isNotLazyLoaded” method in the parent interface, but this will lead to ambiguous method errors. That is why we explicitly define which collections bags to be handled lazily (and which you perhaps want to leave with the default behaviour):

[INFO] -------------------------------------------------------------
[ERROR] COMPILATION ERROR :
[INFO] -------------------------------------------------------------
[ERROR] PersonMapper.java:[11,27] Ambiguous presence check methods found for checking Set<AddressEntity>: boolean isNotLazyLoaded(Collection sourceCollection), boolean AddressMapper.isNotLazyLoaded(Collection sourceCollection), boolean BankAccountMapper.isNotLazyLoaded(Collection sourceCollection). See https://mapstruct.org/faq/#ambiguous for more info.
[ERROR] PersonMapper.java:[11,27] Ambiguous presence check methods found for checking Set<BankAccountDTO>: boolean isNotLazyLoaded(Collection sourceCollection), boolean AddressMapper.isNotLazyLoaded(Collection sourceCollection), boolean BankAccountMapper.isNotLazyLoaded(Collection sourceCollection). See https://mapstruct.org/faq/#ambiguous for more info.
[ERROR] PersonMapper.java:[12,24] Ambiguous presence check methods found for checking Set<AddressDTO>: boolean isNotLazyLoaded(Collection sourceCollection), boolean AddressMapper.isNotLazyLoaded(Collection sourceCollection), boolean BankAccountMapper.isNotLazyLoaded(Collection sourceCollection). See https://mapstruct.org/faq/#ambiguous for more info.
[ERROR] PersonMapper.java:[12,24] Ambiguous presence check methods found for checking Set<BankAccountDTO>: boolean isNotLazyLoaded(Collection sourceCollection), boolean AddressMapper.isNotLazyLoaded(Collection sourceCollection), boolean BankAccountMapper.isNotLazyLoaded(Collection sourceCollection). See https://mapstruct.org/faq/#ambiguous for more info.

In this post, we have shown how to instruct MapStruct to ignore lazy loaded collections from Hibernate/JPA. This can be achieved as shown above and be used with a DTO pattern as in our example, but also in other scenarios.

PS: We are aware, that there are other patterns, like the Active Record Pattern where mappings as described here are not required. Still, we encouter the scenario with entity beans and DTOs frequently and want to show how to handle this using MapStruct.

--

--

Dominik Seichter
Cloud Workers

Technology Architect, Full Stack Developer und OpenSource Enthusiast. Experte für OSS basierte Entwicklungs-Projekte.