java mapstruct 1.3.1 忽略双向 DTO 映射列表中的 属性

java mapstruct 1.3.1 ignore property in a list for bi-directional DTO mapping

我正在努力解决 MapStruct 的循环依赖问题。 由于循环依赖,我一直有 Whosebug 错误。 为了避免它,我只需要排除列表的 属性 。 我发现了这个:https://github.com/mapstruct/mapstruct/issues/933 我深入浏览了互联网,令我惊讶的是我找不到任何显示使用 MapStruct 进行双向 DTO 映射的完整示例(除了使用 @Context CycleAvoidingMappingContext,对我不起作用)。

[编辑]:感谢 MapStruct 聊天,我找到了解决方法,我将其添加到 EditorMapper

这是我的情况,我想这很常见: 我有 2 个相互引用的 DTO:

public class BookDTO {

    private Long id;

    private String title;

        //... other properties

    //@JsonManagedReference --> not necessary anymore
    private EditorDTO editor;
}
public class EditorDTO {

    private Long id;
    private String name;

        //...other properties

    //@JsonBackReference --> not necessary anymore
    private List< BookDTO > bookList;
}

并且我需要MapStruct能够在Editor中从BookList中排除属性Editor,从而避免死循环。 这是我目前拥有的映射器:

@Mapper
public interface BookMapper {

    BookMapper INSTANCE = Mappers.getMapper( BookMapper.class );

    @Mapping( target = "editor.bookList", ignore = true)
    BookDTO toDTO( BookEntity bookEntity );

    @Named( "NoEditor" )
    @Mapping(target = "editor", ignore = true)
    BookDTO toDTONoEditor( BookEntity bookEntity );

    List<BookDTO> toDTOList( List<BookEntity> bookEntityList );

    @Named( "NoEditor" )
    @IterableMapping(qualifiedByName="NoEditor")
    List<BookDTO> toDTOListNoEditor( List<BookEntity> bookEntityList );

    @Mapping( target = "editor.bookList", ignore = true)
    BookEntity toEntity( BookDTO bookDTO );

    List<BookEntity> toEntityList( List<BookDTO> bookDTOList );
}
@Mapper(uses = BookMapper.class)
public interface EditorMapper {

    EditorMapper INSTANCE = Mappers.getMapper( EditorMapper.class );

    @Named( "NoEditor" )
    @Mapping(target = "bookList", qualifiedByName = "NoEditor")
    EditorDTO toDTO( EditorEntity editorEntity );

    @Named( "NoEditor" )
    @IterableMapping(qualifiedByName="NoEditor")
    List<EditorDTO> toDTOList( List< EditorEntity > editorEntityList );

    EditorEntity toEntity( EditorDTO editorDTO );

    List<EditorEntity> toEntityList( List< EditorDTO > editorDTOList );
}

[编辑]:它现在可以工作,但不是 100% 干净(请参阅我发布的答案了解更多详情)

我在mappers中也尝试过这种方法,但对我的pb没有任何影响

BookDTO toDTO( BookEntity bookEntity, @Context CycleAvoidingMappingContext context );

有谁知道我做错了什么?多谢! :)

[编辑]:我也添加了双向 ManyToMany 映射的解决方案 感谢https://gitter.im/mapstruct/mapstruct-users#,我已经能够得到解决方案。 [编辑]:我仍然有我没有意识到的错误。现在已在此更新中更正。 我不得不 : - 将 uses 属性添加到 EditorMapper@Mapper(componentModel = "spring", uses = BookMapper.class) - 在我忽略 editor 属性 的 BookMapper 中添加替代方法,例如 toDTONoEditortoDTOListNoEditor。 - 在 EditorMapper 中映射这些替代方法 - 每个循环依赖都相同

解决方法如下:

BookDTO

public class BookDTO {

    private Long id;

    private String title;

        //... other properties

    private EditorDTO editor;
    private List< CategoryDTO > categoryList;
}

EditorDTO

public class EditorDTO {

    private Long id;
    private String name;

        //...other properties

    private List< BookDTO > bookList;
}

类别DTO

public class CategoryDTO {

    private Long id;

    private String category;

    private List< BookDTO > bookList;
}

BookMapper

@Mapper(componentModel = "spring", uses = {CategoryMapper.class, EditorMapper.class})
public interface BookMapper {


    @Named( "NoBook" )
    @Mappings( {
            @Mapping(target = "categoryList", qualifiedByName = "NoBook"),
            @Mapping( target = "editor.bookList", ignore = true)
    } )
    BookDTO toDTO( BookEntity bookEntity );

    @Named( "NoEditor" )
    @Mappings( {
            @Mapping(target = "editor", ignore = true),
            @Mapping(target = "categoryList", qualifiedByName = "NoBook")
    } )
    BookDTO toDTONoEditor( BookEntity bookEntity );

    @Named( "NoCategory" )
    @Mappings( {
            @Mapping(target = "categoryList", ignore = true),
            @Mapping(target = "editor", qualifiedByName = "NoBook")
    } )
    BookDTO toDTONoCategory( BookEntity bookEntity );


    @Named( "NoBook" )
    @IterableMapping(qualifiedByName="NoBook")
    List<BookDTO> toDTOList( List<BookEntity> bookEntityList );

    @Named( "NoEditor" )
    @IterableMapping(qualifiedByName="NoEditor")
    List<BookDTO> toDTOListNoEditor( List<BookEntity> bookEntityList );

    @Named( "NoCategory" )
    @IterableMapping(qualifiedByName="NoCategory")
    List<BookDTO> toDTOListNoCategory( List<BookEntity> bookEntityList );


    @Named( "NoBook" )
    @Mappings( {
            @Mapping(target = "categoryList", qualifiedByName = "NoBook"),
            @Mapping( target = "editor.bookList", ignore = true)
    } )
    BookEntity toEntity( BookDTO bookDTO );

    @Named( "NoCategory" )
    @Mapping(target = "categoryList", ignore = true)
    BookEntity toEntityNoCategory( BookDTO bookDTO );


    @Named( "NoBook" )
    @IterableMapping(qualifiedByName="NoBook")
    List<BookEntity> toEntityList( List<BookDTO> bookDTOList );

    @Named( "NoCategory" )
    @IterableMapping(qualifiedByName="NoCategory")
    List<BookEntity> toEntityListNoCategory( List<BookDTO> bookDTOList );
}

EditorMapper

@Mapper(componentModel = "spring", uses = BookMapper.class)
public interface EditorMapper {

    @Named( "NoEditor" )
    @Mapping(target = "bookList", qualifiedByName = "NoEditor")
    EditorDTO toDTO( EditorEntity editorEntity );

    @Named( "NoBook" )
    @Mapping(target = "bookList", ignore = true)
    EditorDTO toDTONoBook( EditorEntity editorEntity );


    @Named( "NoEditor" )
    @IterableMapping(qualifiedByName="NoEditor")
    List< EditorDTO > toDTOList( List< EditorEntity > editorEntityList );

    @Named( "NoBook" )
    @IterableMapping(qualifiedByName="NoBook")
    List< EditorDTO > toDTOListNoBook( List< EditorEntity > editorEntityList );

    @Named( "NoBook" )
    @Mapping(target = "bookList", qualifiedByName = "NoBook")
    EditorEntity toEntity( EditorDTO editorDTO );

    @Named( "NoBook" )
    @IterableMapping(qualifiedByName="NoBook")
    List< EditorEntity > toEntityList( List< EditorDTO > editorDTOList );
}

CategoryMapper

@Mapper(componentModel = "spring",uses = BookMapper.class)
public interface CategoryMapper {


    @Named( "NoCategory" )
    @Mapping(target = "bookList", qualifiedByName = "NoCategory")
    CategoryDTO toDTO( CategoryEntity categoryEntity );

    @Named( "NoBook" )
    @Mapping(target = "bookList", ignore = true)
    CategoryDTO toDTONoBook( CategoryEntity categoryEntity );


    @Named( "NoCategory" )
    @IterableMapping(qualifiedByName="NoCategory")
    List<CategoryDTO> toDTOList( List< CategoryEntity > categoryEntityList );

    @Named( "NoBook" )
    @IterableMapping(qualifiedByName="NoBook")
    List<CategoryDTO> toDTOListNoBook( List< CategoryEntity > categoryEntityList );


    @Named( "NoCategory" )
    @Mapping(target = "bookList", qualifiedByName = "NoCategory")
    CategoryEntity toEntity( CategoryDTO categoryDTO );

    @Named( "NoBook" )
    @Mapping(target = "bookList", ignore = true)
    CategoryEntity toEntityNoBook( CategoryDTO categoryDTO );


    @Named( "NoCategory" )
    @IterableMapping(qualifiedByName="NoCategory")
    List<CategoryEntity> toEntityList( List< CategoryDTO > categoryDTOList );

    @Named( "NoBook" )
    @IterableMapping(qualifiedByName="NoBook")
    List<CategoryEntity> toEntityListNoBook( List< CategoryDTO > categoryDTOList );

}

这样,循环依赖在进入无限循环之前就被打破了。 然而,它是 99% 令人满意的,因为 EditorBook 对象并不完全干净。 Editor 包含 bookList,嗯。但是 bookList 中的每本书仍然包含一个空的 editor 字段。 Book 对象反之亦然。 但这似乎是一个 De/Serialization 问题,而不是 MapStruct 问题。 这是 Json 结果

编辑器

{
  "id": 1,
  "name": "Folio",
  "coordinates": null,
  "bookList": [
    {
      "id": 1,
      "title": "Le cycle de Fondation, I : Fondation",
      "categoryList": [
        {
          "id": 5,
          "category": "LITERATURE&FICTION"
        }
      ],
      "language": "French",
      "isbn": 2070360539,
      "publicationDate": null,
      "numberOfPages": 416,
      "authorList": [],
      "libraryList": [
        {
          "id": 2,
          "name": "Library2",
          "coordinates": null
        },
        {
          "id": 1,
          "name": "Library1",
          "coordinates": null
        }
      ],
      "editor": null
    }
  ]
}

书本

{
  "id": 1,
  "title": "Le cycle de Fondation, I : Fondation",
  "categoryList": [
    {
      "id": 5,
      "category": "LITERATURE&FICTION",
      "bookList": null
    }
  ],
  "language": "French",
  "isbn": 2070360539,
  "publicationDate": null,
  "numberOfPages": 416,
  "authorList": [],
  "libraryList": [
    {
      "id": 2,
      "name": "Library2",
      "coordinates": null
    },
    {
      "id": 1,
      "name": "Library1",
      "coordinates": null
    }
  ],
  "editor": {
    "id": 1,
    "name": "Folio",
    "coordinates": null,
    "bookList": null
  }
}

类别

{
  "id": 1,
  "category": "CHILDREN",
  "bookList": [
    {
      "id": 5,
      "title": "Le petit prince",
      "categoryList": null,
      "language": "French",
      "isbn": 9782070612758,
      "publicationDate": null,
      "numberOfPages": 120,
      "authorList": [],
      "libraryList": [
        {
          "id": 2,
          "name": "Library2",
          "coordinates": null
        },
        {
          "id": 1,
          "name": "Library1",
          "coordinates": null
        }
      ],
      "editor": null
    }
  ]
}

希望对您有所帮助:)

你肯定有帮助!谢谢你的例子。为了完整性和更多细节,我将在此处添加我的信息,因为我认为可用的信息太少了:

在 bi-directional 关系的情况下,由于循环引用,此类映射可能会触发 WhosebugError。

示例:类食谱、书籍和成分双向相关 1-to-many 和许多-to-many.

  • 一个食谱有很多成分,但只在一本书中提到。
  • 一本书里有很多食谱。
  • 一种成分仅用于 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);
    }

如果您停在那里并尝试以这种方式映射 类,由于您现在定义的循环引用,您将被 WhosebugError 击中(食谱包含具有 属性 的成分有配料的食谱……)。 这种默认的 Mapper 设置只能在没有双向关系的情况下使用,这也会触发反向映射。

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

  • 深入了解前端中的关联对象:例如。显示食谱的成分列表
  • 保存对象时保持反向关系:例如。如果您只映射 A -> B。 RecipeDTO 中的 IngredientDTO 将没有配方 属性 并且在保存成分时您需要将配方 ID 作为参数传递并跳过一些环以关联成分在将成分实体保存到数据库之前使用配方实体对象的实体对象。

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

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

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

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

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

如果您使用命名映射,最好使用与@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

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

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