2

I wonder if and how Mapstruct could help with mapping ojects with bi-directional relations (in my case one to many):

public class A{
     private Set<B> listB;
}

public class B{
     private A a;
}

Mapping from/to an entity yields a StackOverflowError. (i would expect this to happen). On the other hand the closed Mapstruct issues 469 and 1163 seem to imply that mapstruct will not directly support it. I tried this example:

https://github.com/mapstruct/mapstruct-examples/tree/master/mapstruct-mapping-with-cycles

But this is just not working. With or without applying the "CycleAvoidingMappingContext" the stackoverflowerror keeps the same.

So how to map objects with cycles and leveraging mapstruct? (i am want to avoid complete manual mapping)

Vadim Kotov
  • 8,084
  • 8
  • 48
  • 62
dermoritz
  • 12,519
  • 25
  • 97
  • 185
  • Can you provide the code which is not working for you? The example you linked should work. – Filip Apr 12 '19 at 15:55
  • I tried it with the code from the example. Using this class: https://github.com/mapstruct/mapstruct-examples/blob/master/mapstruct-mapping-with-cycles/src/main/java/org/mapstruct/example/mapper/CycleAvoidingMappingContext.java – dermoritz Apr 13 '19 at 13:44
  • Maybe showing the mapper that you've tried would help us more to see where the error is. I just tried it (and answered) with the info you provided and it works as expected. – Filip Apr 14 '19 at 06:48

2 Answers2

1

In order for the mapping to work you can try with the following mapper:

@Mapper
public interface MyMapper {

    A map(A a, @Context CycleAvoidingMappingContext context);

    Set<B> map(Set<B> b, @Context CycleAvoidingMappingContext context);

    B map(B b, @Context CycleAvoidingMappingContext context);
}

If the methods for mapping from Set<B> into Set<B> is not there then the set in A won't be mapped.

Filip
  • 19,269
  • 7
  • 51
  • 60
0

In meantime i found a solution: First i ignore the field in question and map it in "AfterMapping". In my case i want to map also the child independently. In my case the @Context only contains a Boolean (boolean is not working) that tells where i entered the cycle (startedFromPru pru is the parent):

Mapper for parent (contains set of licenses):

@Mapper(componentModel = "spring", uses = {DateTimeMapper.class, LicenseMapper.class, CompanyCodeMapper.class, ProductHierarchyMapper.class})
public abstract class ProductResponsibleUnitMapper {

    @Autowired
    private LicenseMapper licenseMapper;

    @Mappings({@Mapping(target = "licenses", ignore = true)})
    public abstract ProductResponsibleUnit fromEntity(ProductResponsibleUnitEntity entity, @Context Boolean startedFromPru);

    @Mappings({@Mapping(target = "licenses", ignore = true)})
    public abstract ProductResponsibleUnitEntity toEntity(ProductResponsibleUnit domain, @Context Boolean startedFromPru);

    @AfterMapping
    protected void mapLicenseEntities(ProductResponsibleUnit pru, @MappingTarget ProductResponsibleUnitEntity pruEntity, @Context Boolean startedFromPru){
        if(startedFromPru) {
            pruEntity.getLicenses().clear(); //probably not necessary
            pru.getLicenses().stream().map(license -> licenseMapper.toEntity(license, startedFromPru)).forEach(pruEntity::addLicense);
        }
    }

    @AfterMapping
    protected void mapLicenseEntities(ProductResponsibleUnitEntity pruEntity, @MappingTarget ProductResponsibleUnit pru, @Context Boolean startedFromPru){
        if(startedFromPru) {
            pru.getLicenses().clear(); //probably not necessary
            pruEntity.getLicenses().stream().map(licenseEntity -> licenseMapper.fromEntity(licenseEntity, startedFromPru)).forEach(pru::addLicense);
        }
    }

}

Mapper for child:

@Mapper(componentModel = "spring", uses = { DateTimeMapper.class, CompanyCodeMapper.class, ProductHierarchyMapper.class,
        CountryCodeMapper.class })
public abstract class LicenseMapper {

    @Autowired
    private ProductResponsibleUnitMapper pruMapper;

    @Mappings({ @Mapping(source = "licenseeId", target = "licensee"), @Mapping(target = "licensor", ignore = true) })
    public abstract License fromEntity(LicenseEntity entity, @Context Boolean startedFromPru);


    @Mappings({ @Mapping(source = "licensee", target = "licenseeId"), @Mapping(target = "licensor", ignore = true) })
    public abstract LicenseEntity toEntity(License domain, @Context Boolean startedFromPru);

    @AfterMapping
    protected void mapPru(LicenseEntity licenseEntity, @MappingTarget License license,
            @Context Boolean startedFromPru) {
        //only call ProductResponsibleUnitMapper if mapping did not start from PRU -> startedFromPru is false
        if (!startedFromPru) {
            license.setLicensor(pruMapper.fromEntity(licenseEntity.getLicensor(), startedFromPru));
        }
    }

    @AfterMapping
    protected void mapPru(License license, @MappingTarget LicenseEntity licenseEntity,
            @Context Boolean startedFromPru) {
        //only call ProductResponsibleUnitMapper if mapping did not start from PRU -> startedFromPru is false
        if (!startedFromPru) {
            licenseEntity.setLicensor(pruMapper.toEntity(license.getLicensor(), startedFromPru));
        }
    }
dermoritz
  • 12,519
  • 25
  • 97
  • 185