Cyclic references when converting with MapStruct. Overflow error. Context does not work

104
June 30, 2022, at 6:10 PM

I have 2 entities, with 1-to-1 association (ProfileEntity and VCardEntity)

Entity vcard:

@Entity
@Table(name = "vcard")
@AllArgsConstructor
@NoArgsConstructor
@Data
@SequenceGenerator(name="vcard_id_seq_generator", sequenceName="vcard_id_seq", allocationSize = 1)
public class VCardEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "vcard_id_seq_generator")
    @Column(name="vcard_id")
    Long id;
    String account;
    @Column(name = "first_name")
    String firstName;
    @Column(name = "last_name")
    String lastName;
    @Column(name = "pbxinfo_json")
    String pbxInfoJson;
    @Column(name = "avatar_id")
    String avatarId;
    @OneToOne(mappedBy = "vcard")
    ProfileEntity profile;
}

entity Profile:

@Entity
@AllArgsConstructor
@NoArgsConstructor
@Data
@Table(name = "profile")
public class ProfileEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    @Column(name = "profile_id")
    private Long profileId;
    private String account;
    @Column(name = "product_id")
    private String productId;
    
    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "vcard_id", referencedColumnName = "vcard_id")
    private VCardEntity vcard;
}

I use map struct as follow:

public class CycleAvoidingMappingContext {
    private Map<Object, Object> knownInstances = new IdentityHashMap<Object, Object>();
    @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(componentModel = "spring")
public interface EntityToProfile {
    ProfileEntity profileToEntity(Profile profile, @Context CycleAvoidingMappingContext context);
    Profile entityToProfile(ProfileEntity entity, @Context CycleAvoidingMappingContext context);
}
@Mapper(componentModel = "spring")
public interface EntityToVCard {
    VCard entityToVcard(VCardEntity entity, @Context CycleAvoidingMappingContext context);
    VCardEntity vcardToEntity(VCard vcard, @Context CycleAvoidingMappingContext context);
}

Finally i call mapping in my service:

@Service
@RequiredArgsConstructor
@Slf4j
public class DefaultChatService implements ChatService {
    private final ProfileRepository profileRepository;
    private final EntityToProfile entityToProfileMapper;
    private final EntityToVCard entityToVCardMapper;
    @Override
    public List<Profile> findAllProfile(Optional<Long> id) {
        if (id.isPresent()) {
            Optional<ProfileEntity> result = profileRepository.findById(id.get());
            if (result.isPresent()) {
                Profile profile = entityToProfileMapper.entityToProfile(result.get(), new CycleAvoidingMappingContext());
                return Stream.of(profile).collect(Collectors.toList());
            }
        }
        return new ArrayList<Profile>();
    }
}

and i got the error ERROR 15976 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Handler dispatch failed; nested exception is java.lang.StackOverflowError] with root cause java.lang.StackOverflowError: null

Any thoughts how can i fix it? In my view i did everything as it's written here Prevent Cyclic references when converting with MapStruct but it doesn't work for me

Answer 1

Found a solution, all i had to do was to change from @Value to @Data for my models

example given:

@Data
public class Profile {
    Long profileId;
    String account;
    String productId;
    VCard vcard;
}

and

@Data
public class VCard {
    Long id;
    String account;
    String firstName;
    String lastName;
    String pbxInfoJson;
    String avatarId;
    Profile profile;
}

Otherwise mapstruct could not generate proper mapping code. It was trying to store an instance in knownInstances after creating an object, for example Profile. But because @value doesn't provide a way to set properties after creating the object (immutable object) it had to create all settings first and then use all args constructor which led to a mapping profile first which in turn was trying to do the same and map vcard first before storing the VCard object in knownInstances. This is why the cyclic reference problem could not be solved

Properly generated code:

public Profile entityToProfile(ProfileEntity entity, CycleAvoidingMappingContext context) {
        Profile target = context.getMappedInstance( entity, Profile.class );
        if ( target != null ) {
            return target;
        }
        if ( entity == null ) {
            return null;
        }
        Profile profile = new Profile();
        context.storeMappedInstance( entity, profile );
        profile.setAccount( entity.getAccount() );
        profile.setProductId( entity.getProductId() );
        profile.setDeviceListJson( entity.getDeviceListJson() );
        profile.setLastSid( entity.getLastSid() );
        profile.setBalanceValue( entity.getBalanceValue() );
        profile.setBalanceCurrency( entity.getBalanceCurrency() );
        profile.setStatusJson( entity.getStatusJson() );
        profile.setData( entity.getData() );
        profile.setMissedCallsCount( entity.getMissedCallsCount() );
        profile.setFirstCallSid( entity.getFirstCallSid() );
        profile.setLastMissedCallSid( entity.getLastMissedCallSid() );
        profile.setRemoveToCallSid( entity.getRemoveToCallSid() );
        profile.setOutgoingLines( entity.getOutgoingLines() );
        profile.setFeatures( entity.getFeatures() );
        profile.setPermissions( entity.getPermissions() );
        profile.setVcard( vCardEntityToVCard( entity.getVcard(), context ) );
        return profile;
    }
}

As you can see, firstly, it saves the object in context.storeMappedInstance( entity, profile ); and then fills the properties.

Rent Charter Buses Company
READ ALSO
Use Log4j1 and Log4j2 in the same project

Use Log4j1 and Log4j2 in the same project

I have a project in which Log4j1 is used all over the place, I upgraded to log4j2 but there is a jar depending on Log4j1 and I cannot change it

96
How to mock StorageOptions.newBuilder()

How to mock StorageOptions.newBuilder()

Can you help me in mocking StorageOptionsnewBuilder()

96
Reduce custom object using RxJava

Reduce custom object using RxJava

I'm using RxJava, I have a list of Point and i want to calculate the distance between each point and get the sum

99
Provide streaming API endpoint as event-stream or ndjson?

Provide streaming API endpoint as event-stream or ndjson?

Spring offers at least 2 possibilities to provide a streaming api response, which are very similar

112