Sri Jay
Sri Jay

Reputation: 53

How to flatten nested references into a simple target?

I am using spring data jdbc with MapStruct. The POJOs are aligned with the table structure with all "internal-only" data like surrogate keys, audit information etc while the Domain object is hierarchical and contains only business relevant data. I have to map between a table specific POJO (flat structure) and a Domain object with nested objects. How can I best do this with MapStruct 1.2.0 ?

// Pojo for table
public class PersonsRecord {
    @Id
    private Integer surrogateKey; // should not be mapped

    private String loginId;
    private String name;
    private Timestamp memberSince;

    // address columns
    private Integer houseNumber;
    private String addressStreet;
    private String addressCity;
    private String country;

    // getters and setters
}

// Domain object
public class PersonDto {
    private String loginId;
    private String name;
    private Timestamp memberSince;

    private Address address;
}

@Mapper(uses = MapperUtils.class)
public interface PersonMapper {

    PersonDto toPersonDto(PersonRecord person);

    Address toAddress(PersonRecord person);
}

The generated code for the toPersonDto ignores the Address and does not call the toAddress method like MapStruct reference mentions. May be because both methods takes the same input type? The generated code for toAddress looks good. Just that it is not automatically wired by MapStruct generation.

The reverse mapping generation is similar. No automatic call.

Is there a way to make this automatic (without custom methods, or decorators etc) ? I have similar pattern in many places.

Note : The MapperUtils is used for instantiating DTOs with immutable fields.

Update: I used the answer by Filip for mapping from PersonRecord to PersonDto including the Person.Address. For the reverse mapping I did the following:

@Mapper(uses = MapperUtils.class)
public abstract class PersonMapper {
    @Mapping(target = "address", source = "person")
    public abstract PersonDto toPersonDto(PersonRecord person);

    protected abstract Address toAddress(PersonRecord person);

    public abstract PersonRecord toRecord(PersonDto dto);

    public abstract void updateAddress(PersonDto dto, @MappingTarget PersonRecord person);

    @AfterMapping
    protected void updateAddress(PersonDto dto, @MappingTarget PersonRecord record) {
        updateAddress(dto, record);
    }
}

That worked without issues. Thanks Filip for providing the direction.

Upvotes: 4

Views: 5070

Answers (1)

Filip
Filip

Reputation: 21403

The reason why the toAddress method is not picked up is that MapStruct doesn't know that it needs to map the toPersonDto parameter to the PersonDto.address. You are also most likely getting a warning that the address is not mapped.

In order to do the mapping you need to tell MapStruct how to map the address. You can do that by using @Mapping. Your mapper will then look like

@Mapper(uses = MapperUtils.class)
public interface PersonMapper {

    @Mapping(target = "address", source = "person")
    PersonDto toPersonDto(PersonRecord person);

    Address toAddress(PersonRecord person);
}

Update I forgot to add how to perform the reverse.

One way to perform the reverse mapping would be with custom @Mapping annotation, you will have to do this for each entry in Address. This is a bit cumbersome. There is an open Pull Request for MapStruct to allow this mapping to be done more easily. The PR is mapstruct/mapstruct#1686.

Another way would be to use multi source parameter mapping. This can look like:

@Mapper(uses = MapperUtils.class)
public interface PersonRecordMapper {

    default PersonRecord toPersonRecord(PersonDto personDto) {
        if (personDto == null) {
            return null;
        }

        return toPersonRecord(personDto, personDto.getAddress());
    }

    @Mapping(target = "surrogateKey", ignore = true)
    PersonRecord toPersonRecord(PersonDto personDto, Address address);

}

This is going to work without manual @Mapping if and only if the names properties in the Address match the ones in PersonRecord. Otherwise you will still need to define @Mapping.

Upvotes: 3

Related Questions