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.
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.
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
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.