Search code examples
javaspring-bootormmapstructbidirectional

Mapstruct map using bidirectional add* method


I have the following entities:

@Entity, Getter, Setter
public class Plant { // the "child" class

    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    private PlantGroup plantGroup;
}

@Entity, Getter, Setter
public class PlantGroup {

    private String name;

    @Setter(AccessLevel.NONE)
    @OneToMany(mappedBy = "plantGroup")
    private List<Plant> plants = new ArrayList<>();

    public void addPlant(Plant plant) {
        plants.add(plant);
        plant.setPlantGroup(this);
    }

}

I want to map from the following DTO:

@Getter
@Setter
public class PlantGroupDTO {

    private String name;

    private List<Long> plantIds;
}

This is my mapper:

@Mapper(componentModel = "spring", collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED)
public abstract class PlantGroupMapper {
    
    @Autowired
    PlantService plantService;

    public abstract PlantGroup map(PlantGroupDTO dto);

    public List<Plant> map(List<Long> ids) {
    // I would expect this to be called
    }


}

What I want to achieve is: while mapping from my List<Long> plantIds to List<Plant> plants I am not only retrieving them from the service, but also add them to the return value PlantGroup by calling plantGroup.addPlant(plant);

However I don't understand the error message:

Can't map property "List<Long> plants" to "Plant plants". Consider to declare/implement a mapping method: "Plant map(List<Long> value)".

Why does it want to map the List to a single entity? Is there another best practice to force Mapstruct to use the bidirectional add* method?

Basically I just need this line to be executed:

dto.getPlantIds()
.forEach(plantId -> 
    plantGroup.addPlant(plantService.findById(plantId)
        .orElseThrow(() -> new EntityNotFoundException(plantId, Plant.class))));

Solution

  • I was already using the correct collectionMappingStrategy CollectionMappingStrategy.ADDER_PREFERRED (reference), but I did not correctly understand the error message - also a bit misleading from my point of view.

    As @Luca Basso Ricci suggested all I had to do was add a method like Plant map(Long id). It works, however Hibernate needs some more work to get updating bidirectional relationships going when working with Collections otherwise it would just append and not delete child entities properly.

    My working solution:

    @Mapper(componentModel = "spring", collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED)
    public abstract class PlantGroupMapper {
    
        @Autowired
        PlantService plantService;
    
        @BeforeMapping
        public void prepareForUpdate(@MappingTarget PlantGroup entity) {
            entity.getPlants().clear();
        }
        
        @Mapping(target = "plants", source = "plantIds", qualifiedByName = "mapPlantFromId")
        public abstract PlantGroup map(PlantGroupDTO dto);
        
        @Mapping(target = "plants", source = "plantIds", qualifiedByName = "mapPlantFromId")
        public abstract DefectGroup update(DefectGroupBasicDTO dto, @MappingTarget DefectGroup entity);
    
        @Named("mapPlantFromId")
        Plant mapPlantFromId(Long id) {
            return plantService.findById(id).orElseThrow(() -> new EntityNotFoundException(id, Plant.class));
        }
    
    }
    

    The prepareForUpdate method gets invoked by both the map() and the update() method, although it is quite useless in the map method because it is called after creating the Object so it's always clearing an empty array.

    As one must know the behaviour in Hibernate (see Hibernate @OneToMany remove child from list when updating parent), I'm not sure if this is the cleanest solution for other developers to understand.
    For now I just implemented the update() method myself and deleted the @BeforeMapping method again. But the DTO in question has only 1 extra field, so I might change my point of view on that in the future ;)