Is there a way to specify a custom getter in a Mapstruct mapper for a single field?

Viewed 1853

I'm trying to find a better solution for preventing hibernate proxy initialization when mapping an entity to a response DTO via MapStruct.

I've been converting our codebase to use MapStruct from ModelMapper. If I wanted to accomplish what I'm asking with ModelMapper, I could do something simple like this:

    modelMapper
        .createTypeMap(Entity.class, DTO.class)
        .addMapping(Entity::customGetterMethod, Category::setNormalSetterHere);

That Custom getter method allows me to check if a field has already been fetched from the DB or not to avoid N+1 initializations.

it looks like:

 public Set<Entity> customGetterMethod() {
    return Hibernate.isInitialized(this.entities) ? this.entities : null;
  }

I cannot simply override the normal getter because there are reasonable cases when working with an entity that we would want to allow lazy initialization.

I've tried overriding the default naming strategy to use my own custom getter naming, but since I still need to access normal getters for most basic fields, I couldn't get it to reliably use my customGetter and ignore the default getter when it exists ( and even if I could, its a still seems like a messy solution and difficult to bring teammates up to speed with).

The solution so far has been to use an expression and just copy and modify the generated mapping code for these sorts of fields:

  @Mapping(
      target = "entities",
      expression = "java( mapEntities( source.customGetterMethod(), context ) )")
  public abstract ResponseDto toDto(Entity source, @Context CycleAvoidingContext context);

   protected Set<ResponseDto> mapEntities(Set<Entity> set, CycleAvoidingContext context) {
       /* a copy of the auto-generated code for this mapping essentially */
  }

With all the entities and relationships in our projects, this is hardly sustainable since I have to add in an annotated mapping with an expression string for each field along with custom (not custom, just copied) mapping logic. From a maintainability and documentation standpoint, it adds a lot of complexity.

I'm hoping someone can point me to some other mapstruct functionality to accomplish using a custom getter for particular fields in a more streamlined way.

1 Answers

You have 2 options that you could use right now for this, and one option that you can use in the future.

Custom Getter

Write a custom getter using the bean style approach. e.g.

public Set<Entity> getCustomEntities() {
    return Hibernate.isInitialized(this.entities) ? this.entities : null;
}

Then in your mapping you need to use

@Mapping(target = "entities", source = "customEntities")

Use a presence check

MapStruct has the concept of presence check. This allows you to write a custom boolean method that MapStruct will use to check if the property is present or not.

e.g.

public boolean hasEntities() {
    return Hibernate.isInitialized(this.entities);
}

Custom Condition Check

Starting from 1.5 MapStruct will offer a way to have custom (out of bean) conditional (presence check) methods.

e.g.

public MapStructHibernateUtils {


    @Condition
    public static <T> boolean isInitialized(Collection<T> collection) {
        return Hibernate.isInitialized(collection);
    }

}

And then in your mapper you will do

@Mapper(uses = MapStructHibernateUtils.class)
public interface CustomMapper {


    ResponseDto toDto(Entity source, @Context CycleAvoidingContext context);

}

There is no need to add a @Mapping because MapStruct will call the isInitialized on the collection returned by getEntities()

Related