Search code examples
javaspringspring-bootmappingmapstruct

Is there a way to prevent MapStruct from overwriting values during the update of a record?


I have seen that there was a similar question in 2018: Mapstruct to update values without overwriting but there was no example of solving this issue.

Thus, I could not figure out how to solve it.

I am using Lombok and MapStruct

UserEntity represents the table in the Database

@Getter
@Setter
@Entity
@Table(name = "users")
public class UserEntity implements Serializable {

    private static final long serialVersionUID = -3549451006888843499L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)     // this specifies that the id will be auto-incremented by the database
    private Long id;
    
    @Column( nullable = false)
    private String userId;
    
    @Column( nullable = false, length = 50)
    private String firstName;
    
    @Column( nullable = false, length = 50)
    private String lastName;
    
    @Column( nullable = false, length = 120)
    private String email;
    
    @Column( nullable = false)
    private String encryptedPassword;   // I am encrypting the password during first insertion of a new record

}// end of class

UserDTO is used as an intermediate between the UserEntity and the UserRequestModel (I will mention it later).

@Getter
@Setter
@ToString
public class UserDTO implements Serializable {

    private static final long serialVersionUID = -2583091281719120521L;
    
//  ------------------------------------------------------
//  Attributes
//  ------------------------------------------------------
    private Long id;            // actual id from the database table
    private String userId;      // public user id
    private String firstName;
    private String lastName;
    private String email;
    private String password;
    private String encryptedPassword;       // in the database we store the encrypted password  

}// end of class

UserRequestModel is used when a request arrives in the UserController.

@Getter
@Setter
@ToString
public class UserRequestModel {
    
//  ------------------------------------------------------
//  Attributes
//  ------------------------------------------------------
    private String firstName;
    private String lastName;
    private String email;
    
}// end of class

I created a UserMapper interface which is used by MapStruct.

@Mapper(componentModel = "spring", nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
public interface UserMapper {

    @Mapping(target = "password", ignore = true)
    UserDTO mapEntityToDto(UserEntity userEntity);

    @Mapping(target = "encryptedPassword")
    UserEntity mapDtoToEntity(UserDTO userDto);
    
    UserResponseModel mapDtoToResponseModel(UserDTO userDto);
    
    @Mapping(target = "encryptedPassword", ignore = true)
    @Mapping(target = "id", ignore = true)
    @Mapping(target = "userId", ignore = true)
    UserDTO mapUserRequestModelToDto(UserRequestModel userRequestModel);
    
}// end of interface

Finally, I use UserResponseModel to return a record to the client application.

@Getter
@Setter
@ToString
public class UserResponseModel {
    
//  ------------------------------------------------------
//  Attributes
//  ------------------------------------------------------
    
    // This is NOT the actual usedId in the database!!!
    // We should not provide the actual value. For security reasons.
    // Think of it as a public user id.
    private String userId;
    private String firstName;
    private String lastName;
    private String email;

}// end of class

The object above are used for the mapping. Now, I am going to show you the code from the UserController and the UserService. This issue occurs inside the UserService class.

@PutMapping(path = "/{id}")
public UserResponseModel updateUser(@PathVariable("id") String id , @RequestBody UserRequestModel userRequestModel) {
        
    UserResponseModel returnValue = new UserResponseModel();
        
    UserDTO userDTO = userMapper.mapUserRequestModelToDto(userRequestModel);
        
    // call the method to update the user and return the updated object as a UserDTO
    UserDTO updatedUser = userService.updateUser(id, userDTO);
        
    returnValue = userMapper.mapDtoToResponseModel(updatedUser);
        
    return returnValue;
        
}// end of updateUser

UserService is used to implement the business logic of the application.

@Override
public UserDTO updateUser(String userId, UserDTO user) {
        
    UserDTO returnValue = new UserDTO();
        
    UserEntity userEntity = userRepository.findByUserId(userId);
        
    if(userEntity == null) {
        throw new UserServiceException("No record found with the specific id!");        // our own exception
    }
        
    // get the changes from the DTO and map them to the Entity
    // this did not work. The values of the Entity were set to null if they were not assigned in the DTO
    userEntity = userMapper.mapDtoToEntity(user);
        
    // If I set the values of the Entity manually, then it works
//      userEntity.setFirstName(user.getFirstName());
//      userEntity.setLastName(user.getLastName());
//      userEntity.setEmail(user.getEmail());
        
    // save the Entity
    userRepository.save(userEntity);
        
    returnValue = userMapper.mapEntityToDto(userEntity);
        
    return returnValue;
        
}// end of updateUser

If I try to run this code, I get the following error.

enter image description here

I used the debugger in order to understand the issue and I noticed that the MapStruct was overwriting all the attributes and not only those that had been send in the request.

The code that was automatically generated by MapStruct is the following:

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2020-11-01T11:56:36+0200",
    comments = "version: 1.4.1.Final, compiler: Eclipse JDT (IDE) 3.21.0.v20200304-1404, environment: Java 11.0.9 (Ubuntu)"
)
@Component
public class UserMapperImpl implements UserMapper {

    @Override
    public UserDTO mapEntityToDto(UserEntity userEntity) {
        if ( userEntity == null ) {
            return null;
        }

        UserDTO userDTO = new UserDTO();

        userDTO.setEmail( userEntity.getEmail() );
        userDTO.setEncryptedPassword( userEntity.getEncryptedPassword() );
        userDTO.setFirstName( userEntity.getFirstName() );
        userDTO.setId( userEntity.getId() );
        userDTO.setLastName( userEntity.getLastName() );
        userDTO.setUserId( userEntity.getUserId() );

        return userDTO;
    }

    @Override
    public UserEntity mapDtoToEntity(UserDTO userDto) {
        if ( userDto == null ) {
            return null;
        }

        UserEntity userEntity = new UserEntity();

        userEntity.setEncryptedPassword( userDto.getEncryptedPassword() );
        userEntity.setEmail( userDto.getEmail() );
        userEntity.setFirstName( userDto.getFirstName() );
        userEntity.setId( userDto.getId() );
        userEntity.setLastName( userDto.getLastName() );
        userEntity.setUserId( userDto.getUserId() );

        return userEntity;
    }

    @Override
    public UserResponseModel mapDtoToResponseModel(UserDTO userDto) {
        if ( userDto == null ) {
            return null;
        }

        UserResponseModel userResponseModel = new UserResponseModel();

        userResponseModel.setEmail( userDto.getEmail() );
        userResponseModel.setFirstName( userDto.getFirstName() );
        userResponseModel.setLastName( userDto.getLastName() );
        userResponseModel.setUserId( userDto.getUserId() );

        return userResponseModel;
    }

    @Override
    public UserDTO mapUserRequestModelToDto(UserRequestModel userRequestModel) {
        if ( userRequestModel == null ) {
            return null;
        }

        UserDTO userDTO = new UserDTO();

        userDTO.setEmail( userRequestModel.getEmail() );
        userDTO.setFirstName( userRequestModel.getFirstName() );
        userDTO.setLastName( userRequestModel.getLastName() );
        userDTO.setPassword( userRequestModel.getPassword() );

        return userDTO;
    }
}

Most probably, I am not doing something correctly.

Do you think the issue occurs because I do not have constructors? Normally Java automatically creates the no-arguments constructor, if we do not implement it.

I am adding the following image, so that I can demonstrate what I want to do. Maybe this flow can help.

enter image description here

Most probably the method mapDtoToEntity should accept 2 attributes in order to map the UserDTO to UserEntity. For instance:

userMapper.mapDtoToEntity(userDto, userEntity);
public UserEntity mapDtoToEntity(UserDTO userDto, UserEntity userEntity) {
        
    if( userDto == null ) {
        return null;
    }
        
    // set ONLY the attributes of the UserEntity that were assigned 
    // to the UserDTO by the UserRequestModel. In the current case only 3 attributes
        
    
    // return the updated UserEntity
        
}

Thank you Sergio Lema!!! I added your modification in my code and everything worked!

UPDATED VERSION

I had to add the following method to UserMapper class.

@Mapping(target = "firstName", source = "firstName")
@Mapping(target = "lastName", source = "lastName")
@Mapping(target = "email", source = "email")
void updateFields(@MappingTarget UserEntity entity, UserDTO dto);

Then, I had to modify the updateUser method inside the UserService class.

@Override
public UserDTO updateUser(String userId, UserDTO user) {
        
    UserDTO returnValue = new UserDTO();

    UserEntity userEntity = userRepository.findByUserId(userId);

    if (userEntity == null)
            throw new UserServiceException(ErrorMessages.NO_RECORD_FOUND.getErrorMessage());
        
    // update only specific fields of userEntity
    userMapper.updateFields( userEntity, user);

    userRepository.save(userEntity);
        
    // map again UserEntity to UserDTO in order to send it back
    returnValue = userMapper.mapEntityToDto(userEntity);

    return returnValue;
        
}// end of updateUser

Solution

  • In fact, you are creating a new instance of UserEntity each time a PUT is done. What should be done is an update of the desired fields. For that, you can use @MappingTarget as following:

    @Mapping(target = "targetFieldName", source = "sourceFieldName")
    void updateFields(@MappingTarget UserEntity entity, UserDTO dto);
    

    This annotation ensures that no new object is created, it will maintain the original object, just updating the desired fields.