2
votes

I'm struggling with form update. Please consider this example:

// Entity with which I need to perform CRUD operations
public class User {
    private String name;
    private String email;
    private String phone;
    private String address;
}

I send to UI is UserDTO:

public class UserDTO {
    private String name;
    private ContactDataDTO contactDataDTO;
}

public class ContactDataDTO {
    private String email;
    private String phone;
    private String address;
}

My mapper:

@Mapper
public interface UserMapper {

    @Mappings({
        @Mapping(source="email", target="contactDataDTO.email"),
        @Mapping(source="phone", target="contactDataDTO.phone"),
        @Mapping(source="address", target="contactDataDTO.address")
    })
    UserDTO userToUserDTO(User user);

    @InheritInverseConfiguration
    User updateUserFromUserDTO(UserDTO userDTO, @MappingTarget User user);

}

userToUserDTO() works as expected, but generated userDTOToUser() for me seems wierd:

@Override
public User updateUserFromUserDTO(UserDTO userDTO, User user) {
    if ( userDTO == null ) {
        return null;
    }

    String address = userDTOContactDataDTOAddress( userDTO );
    if ( address != null ) {
        user.setAddress( address );
    }
    String phone = userDTOContactDataDTOPhone( userDTO );
    if ( phone != null ) {
        user.setPhone( phone );
    }
    String email = userDTOContactDataDTOEmail( userDTO );
    if ( email != null ) {
        user.setEmail( email );
    }
    user.setName( userDTO.getName() );

    return user;
}

Problematic use case:

  1. Fill in all fields for User.
  2. Open form again and clear phone field.
  3. That means to backend I will send smth like this:

userDTO: {
        name: 'John Doe';
        contactDataDTO: {
            email: '[email protected]',
            phone: null,
            address: 'Home'
        }
    }

So, user.phone won't be updated, as far as I have null check for it in generated code.


I thought NullValueCheckStrategy is what I need, but there no option which fits me. For now the only option I see - write my own implementation of userDTOToUser() without null checks. Maybe you can advise better solution, cause for me it looks like a problem that can happen in any mapper for a target update from DTO with non-primitive source.

Runnable demo: https://repl.it/@aksankin/SlateblueUnimportantStack

Thanks a lot.

3
one of the prettiest code styles i've seen. - aran
In MapStruct 1.3Beta2 we introduced the NullValuePropertyMappingStrategy specifically to control update methods. It seems to me this is not working for nested properties. I need to investigate a bit further. - Sjaak
@sjaak tried it, didn't help. Tried basically all annotation and all options anyhow connected to "null") The cleanest from all dirty workaround I can think about now is to create new User object from UserDTO and then just update User.id with UserDTO.id. But I would be happy to know the recommended way to do it if it's possible. - Oksana Mykhalets'
I tried it as wel (and you are right). Please write an issue on MapStruct (github.com/mapstruct/mapstruct/issues).. I already started to take a look :). In the mean time, don't use target nesting in this scenario (this is what happens when you revert source nesting). I'll provide a work-around as answer. - Sjaak
@OksanaMykhalets': As an afterburner: it will work out of the box with your current example (I used your example to implement this, thanks). 1.3 should be released soon. - Sjaak

3 Answers

0
votes

Probably Optional<> is what you're searching for. In this case you would have null for empty field, Optional if null was send from UI and Optional for actual value. But probably you need to create different DTO for request and response.

0
votes

If you want null to have a certain value for you then you are looking for source presence checking. You can then control in the setPhone of the DTO whether it was set or not and add hasPhone that would use the flag. Then MapStruct will use the presence check method when setting the value.

0
votes

try:

@Mapper(  )
public interface UserMapper {

    UserMapper INSTANCE = Mappers.getMapper( UserMapper.class );

    @Mappings({
        @Mapping(source="email", target="contactDataDTO.email"),
        @Mapping(source="phone", target="contactDataDTO.phone"),
        @Mapping(source="address", target="contactDataDTO.address")
    })
    UserDTO userToUserDTO(User user);

    default void updateUserFromUserDTO(UserDTO userDTO, User user) {
        intUpdateUserFromUserDTO( userDTO, userDTO.getContactDataDTO(), user );
    }

    void intUpdateUserFromUserDTO(UserDTO userDTO, ContactDataDTO contactDataDTO, @MappingTarget User user);

}

(note: I returned void iso a type, which is strictly not needed).