使用MapStruct进行转换时防止循环引用

val*_*epu 4 java cyclic-reference mapstruct

今天我开始使用MapStruct为我的项目创建我的Model to DTO转换器,我想知道它是否自动处理循环引用,但事实证明它没有.

这是我测试它的转换器:

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


}
Run Code Online (Sandbox Code Playgroud)

这是测试:

        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);
Run Code Online (Sandbox Code Playgroud)

Notifica,Avvisinotifica和他们各自的模型都是带有setter和getter的简单POJO,因此我认为不需要发布代码(Notifica扩展Corrispondenza,如果你想知道的话)

这段代码进入了一个无限循环,没有什么比这更令人惊讶的了(虽然我希望它能处理这些情况).虽然我认为我可以找到一种优雅的方式来手动处理它(我正在考虑使用方法@MappingTarget来插入Referenced对象)我想知道是否有一些方法告诉M​​apStruct如何自动处理循环引用.

小智 9

实际上,这种使用 CycleAvoidingMappingContext 的方法对使用 MapStruct 版本 1.3.1 的我来说不起作用。由于我找不到太多有效的示例,因此我决定将我的解决方案发布在这里以供其他人查找。

在双向关系的情况下,此类映射可能会由于循环引用而触发 StackOverflowError。

示例:类 Recipe、Book 和 Ingredient 是一对多和多对多双向相关的。

  • 一个食谱有很多成分,但只有一本书提到。
  • 一本书里有很多食谱。
  • 一种成分仅在 1 个配方中使用(假设一种成分还具有固定其数量、计量单位等的属性,因此它确实仅特定于一个配方)。
    public class Recipe {
        Long id;
        // ... Other recipe properties go here
        Book book;
        Set<Ingredient> ingredients;
    }
    
    public class Book {
        Long id;
        // ... Other book properties go here
        Set<Recipe> recipes;
    }
    
    public class Ingredient {
        Long id;
        // ... Other ingredient properties go here
        Recipe recipe;
    }
Run Code Online (Sandbox Code Playgroud)

我假设您也有具有相同属性的 DTO 类,但当然指的是它们相应的 DTO 类。

这些将是用于从实体类映射到 DTO 类的默认映射器设置(在本例中不依赖于 Spring):

// MapStruct can handle primitive and standard classes like String and Integer just fine, but if you are using custom complex objects it needs some instructions on how it should map these
    @Mapper(uses = {BookMapper.class, IngredientMapper.class})
    public interface RecipeMapper {
        RecipeMapper INSTANCE = Mappers.getMapper( RecipeMapper.class );

        RecipeDTO toDTO(Recipe recipe);

        Recipe toEntity(RecipeDTO recipeDTO);
    }

    @Mapper(uses = {RecipeMapper.class, IngredientMapper.class})
    public interface BookMapper {
        BookMapper INSTANCE = Mappers.getMapper( BookMapper.class );

        BookDTO toDTO(Book book);

        Book toEntity(BookDTO book);
    }

    @Mapper(uses = {RecipeMapper.class, BookMapper.class})
    public interface IngredientMapper {
        IngredientMapper INSTANCE = Mappers.getMapper( IngredientMapper.class );

        IngredientDTO toDTO(Ingredient ingredient);

        Ingredient toEntity(IngredientDTO ingredientDTO);
    }
Run Code Online (Sandbox Code Playgroud)

如果您停在那里并尝试以这种方式映射类,您将因您现在定义的循环引用而受到 StackOverflowError 的影响(配方包含的成分具有属性配方,而配方包含成分...)。仅当不存在也会触发逆映射的双向关系时,才能使用此类默认映射器设置。

您可以将其写下来,如 A -> B -> A -> B -> A ... 关于对象映射,我的经验表明您应该能够将其映射为: A -> B -> A (实体到 DTO 和 DTO 到实体映射的关系(不包括这次打破循环的关系)。这使您能够:

  • 深入了解前端中的关联对象:例如。显示食谱的成分列表
  • 保存对象时保留逆关系:例如。如果您只映射 A -> B。RecipeDTO 中的 IngredientDTO 不会有配方属性,并且在保存成分时,您需要传入配方 id 作为参数,并跳过一些步骤将成分实体对象与在将成分实体保存到数据库之前,配方实体对象。

定义像 A -> B -> A 这样的映射(排除这次打破循环的关系)将归结为定义单独的映射,以便当您想要从要打破的点的映射中排除相关的复杂对象时循环。

@IterableMapping(qualifiedByName = "<MAPPING_NAME>") 用于映射复杂对象的集合,指的是针对单个复杂对象的映射。

@Mapping(target = "PropertyName", QualifiedByName = "<MAPPING_NAME>") 可用于指向替代映射,该映射在映射复杂对象集合时(当您想要打破循环时)排除逆向关系

@Mapping(target = "[.]",ignore = true) 可用于指示根本不应映射对象的属性。因此,这可以用来完全忽略复杂对象(的集合),或者直接忽略单个(不是集合)相关复杂对象内部的属性,以防不需要它们。

如果您不使用QualifiedByName属性和匹配的@Named()注释,并且在 Mapper 接口中有多个具有相同返回类型和输入参数类型的方法,则您的映射将不会在编译时出现有关模糊映射的错误。

如果您使用命名映射,最好使用与 @Named 注释值匹配的方法名称。

因此,我们首先记下想要的行为,然后对其进行编码:

1. When mapping a Recipe, we will need to map the book property in such a way that its inverse relation to recipes is mapped without the book property the second time
    Recipe A -> Book X  -> Recipe A (without book property value as this would close the cycle)
        -> Recipe B (without book property value, as same mapping is used for all these recipes unfortunately as we don't know up front which one will cause the cyclic reference)...
            -> Ingredients I (without recipe property value as they would all point back to A)
                             
2. When mapping a Book, we will need to map the recipes property in such a way that its inverse relation to book isn't mapped as it will point back to the same book.
        Book X -> Recipe A (without book property as this would close the cycle)
                    -> Ingredients (without recipe property as all these will point back to Recipe A)
                        -> Recipe B (without book property, as same mapping is used for all these and all could potentially close the cycle)
                        -> Recipe C
                
3. When mapping an Ingredient, we will need to map the recipe property in such a way that its inverse relation to ingredient isn't mapped as one of those ingredients will point back to the same ingredient
Run Code Online (Sandbox Code Playgroud)

食谱中的书籍属性需要在没有食谱属性的情况下进行映射,因为其中之一也会循环回食谱。

    @Mapper(uses = {BookMapper.class, IngredientMapper.class})
    public interface RecipeMapper {
        RecipeMapper INSTANCE = Mappers.getMapper( RecipeMapper.class );

        @Named("RecipeSetIgnoreBookAndIngredientChildRecipes")
        @IterableMapping(qualifiedByName = "RecipeIgnoreBookAndIngredientChildRecipes")
        Set<RecipeDTO> toDTOSetIgnoreBookAndIngredientChildRecipes(Set<Recipe> recipes);

        @Named("RecipeSetIgnoreIngredientsAndBookChildRecipe")
        @IterableMapping(qualifiedByName = "RecipeIgnoreIngredientsAndBookChildRecipe")
        Set<RecipeDTO> toDTOSetIgnoreIngredientsAndBookChildRecipe(Set<Recipe> recipes);
                                
        // In this mapping we will ignore the book property and the recipe property of the Ingredients to break the mapping cyclic references when we are mapping a book object
        // Don't forget to add the matching inverse mapping from DTO to Entity, this is basically just a copy with switch input parameter and return types
        @Named("RecipeIgnoreBookAndIngredientChildRecipes")
        @Mappings({
            @Mapping(target = "book", ignore = true),                                               // book is a single custom complex object (not a collection), so we can directly ignore its child properties from there
            @Mapping(target = "ingredients", qualifiedByName = "IngredientSetIgnoreRecipes"),       // ingredients is a collection of complex objects, so we can't directly ignore its child properties as in the end, a Mapper needs to be defined to Map a single POJO into another
        })
        RecipeDTO toDTOIgnoreBookAndIngredientChildRecipes(Recipe recipe);

        @Named("RecipeIgnoreIngredientsAndBookChildRecipe")
        @Mappings({
            @Mapping(target = "book.recipes", ignore = true),
            @Mapping(target = "ingredients", ignore = true),
        })
        RecipeDTO toDTOIgnoreIngredientsAndBookChildRecipe(Recipe recipe);

        // Don't forget to add the matching inverse mapping from DTO to Entity, this is basically just a copy with switch input parameter and return types
        @Mappings({
            @Mapping(target = "book.recipes", ignore = true),                                       // book is a single custom complex object (not a collection), so we can directly ignore its child properties from there
            @Mapping(target = "ingredients", qualifiedByName = "IngredientSetIgnoreRecipes"),       // ingredients is a collection of complex objects, so we can't directly ignore its child properties as in the end, a Mapper needs to be defined to Map a single POJO into another
        })
        RecipeDTO toDTO(Recipe recipe);
        
        @Named("RecipeSetIgnoreBookAndIngredientChildRecipes")
        @IterableMapping(qualifiedByName = "RecipeIgnoreBookAndIngredientChildRecipes")
        Set<Recipe> toEntitySetIgnoreBookAndIngredientChildRecipes(Set<RecipeDTO> recipeDTOs);
        
        @Named("RecipeSetIgnoreIngredientsAndBookChildRecipe")
        @IterableMapping(qualifiedByName = "RecipeIgnoreIngredientsAndBookChildRecipe")
        Set<Recipe> toEntitySetIgnoreIngredientsAndBookChildRecipe(Set<RecipeDTO> recipeDTOs);
        
        @Mappings({
            @Mapping(target = "book.recipes", ignore = true),                                       // book is a single custom complex object (not a collection), so we can directly ignore its child properties from there
            @Mapping(target = "ingredients", qualifiedByName = "IngredientSetIgnoreRecipes"),       // ingredients is a collection of complex objects, so we can't directly ignore its child properties as in the end, a Mapper needs to be defined to Map a single POJO into another
        })
        Recipe toEntity(RecipeDTO recipeDTO);
        
        @Named("RecipeIgnoreBookAndIngredientChildRecipes")
        @Mappings({
            @Mapping(target = "book", ignore = true),                                               // book is a single custom complex object (not a collection), so we can directly ignore its child properties from there
            @Mapping(target = "ingredients", qualifiedByName = "IngredientSetIgnoreRecipes"),       // ingredients is a collection of complex objects, so we can't directly ignore its child properties as in the end, a Mapper needs to be defined to Map a single POJO into another
        })
        Recipe toEntityIgnoreBookAndIngredientChildRecipes(RecipeDTO recipeDTO);
        
                                @Named("RecipeIgnoreIngredientsAndBookChildRecipe")
        @Mappings({
            @Mapping(target = "book.recipes", ignore = true),
            @Mapping(target = "ingredients", ignore = true),
        })
        Recipe toEntityIgnoreIngredientsAndBookChildRecipe(RecipeDTO recipeDTO);
        
    }



    @Mapper(uses = {RecipeMapper.class, IngredientMapper.class})
    public interface BookMapper {
        BookMapper INSTANCE = Mappers.getMapper( BookMapper.class );
        
        @Mappings({
            @Mapping(target = "recipes", qualifiedByName = "RecipeSetIgnoreBookAndIngredientChildRecipes"),
        })
        BookDTO toDTO(Book book);

        @Mappings({
            @Mapping(target = "recipes", qualifiedByName = "RecipeSetIgnoreBookAndIngredientChildRecipes"),
        })
        Book toEntity(BookDTO book);
    }



    @Mapper(uses = {RecipeMapper.class, BookMapper.class})
    public interface IngredientMapper {
        IngredientMapper INSTANCE = Mappers.getMapper( IngredientMapper.class );

        // Don't forget to add the matching inverse mapping from DTO to Entity, this is basically just a copy with switch input parameter and return types
        @Named("IngredientSetIgnoreRecipes")
        IterableMapping(qualifiedByName = "IngredientIgnoreRecipes")                                // Refer to the mapping for a single object in the collection
        Set<IngredientDTO> toDTOSetIgnoreRecipes(Set<Ingredient> ingredients);

        // Don't forget to add the matching inverse mapping from DTO to Entity, this is basically just a copy with switch input parameter and return types
        @Named("IngredientIgnoreRecipes")
        @Mappings({
            @Mapping(target = "recipes", ignore = true),                                            // ignore the recipes property entirely
        })
        IngredientDTO toDTOIgnoreRecipes(Ingredient ingredient);

        @Mappings({
            @Mapping(target = "recipes", qualifiedByName = "RecipeSetIgnoreIngredientsAndBookChildRecipe")
        })
        IngredientDTO toDTO(Ingredient ingredient);

        @Named("IngredientSetIgnoreRecipes")
        IterableMapping(qualifiedByName = "IngredientIgnoreRecipes")                                // Refer to the mapping for a single object in the collection
        Set<Ingredient> toEntitySetIgnoreRecipes(Set<IngredientDTO> ingredientDTOs);

        @Named("IngredientIgnoreRecipes")
        @Mappings({
            @Mapping(target = "recipes", ignore = true),
        })
        Ingredient toEntityIgnoreRecipes(IngredientDTO ingredientDTO);

        @Mappings({
            @Mapping(target = "recipes", qualifiedByName = "RecipeSetIgnoreIngredientsAndBookChildRecipe")
        })
        Ingredient toEntityIgnoreRecipes(IngredientDTO ingredientDTO);
    }
Run Code Online (Sandbox Code Playgroud)

用法

<ENTITY_NAME>DTO <eNTITY_NAME>DTO = <ENTITY_NAME>Mapper.INSTANCE.toDTO( <eNTITY_NAME> );`
Run Code Online (Sandbox Code Playgroud)


Geo*_*lou 8

Notifica和Avvisinotifica并没有帮助我理解你的模型.因此,假设您拥有上述Child和Father模型,

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.
}  
Run Code Online (Sandbox Code Playgroud)

你应该像这样创建一个Mapper,

@Mapper
public abstract class ChildMapper {

    @AfterMapping
    protected void ignoreFathersChildren(Child child, @MappingTarget ChildDto childDto) {
        childDto.getFather().setChildren(null);
    }

    public abstract ChildDto myMethod(Child child);
}
Run Code Online (Sandbox Code Playgroud)

所述@AfterMapping注释意味着,该方法将生成的源内被导入,该属性的映射之后.因此,Mapper实现将是这样的,

@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;
    }
}
Run Code Online (Sandbox Code Playgroud)

在此实现中,子项具有父集.这意味着存在循环引用,但是使用该ignoreFathersChildren(child, childDto)方法我们删除引用(我们将其设置为null).

===

更新

使用mapstruct版本1.2.0.Final你可以做得更好,

@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);
}
Run Code Online (Sandbox Code Playgroud)

  • 从MapStruct 1.2.0.Beta1(昨天发布)开始,另一种可能的方法是使用一个上下文参数来跟踪已映射的对象.您可以找到一个完整的示例,说明如何执行此操作[此处](https://github.com/mapstruct/mapstruct-examples/tree/master/mapstruct-mapping-with-cycles/src/main/java/org/mapstruct /例). (4认同)
  • `FatherDto` 不应该有一个 `List&lt;ChildDto&gt;` 而不是 `List&lt;Child&gt;` 吗? (2认同)

Gun*_*nar 5

MapStruct 中尚未检测或特殊处理此类情况,但有一个功能请求:#469。如果您对如何处理周期有任何想法,请在该问题上发表评论。


mic*_*eak 5

至少在 mapstruct 1.3 中,您可以使用以下内容:

该解决方案的灵感来自https://github.com/mapstruct/mapstruct-examples/tree/master/mapstruct-mapping-with-cycles/src/main/java/org/mapstruct/example/mapper

定义一个 Context 类(受到https://github.com/mapstruct/mapstruct-examples/blob/master/mapstruct-mapping-with-cycles/src/main/java/org/mapstruct/example/mapper/CycleAvoidingMappingContext.爪哇):

/**
 * 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 );
    }
}
Run Code Online (Sandbox Code Playgroud)

在使用循环引用映射类的每个映射器中,添加以下内容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);

}
Run Code Online (Sandbox Code Playgroud)

  • 末尾补充了用法 (2认同)