I'm struggling with cycle dependency problem with MapStruct.
I keep having a StackOverFlow error due to circular dependencies.
To avoid it, I just need to exclude a property of a List.
I found this : https://github.com/mapstruct/mapstruct/issues/933
I deeply looked over the internet and I have been surprised that I couldn't find any full exemple showing a bi-directional DTO mapping with MapStruct (except using @Context CycleAvoidingMappingContext
, not working to me).
[EDIT]: I found a workaround thanks to MapStruct chat, I add it to EditorMapper
Here is my case, pretty common I guess : I have 2 DTOs referencing each other:
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;
}
And I need MapStruct to be able to exclude the property Editor from the BookList in Editor, and then avoid the infinite loop. Here is what I currently have as mappers:
@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 );
}
[EDIT]: it now works but it's not 100% clean (please see the answer I posted for more details)
I also tried this kind of method in mappers, but it didn't have any effect on my pb.
BookDTO toDTO( BookEntity bookEntity, @Context CycleAvoidingMappingContext context );
Does anyone know what I'm doing wrong? THANKS A LOT! :)
[EDIT]: I add the solution for a bi-directional ManyToMany mapping too
Thanks to https://gitter.im/mapstruct/mapstruct-users#, I have been able to get the solution.
[EDIT]: I still had errors that I didn't realize. It's now corrected in this update.
I had to :
- add uses
attribut to EditorMapper
: @Mapper(componentModel = "spring", uses = BookMapper.class)
- add alternatives methods like toDTONoEditor
or toDTOListNoEditor
in BookMapper
where I ignore the editor
property.
- map theses alternative methods in EditorMapper
- same for each circular dependency
Here is the solution:
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;
}
CategoryDTO
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 );
}
This way, The circular dependency is broken before it comes to infinite loop.
However, it is 99% satisfying, because the Editor
and Book
objects aren't perfectly clean. Editor
contains the bookList, well. But each book in bookList
still contains a null editor
field. And vice versa for the Book
object.
But it seems to be a De/Serialization problem, not a MapStruct one.
Here are the Json resulting
Editor
{
"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
}
]
}
Book
{
"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
}
}
Category
{
"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
}
]
}
Hope this help :)