10

I'm using MapStruct to make dto <-> entity mapping. The same mappers are used to create and update entities from dtos. A verification of the dto's id is done to know whether a new entity must be created (id == null) or it should be retrieved from database (id != null) .

I'm actually using MapperDecorator as a workaround. Example :

Mapper

@Mapper
@DecoratedWith(UserAccountDecorator.class)
public interface UserAccountMapper {

    UserAccountDto map(User user);

    User map(UserAccountDto dto);

    User map(UserAccountDto dto, @MappingTarget User user);
}

Decorator

public abstract class UserAccountDecorator implements UserAccountMapper {

    @Autowired
    @Qualifier("delegate")
    private UserAccountMapper delegate;

    @Autowired
    private UserRepository userRepository;

    @Override
    public User map(UserAccountDto dto) {
        if (dto == null) {
            return null;
        }

        User user = new User();
        if (dto.getId() != null) {
            user = userRepository.findOne(dto.getId());
        }

        return delegate.map(dto, user);
    }

}

But this solution becomes heavy due to the fact that a decorator must be created for each mapper.

Is there any good solution to perform that ?


I'm using :

  1. MapStruct : 1.1.0
Radouane ROUFID
  • 10,595
  • 9
  • 42
  • 80

2 Answers2

22

I solved my problem by following the advice of Gunnar in the comment.

I moved to MapStruct 1.2.0.Beta1 and created a UserMapperResolver like below

@Component
public class UserMapperResolver {

    @Autowired
    private UserRepository userRepository;

    @ObjectFactory
    public User resolve(BaseUserDto dto, @TargetType Class<User> type) {
        return dto != null && dto.getId() != null ? userRepository.findOne(dto.getId()) : new User();
    }

}

Which I use then in my UserMapper :

@Mapper(uses = { UserMapperResolver.class })
public interface BaseUserMapper {

    BaseUserDto map(User user);

    User map(BaseUserDto baseUser);

}

The generated code is now :

@Override
    public User map(BaseUserDto baseUser) {
        if ( baseUser == null ) {
            return null;
        }

        User user = userMapperResolver.resolve( baseUser, User.class );

        user.setId( baseUser.getId() );
        user.setSocialMediaProvider( baseUser.getSocialMediaProvider() );
...
}

Works well !

Community
  • 1
  • 1
Radouane ROUFID
  • 10,595
  • 9
  • 42
  • 80
  • Hi Radouane ROUFID, thanks for sharing your final solution. I have a small question : Let's admit I want to affect in my resolver, an object retrieved from my Repository into a field of the entity Object. It is possible to do this in Object Factories of MapStruct ? Didn't found any similar use case examples. EG: I have a User which have a Phone entity in OneToOne relationship. If I get a UserDto which contains a phoneId and I want to map it to a User with an actual Phone entity. thanks ! – Alex Feb 07 '18 at 16:43
  • You can inject you PhoneRepository in your UserResolver and fetch the phone entity from db. `@Autowired private UserRepository userRepository; @Autowired private PhoneREpository phoneRe; @ObjectFactory public User resolve(BaseUserDto dto, @TargetType Class type) { User user = userRepository.findOne(dto.getId()); user.setPhone(phoneRep.getFIndOne(dto.getPhoneId())) return user; }` – Radouane ROUFID Feb 08 '18 at 09:55
  • Yes I would do that but in the case of the creation of a User ? I return a new User item with just the Phone info but nothing else ? Does mapper is called after to complete the object with the actual DTO or do we have to do all the mapping logic in the resolver ? – Alex Feb 08 '18 at 14:08
  • ObjectFactory create/fetch the object in which the mapping will be done. You can check the generated code in my solution. – Radouane ROUFID Feb 08 '18 at 14:19
  • awsome solution like it so much – Mohammed Housseyn Taleb Jul 01 '22 at 15:27
2

MapStruct alone can't do that. However, with some generics and a main abstract class you can make your life easier.

You need one generic interface. It must not be annotated with @Mapper, because if it is MapStruct will try to generate an implementation and it will fail. It cannot generate generic mappers.

public interface GenericMapper<E, DTO> {

    DTO map(E entity);

    E map(DTO dto);

    E map(DTO dto, @MappingTarget E entity);
}

Then you need one abstract class where you'll have your logic.

public abstract class AbstractGenericMapper<E, DTO> implements GenericMapper<E, DTO> {

    @Autowired
    private Repository<E> repository;

    @Override
    public final E map (DTO dto) {
        if (dto == null) {
            return null;
        }

        // You can also use a Java 8 Supplier and pass it down the constructor
        E entity = newInstance();
        if (dto.getId() != null) {
            user = repository.findOne(dto.getId());
        }

        return map(dto, entity);
    }

    protected abstract E newInstance();
}

And then each of your mappers will only need to extend this abstract class.

@Mapper
public abstract class UserAccountMapper extends AbstractGenericMapper<User, UserDto> {

    protected User newInstance() {
        return new User();
    }
}

MapStruct will then generate an implementation for your mapper and you will only have to extend the AbstractGenericMapper for the future. Of course you will need to adapt the generic parameters so you can at least get the id from the via some interface maybe. If you have different type of ids then you will have to add that generic parameter to the AbstractGenericMapper as well.

MWiesner
  • 8,868
  • 11
  • 36
  • 70
Filip
  • 19,269
  • 7
  • 51
  • 60
  • Thank you Filip, I will try this solution ! – Radouane ROUFID Feb 21 '17 at 21:08
  • 3
    An alternative may be object factory methods receiving the source objects as enabled in [MapStruct 1.2.0.Beta1](http://mapstruct.org/news/2017-02-20-mapstruct-1_2_0_Beta1-is-out-with-lombok-support-and-direct-field-access/#more-powerful-target-bean-factories) (released yesterday). You could have a generic factory method which either instantiates a new entity or loads it from the database, depending on the DTO state (id present or not). – Gunnar Feb 21 '17 at 22:11
  • Very nice, This solution is better. Thank you Gunnar, You are doing a great job ! – Radouane ROUFID Feb 22 '17 at 05:39
  • I was also thinking about the new possibility of the factory methods to return an object. However, if that is used, MapStruct will immediately return the object if isn't `null` – Filip Feb 22 '17 at 06:57