当前位置: 首页 > 知识库问答 >
问题:

使用MapSTRt转换时防止循环引用

管弘
2023-03-14

今天,我开始使用MapSTRt为我的项目创建我的模型到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);


}

这就是测试:

        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、AvvisionNotifica及其各自的模型都是带有setter和getter的简单POJO,因此我认为无需发布代码(如果您想知道,Notifica扩展了Corrispondenza)

这段html" target="_blank">代码进入了一个无限的循环,这里没有什么令人惊讶的(尽管我希望它能处理这些情况)。虽然我认为我可以找到一种优雅的方法来手动处理它(我在考虑使用带有@MappingTarget的方法来插入引用对象),但我想知道是否有某种方法可以告诉MapSTRt如何自动处理循环引用...

共有3个答案

刁茂才
2023-03-14

实际上,这种使用CycleAvoidingMappingContext的方法在使用MapStruct 1.3.1版时并不适用。由于找不到太多有效的例子,我决定在这里发布我的解决方案,供其他人查找。

在双向关系的情况下,这种映射可能会由于循环引用而触发StackOverflow错误。

例如:类配方,书籍和成分是双向相关的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;
    }

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

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

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

如果你在这里停下来,尝试以这种方式映射类,你会被StackOverflower错误击中,因为你现在已经定义了循环引用(Recipe包含具有属性Recipe的配料,Recipe包含配料…)。只有在没有双向关系也会触发反向映射时,才能使用这种默认映射器设置。

你可以把这个写下来

  • 向下钻取到前端中的关联对象:例如。显示食谱的成分列表
  • 保存对象时保持反比关系:ex.如果你只绘制一张-

将映射定义为-

@IterableMapping(qualifiedByName=”

@映射(目标="属性名称",合格的ByName="

@映射(目标 = "[.]", 忽略=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

配方中的book属性需要在没有recipes属性的情况下进行映射,因为其中一个属性也会循环回配方。

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

用法

<ENTITY_NAME>DTO <eNTITY_NAME>DTO = <ENTITY_NAME>Mapper.INSTANCE.toDTO( <eNTITY_NAME> );`
申屠裕
2023-03-14

至少在mapstruct1.3中,您可以使用以下内容:

这个解决方案受到了广泛的启发https://github.com/mapstruct/mapstruct-examples/tree/master/mapstruct-mapping-with-cycles/src/main/java/org/mapstruct/example/mapper

定义一个上下文类(受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 );
    }
}

在每个映射器中,使用循环引用映射类,添加这个org。mapstruct。上下文

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

用法(新增2021-09-21):

然后,可以使用以下命令调用mapper方法:

SomeObjWithCyclesMapper.INSTANCE.domainToEntity(objWithCycles, new CycleAvoidingMappingContext());

其中,objWithCycles是要映射的类SomeObjWithCycles的对象。

宗政小林
2023-03-14

Notifica和AvvisionNotifica没有帮助我理解你们的模型。因此,假设你有上述的孩子和父亲模型,

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

你应该创建一个这样的映射器,

@Mapper
public abstract class ChildMapper {

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

    public abstract ChildDto myMethod(Child child);
}

不如按照接下来的方式来。此解决方案假设ChildDto::father属性的类型为父亲,而不是FatherDto,这不是正确的数据体系结构。
@After映射注释意味着该方法将在映射属性后导入到生成的源中。因此,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;
    }
}

在这个实现中,孩子有父集。这意味着存在一个循环引用,但是使用忽略父子(子,子Dto)方法我们删除引用(我们将其设置为null)。

使用mapstruct版本1.2.0。最后你可以做得更好,

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

使用mapstruct 1.4.2版。最终你可以做得更好,

@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);
}
 类似资料:
  • 我有三块地 小时——分钟——和总数 如果小时或分钟发生变化,我想计算一个总数。如果总数发生变化,我想计算相应的分钟和小时数。 例子: 1h 30分钟=1.5总计 2.25总计=2h 15分钟 我正试图用手表来达到这个目的 但是,这会导致一个循环,因为on处理程序总是调用另一个处理程序。如何以更明智的方式实现这一点?

  • 问题内容: 我有一个带有循环引用的结构。出于调试目的,我想将其转储。基本上是任何格式,但我选择了JSON。 由于可以是任何类,因此我选择了不需要JAXB批注的GSON。 但是GSON击中了循环引用并递归直到。 如何将GSON限制为 忽略某些班级成员?两者和都不服从。 忽略某些对象图路径?例如,我可以指示GSON不要序列化。 最多达到2个级别的深度? 问题答案: 只需将字段设为瞬态(如中的)。GSO

  • 我有一个循环引用的结构。出于调试的目的,我想转储它。基本上可以是任何格式,但我选择了JSON。 由于它可以是任何类,我选择了不需要JAXB注释的GSON。 但是GSON会命中循环引用并递归,直到。 如何将 GSON 限制为 > 忽略某些类成员?不遵守和。 忽略某些对象图路径?例如,我可以指示GSON不要序列化< code > release . custom fields . product 。

  • 我搜索了Google、StackOverflow和Github问题,但没有找到任何内容 https://docs.temporal.io/docs/java-implementing-workflows#child-workflows 似乎我可以轻松地创建一个包含多个工作流的圆圈,将其他人称为子工作流。工作流甚至可以一遍又一遍地调用自己。节奏/时间是否提供任何东西来防止这种情况或超出范围? 提前感

  • 我想在文件目录上迭代此代码: 我目录中的图像: Film_Crew.jpg Film\u Crew副本。jpg公司 Film_Crew复制2.jpg Film\u Crew副本3。jpg公司 Film\u Crew副本4。jpg公司 Film\u Crew副本5。jpg公司 Film\u Crew副本6。jpg公司 我尝试使用此代码,因为上次尝试时它对我有效,但现在不起作用。 我在终端的Mac电脑

  • 问题内容: 我有一个包含循环引用的JavaScript对象定义:它具有一个引用父对象的属性。 它还具有一些我不想传递给服务器的功能。我将如何序列化和反序列化这些对象? 我读过,做到这一点的最好方法是使用道格拉斯·克罗克福德的stringify。但是,我在Chrome中遇到以下错误: TypeError:将圆形结构转换为JSON 编码: 这是我为这个问题创建的测试用例。这段代码中有错误,但是本质上我