12

I have the following example in which i have a separate domain layer and a separate persistence layer. I am using Mapstruct for mapping and I get StackOverflow when mapping from domain to entity or from entity to domain because of the bidirectional reference that always gets called on -> infinite loop scenario. How can I use Mapstruct for this scenario?

class User {
  private UserProfile userProfile;
}

class UserProfile {
 private User user;
}

@Entity
class UserEntity {
  @OneToOne
  @PrimaryKeyJoinColumn
  private UserProfileEntity userProfile;
}

@Entity
class UserProfileEntity {
  @OneToOne(mappedBy = "userProfile")
  private UserEntity userEntity;
}

class for mapping is pretty basic

@Mapper
interface UserMapper {

UserEntity mapToEntity(User user);

User mapToDomain(UserEntity userEntity);
}
2dor
  • 851
  • 3
  • 15
  • 35

1 Answers1

18

Check out the Mapstruct mapping with cycles example.

A solution to your problem is also demonstrated in the documentation for Context annotation.

Example

A complete example: https://github.com/jannis-baratheon/stackoverflow--mapstruct-mapping-graph-with-cycles.

Reference

Mapper:

@Mapper
public interface UserMapper {

    @Mapping(target = "userProfileEntity", source = "userProfile")
    UserEntity mapToEntity(User user,
                           @Context CycleAvoidingMappingContext cycleAvoidingMappingContext);

    @InheritInverseConfiguration
    User mapToDomain(UserEntity userEntity,
                     @Context CycleAvoidingMappingContext cycleAvoidingMappingContext);

    @Mapping(target = "userEntity", source = "user")
    UserProfileEntity mapToEntity(UserProfile userProfile,
                                  @Context CycleAvoidingMappingContext cycleAvoidingMappingContext);

    @InheritInverseConfiguration
    UserProfile mapToDomain(UserProfileEntity userProfileEntity,
                            @Context CycleAvoidingMappingContext cycleAvoidingMappingContext);
}

where CycleAvoidingMappingContext keeps track of the already mapped objects and reuses them avoiding the stack overflow:

public class CycleAvoidingMappingContext {
    private final Map<Object, Object> knownInstances = new IdentityHashMap<>();

    @BeforeMapping
    public <T> T getMappedInstance(Object source,
                                   @TargetType Class<T> targetType) {
        return targetType.cast(knownInstances.get(source));
    }

    @BeforeMapping
    public void storeMappedInstance(Object source,
                                    @MappingTarget Object target) {
        knownInstances.put(source, target);
    }
}

Mapper usage (mapping single object):

UserEntity mappedUserEntity = mapper.mapToEntity(user, new CycleAvoidingMappingContext());

You can also add a default method on your mapper:

@Mapper
public interface UserMapper {

    // (...)

    default UserEntity mapToEntity(User user) {
        return mapToEntity(user, new CycleAvoidingMappingContext());
    }

    // (...)
}
jannis
  • 4,843
  • 1
  • 23
  • 53
  • 1
    I liked the default method with reference to `return mapToEntity(user, new CycleAvoidingMappingContext());`. However this resulted in a [ambiguous](https://mapstruct.org/faq/#How-to-solve-ambiguous-methods) error message. I had to add a `@Qualifier` like described [here](https://mapstruct.org/faq/#How-to-avoid-MapStruct-selecting-a-method). I added `@DoIgnore` to all my new default methods. After that everything was ok. – Avec Apr 14 '21 at 06:00
  • Declaring a mapper as an abstract class which contains "knownInstances" field and the other two methods helps removing the need for instantiation of CycleAvoidingMappingContext class on each mapper method invocation. CycleAvoidingMappingContext's methods are generic and could be defined in a base-class which is inherited. – Jamali Apr 25 '22 at 09:12
  • more info about default method from @Avec made perfect answer more perfect thx! – Implermine Feb 03 '23 at 07:14