Prevent Cyclic references when converting with MapStruct

valepu picture valepu · Mar 25, 2016 · Viewed 9.2k times · Source

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.

Answer

George Siggouroglou picture George Siggouroglou · Feb 14, 2017

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 ommitted.
}

public class Father {
    private int x;
    private List<Child> children;
    // Empty constructor and getter/setter methods ommitted.
}

public class ChildDto {
    private int id;
    private Father father;
    // Empty constructor and getter/setter methods ommitted.
}

public class FatherDto {
    private int id;
    private List<Child> children;
    // Empty constructor and getter/setter methods ommitted.
}  

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);
}

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

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);
}