0

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

Mike
  • 41
  • 2

1 Answers1

0

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.

Mike
  • 41
  • 2