Reputation: 8219
I have as parent class : User.java
, and 2 classes : FacebookUser.java
and TwitterUser.java
they are entities that returned depends on the type column in database using DiscriminatorColumn
, I want to write correct mapper to map User that could be instance of FacebookUser or TwitterUser. I have the following mapper that seems not works as intended, only Mapping the User
parent not the children:
@Mapper
public interface UserMapper {
public static UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
User map(UserDTO userDTO);
@InheritInverseConfiguration
UserDTO map(User user);
List<UserDTO> map(List<User> users);
FacebookUser map(FacebookUserDTO userDTO);
@InheritInverseConfiguration
FacebookUserDTO map(FacebookUser user);
TwitterUser map(TwitterUserDTO userDTO);
@InheritInverseConfiguration
TwitterUserDTO map(TwitterUser user);
}
Then I use :
UserDTO userDto = UserMapper.INSTANCE.map(user);
Classes to map:
@Entity
@Table(name = "users")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "type", discriminatorType = DiscriminatorType.STRING, length = 10)
@DiscriminatorValue(value = "Local")
public class User {
@Column
private String firstName;
@Column
private String lastName;
///... setters and getters
}
@Entity
@DiscriminatorValue(value = "Facebook")
public class FacebookUser extends User {
@Column
private String userId;
///... setters and getters
}
@Entity
@DiscriminatorValue(value = "Twitter")
public class TwitterUser extends User {
@Column
private String screenName;
///... setters and getters
}
The DTOs:
public class UserDTO {
private String firstName;
private String lastName;
///... setters and getters
}
public class FacebookUserDTO extends UserDTO {
private String userId;
///... setters and getters
}
public class TwitterUserDTO extends UserDTO {
private String screenName;
///... setters and getters
}
Also if I have list of users that mixed with Facebook users and Twitter users, or basic user:
Lets say I have the following users:
User user = new User ("firstName","lastName");
User fbUser = new FacebookUser ("firstName","lastName","userId");
User twUser = new TwitterUser ("firstName","lastName","screenName");
List<User> users = new ArrayList<>();
users.add(user);
users.add(fbUser);
users.add(twUser);
//Then:
List<UserDTO> dtos = UserMapper.INSTANCE.map(users);
I get only firstName
and lastName
but not screenName
or userId
.
Any solution for this?
Upvotes: 8
Views: 8906
Reputation: 1
Was googling about this exact issue yesterday, but couldn't find anything on internet, so let's leave this here.
As the issue mentioned in the former answer has been solved earlier this year, Mapstruct now officially supports downcasting by @SubclassMapping
.
public interface UserMapper {
@Named("UserToDTO")
@SubclassMapping(source = FacebookUser.class, target = FacebookUserDTO.class)
@SubclassMapping(source = TwitterUser.class, target = TwitterUserDTO.class)
UserDTO toDTO(User source);
}
However you can't put parameterized class i.e. List<User>
in that annotation, and Mapstruct isn't actually using this specification above for Lists. Hence, one more function is needed.
public interface UserMapper {
@Named("UserToDTOList")
@IterableMapping(qualifiedByName = "UserToDTO")
List<UserDTO> toDTO(List<User> source);
@Named("UserToDTO")
@SubclassMapping(source = FacebookUser.class, target = FacebookUserDTO.class)
@SubclassMapping(source = TwitterUser.class, target = TwitterUserDTO.class)
UserDTO toDTO(User source);
}
This also applies to field in a class. For my case, we have a list field like that in a class. At first I only added @SubclassMapping
, but after checking the code Mapstruct generated, I found that it isn't using the method for list converting. Adding @IterableMapping
fixed that.
Consider a UsersWrapper
class with a list field in it.
public class UsersWrapper {
List<User> list;
}
And a UsersWrapperDTO
.
public class UsersWrapperDTO {
List<UserDTO> list;
}
Then, we can use one more method in the mapper.
public interface UserMapper {
@Mapping(source = "source.list", target = "list", qualifiedByName = "UserToDTOList")
UsersWrapperDTO toDTO(UsersWrapper source);
@Named("UserToDTOList")
@IterableMapping(qualifiedByName = "UserToDTO")
List<UserDTO> toDTO(List<User> source);
@Named("UserToDTO")
@SubclassMapping(source = FacebookUser.class, target = FacebookUserDTO.class)
@SubclassMapping(source = TwitterUser.class, target = TwitterUserDTO.class)
UserDTO toDTO(User source);
}
Mapstruct actually generates very readable codes, so if you find it doesn't work like you thought, checking there first is also an option.
Ain't familiar with Mapstruct so took me a whole afternoon to put the pieces together! Though, it does seem simple and make sense that I need to combine these two annotations together. Hopefully this post will save people some time.
Upvotes: 2
Reputation: 8219
Currently, it seems it's not available yet as a feature for mapstruct : Support for Type-Refinement mapping (or Downcast Mapping)
I asked the question in their google group: https://groups.google.com/forum/?fromgroups#!topic/mapstruct-users/PqB-g1SBTPg
and found that I need to do manual mapping using default
method inside interface (for java 8).
And got another issue for mapping parent that was almost not applicable so I write one more empty class that child of parent class called LocalUserDTO
:
So the code becomes like the following:
@Mapper
public interface UserMapper {
public static UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
LocalUser map(LocalUserDTO userDTO);
@InheritInverseConfiguration
LocalUserDTO map(LocalUser user);
List<UserDTO> map(List<User> users);
FacebookUser map(FacebookUserDTO userDTO);
@InheritInverseConfiguration
FacebookUserDTO map(FacebookUser user);
TwitterUser map(TwitterUserDTO userDTO);
@InheritInverseConfiguration
TwitterUserDTO map(TwitterUser user);
default UserDTO map(User user) {
if (user instanceof FacebookUser) {
return this.map((FacebookUser) user);
} else if (user instanceof TwitterUser) {
return this.map((TwitterUser) user);
} else {
return this.map((LocalUser) user);
}
}
@InheritInverseConfiguration
default User map(UserDTO userDTO) {
if (userDTO instanceof FacebookUserDTO) {
return this.map((FacebookUserDTO) userDTO);
} else if (userDTO instanceof TwitterUserDTO) {
return this.map((TwitterUserDTO) userDTO);
} else {
return this.map((LocalUserDTO) userDTO);
}
}
}
Upvotes: 8