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