实际上,使用 MapStruct 1.3.1 版时,这种带有 CycleAvoidingMappingContext 的方法对我不起作用。由于我找不到太多工作示例,因此我致力于在此处发布我的解决方案以供其他人查找。
在双向关系的情况下,此类映射可能会由于循环引用而触发 StackOverflowError。
示例:类食谱、书籍和成分,它们以一对多和多对多的方式双向相关。
- 一个食谱有很多成分,但只有一本书提到。
- 一本书里有很多食谱。
- 一种成分仅在一个配方中使用(假设一种成分还具有固定其数量、计量单位等的属性,因此它确实仅特定于一个配方)。
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 类。
这些将是从实体类映射到 DTO 类的默认 Mapper 设置(在这种情况下不依赖于 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);
}
如果您停在那里并尝试以这种方式映射类,由于您现在定义的循环引用,您将受到 StackOverflowError 的打击(配方包含具有属性配方的成分......)。只有在不存在会触发反向映射的双向关系时才能使用此类默认映射器设置。
你可以把它写下来像 A -> B -> A -> B -> A ...关于对象映射,我的经验表明你应该能够像这样映射它: A -> B -> A (这次不包括关系以打破循环)对于实体到 DTO 和 DTO 到实体的映射。这使您能够:
- 深入到前端的关联对象:例如。显示食谱的成分列表
- 保存对象时保持反比关系:例如。如果您只映射 A -> B。RecipeDTO 中的成分 DTO 将没有配方属性,并且在保存成分时,您需要将配方 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
配方中的 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> );`