javacyclic-referencemapstruct

Prevent Cyclic references when converting with MapStruct


Today I started using MapStruct to create my Model to DTO converters for my project and i was wondering if it handled cyclic references automatically but it turned out it doesn't.

This is the converter i made to test it:

package it.cdc.snp.services.rest.giudizio;

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;
import org.springframework.stereotype.Component;

import it.cdc.snp.dto.entita.Avvisinotifica;
import it.cdc.snp.dto.entita.Corrispondenza;
import it.cdc.snp.model.notifica.AvvisoDiNotificaModel;
import it.cdc.snp.model.notifica.NotificaModel;
import it.cdc.snp.model.procedimento.ProcedimentoModel;

@Component
@Mapper(componentModel="spring")
public interface NotificaMapper {

    NotificaMapper INSTANCE = Mappers.getMapper( NotificaMapper.class );

    @Mappings({
        @Mapping(source = "avvisinotificas", target = "avvisinotificas"),
    })
    NotificaModel<ProcedimentoModel> corrispondenzaToNotificaModel(Corrispondenza notifica);

    @Mappings({
        @Mapping(source = "corrispondenza", target = "notifica"),
    })
    AvvisoDiNotificaModel avvisinotificaToAvvisoDiNotificaModel(Avvisinotifica avvisinotifica);


}

This is the test:

        Notifica sourceObject1 = new Notifica();
        sourceObject1.setId(new Long(1));
        Avvisinotifica sourceObject2 = new Avvisinotifica();
        sourceObject2.setId(new Long(11));
        List<Avvisinotifica> tests= new ArrayList<>();
        tests.add(sourceObject2);
        sourceObject1.setAvvisinotificas(tests);
        sourceObject2.setCorrispondenza(sourceObject1);

        NotificaModel destObject1 = new NotificaModel<>();
        Avvisinotifica destObject2 = new Avvisinotifica();

        NotificaModel converted = mapper.corrispondenzaToNotificaModel(sourceObject1);

Notifica, Avvisinotifica and their respective models are simple POJOs with setters and getters so i don't think it's needed to post the code (Notifica extends Corrispondenza, if you were wondering)

this code gets into an infinite cycle, nothing very surprising here (though i hoped it'd handle these situations). And while i think i can find an elegant way to manually handle it (i was thinking about using methods with @MappingTarget to insert the Referenced objects ) what i was wondering is if there's some way to tell MapStruct how to automatically handle cyclic references.


Solution

  • Notifica and Avvisinotifica are not helping me understand your models. Thus lets say you have the above Child and Father models,

    public class Child {
        private int id;
        private Father father;
        // Empty constructor and getter/setter methods omitted.
    }
    
    public class Father {
        private int x;
        private List<Child> children;
        // Empty constructor and getter/setter methods omitted.
    }
    
    public class ChildDto {
        private int id;
        private FatherDto father;
        // Empty constructor and getter/setter methods omitted.
    }
    
    public class FatherDto {
        private int id;
        private List<ChildDto> children;
        // Empty constructor and getter/setter methods omitted.
    }  
    

    You should create a Mapper like this,

    @Mapper
    public abstract class ChildMapper {
    
        @AfterMapping
        protected void ignoreFathersChildren(Child child, @MappingTarget ChildDto childDto) {
            childDto.getFather().setChildren(null);
        }
    
        public abstract ChildDto myMethod(Child child);
    }
    

    Initial Version of Mapstuct

    It is better to follow the next ways. This solution assumes that the ChildDto::father property is of type Father, not FatherDto, which is not a correct data architecture.
    The @AfterMapping annotation means that the method will be imported inside the generated source, after the mapping of the properties. Thus, the Mapper implementation will be like this,

    @Component
    public class ChildMapperImpl extends ChildMapper {
    
        @Override
        public ChildDto myMethod(Child child) {
            if ( child == null ) {
                return null;
            }
    
            ChildDto childDto = new ChildDto();
    
            childDto.setId( child.getId() );
            childDto.setFather( child.getFather() );
    
            ignoreFathersChildren( child, childDto );
    
            return childDto;
        }
    }
    

    In this implementation the child has the parent set. This means that a cycle reference exists, but using the ignoreFathersChildren(child, childDto) method we remove the reference (we set it as null).

    Update 1

    Using the mapstruct version 1.2.0.Final you can do it better,

    @Mapper
    public interface ChildMapper {
    
        @Mappings({
    //         @Mapping(target = "father", expression = "java(null)"),
             @Mapping(target = "father", qualifiedByName = "fatherToFatherDto")})
        ChildDto childToChildDto(Child child);
    
        @Named("fatherToFatherDto")
        @Mappings({
             @Mapping(target = "children", expression = "java(null)")})
        FatherDto fatherToFatherDto(Father father);
    }
    

    Update 2

    Using the mapstruct version 1.4.2.Final you can do it even better,

    @Named("FatherMapper")
    @Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
    public interface FatherMapper {
    
        @Named("toDto")
        @Mappings
        FatherDto toDto(Father father);
    
        @Named("toDtoWithoutChildren")
        @Mappings({
             @Mapping(target = "children", expression = "java(null)")})
        FatherDto toDtoWithoutChildren(Father father);
    }
    
    @Named("ChildMapper")
    @Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE, uses = {FatherMapper.class})
    public interface ChildMapper {
    
        @Named("toDto")
        @Mappings({
             @Mapping(target = "father", qualifiedByName = {"FatherMapper", "toDtoWithoutChildren"})})
        ChildDto toDto(Child child);
    
        @Named("toDtoWithoutFather")
        @Mappings({
             @Mapping(target = "father", expression = "java(null)")})
        ChildDto toDtoWithoutFather(Child child);
    }