Prevent Cyclic references when converting with MapStruct

最后都变了- 提交于 2020-05-27 04:32:08

问题


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.


回答1:


There is no detection or special handling of cases like this in MapStruct yet, but there is a feature request for it: #469. If you got any ideas how to deal with cycles, please drop a comment at that issue.




回答2:


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



回答3:


At least in mapstruct 1.3 you can use the following:

The solution is widely inspired by https://github.com/mapstruct/mapstruct-examples/tree/master/mapstruct-mapping-with-cycles/src/main/java/org/mapstruct/example/mapper

Define a Context class (widely inspired by https://github.com/mapstruct/mapstruct-examples/blob/master/mapstruct-mapping-with-cycles/src/main/java/org/mapstruct/example/mapper/CycleAvoidingMappingContext.java ):

/**
 * An implementation to track cycles in graphs to be used as {@link Context} parameter.
 *
 */
public class CycleAvoidingMappingContext {
    private Map<Object, Object> knownInstances = new IdentityHashMap<Object, Object>();

    /**
     * Gets an instance out of this context if it is already mapped.
     * 
     * @param source
     *        given source
     * @param targetType
     *        given target type.
     * @return Returns the resulting type.
     */
    @BeforeMapping
    public <T> T getMappedInstance(Object source, @TargetType Class<T> targetType) {
        return targetType.cast(knownInstances.get(source));
    }

    /**
     * Puts an instance into the cache, so that it can be remembered to avoid endless mapping.
     * 
     * @param source
     *        given source
     * @param target
     *        given target
     */
    @BeforeMapping
    public void storeMappedInstance(Object source, @MappingTarget Object target) {
        knownInstances.put( source, target );
    }
}

In each mapper, which maps classes with cyclic references, add this org.mapstruct.Context:

/**
 * Mapper. Automatically implemented by mapstruct.
 * 
 */
@Mapper
public interface SomeObjWithCyclesMapper {

    /**
     * instance.
     */
    SomeObjWithCyclesMapper INSTANCE = Mappers.getMapper(SomeObjWithCyclesMapper.class);

    /**
     * Mapper method to map entity to domain. Automatically implemented by mapstruct.
     * 
     * @param entity
     *        given entity.
     * @param context
     *        context to avoid cycles.
     * @return Returns the domain object.
     */
    SomeObjWithCycles entityToDomain(SomeObjWithCyclesEntity entity, @Context CycleAvoidingMappingContext context);

    /**
     * Mapper method to map domain object to entity. Automatically implemented by mapstruct.
     * 
     * @param domain
     *        given domain object.
     * @param context
     *        context to avoid cycles.
     * @return Returns the entity.
     */
    SomeObjWithCyclesEntity domainToEntity(SomeObjWithCycles domain, @Context CycleAvoidingMappingContext context);

}


来源:https://stackoverflow.com/questions/36223752/prevent-cyclic-references-when-converting-with-mapstruct

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!