0

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))));
ch1ll
  • 419
  • 7
  • 20
  • 1
    write a `Plant map(Long id)` and iterable should be mapped automatically – Luca Basso Ricci Jun 06 '23 at 14:40
  • You are right! Thank you. Weird error-message though. But within this code I can't access the ```plantGroup.add(Plant p)```. I would like to use this for bidirectional mapping though. Any suggestion about this? – ch1ll Jun 06 '23 at 14:53
  • 1
    I've never used it but https://mapstruct.org/documentation/dev/reference/html/#collection-mapping-strategies – Luca Basso Ricci Jun 06 '23 at 15:12
  • I wrote a method like you suggested, and it works now for mapping the DTO to an entity! Thank you for that. However it does not work for updating because now it does not override the values anymore - obviously, but I cannot configure the behaviour on method level. I'm quite sure there should be a solution for that, I can't be the only one facing this issue. – ch1ll Jun 07 '23 at 08:11
  • Based on documentation using `ADDER_PREFERRED` will use `add*()` if present and fallback to standard setter if adder is missing. Let us know! – Luca Basso Ricci Jun 07 '23 at 08:22
  • yes, it does! It generates the code using ```plantGroup.addPlant(map(plantId));```. But there is no hook for me to add a ```plantGroup.clear()``` before that operation when I'm trying to update an entity. Hibernate needs this though, as stated here https://stackoverflow.com/a/14686770/3824715 – ch1ll Jun 07 '23 at 08:44
  • 1
    did you try a `@beforemapping`? – Luca Basso Ricci Jun 07 '23 at 09:20
  • It does work with ```@BeforeMapping```. However I'm not sure if this code is less cluttered or more readable than if I just wrote the Mapper myself to be honest :D. Only remaining advantage is the error on unmappedTargetProperties – ch1ll Jun 07 '23 at 09:56
  • 1
    If you add spare time, answer to your own question with a minimal working example and accept it for other people with the same problem – Luca Basso Ricci Jun 07 '23 at 10:13
  • 1
    just did, thank you for your hints! – ch1ll Jun 07 '23 at 11:57

1 Answers1

0

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

ch1ll
  • 419
  • 7
  • 20