Search code examples
javamappingmapstructbidirectional

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


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! :)


Solution

  • [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 :)