Problem in updating the entity with unidirectional @ManyToOne relation in SpringBoot application

Viewed 90

I have two entities as below and the main problem is when I want to update the AccountRequestStatus entity. I save the integer enum code of AccountRequestStatusEnum in the database to persist the AcountRequest status in the whole application.

AccountRequestStatusEnum

public enum AccountRequestStatusEnum {

    INITIAL(0),
    SUCCESS(1);

    private final Integer type;

    AccountRequestStatusEnum(Integer type) {
        this.type = type;
    }

    public Integer getType() {
        return type;
    }

    public static AccountRequestStatusEnum of(Integer type) {
        for (AccountRequestStatusEnum accountRequestStatusEnum : AccountRequestStatusEnum.values()) {
            if (type.equals(accountRequestStatusEnum.getType()))
                return accountRequestStatusEnum;
        }
        return null;
    }

}

AccountRequest

@Entity
@Table(name = "T_ACCOUNT_REQUEST", uniqueConstraints = {@UniqueConstraint(columnNames = {"ACCOUNT_NO", "MESSAGE_ID"})})
@SequenceGenerator(
        name = "SEQ_T_ACCOUNT_REQUEST",
        sequenceName = "SEQ_T_ACCOUNT_REQUEST",
        allocationSize = 1)
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode(callSuper = false)
@ToString
public class AccountRequest extends AbstractAuditingEntity {

    private Long id;
    private String messageId;
    private String issuer;
    private EventType type;
    private EventName name;
    private String accountNo;
    private LocalDateTime dateTime;

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "SEQ_T_ACCOUNT_REQUEST")
    @Column(name = "ID", nullable = true, insertable = true, updatable = true, precision = 0)
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    @Column(name = "MESSAGE_ID")
    public String getMessageId() {
        return messageId;
    }

    public void setMessageId(String messageId) {
        this.messageId = messageId;
    }

    @Column(name = "ISSUER")
    public String getIssuer() {
        return issuer;
    }

    public void setIssuer(String issuer) {
        this.issuer = issuer;
    }

    @Transient
    public EventType getType() {
        return type;
    }

    public void setType(EventType type) {
        this.type = type;
    }

    @Column(name = "TYPE")
    public Integer getEventTypeCode() {
        if (Objects.nonNull(type)) {
            return type.getType();
        } else return null;
    }

    public void setEventTypeCode(Integer typeCode) {
        type = EventType.of(typeCode);
    }

    @Transient
    public EventName getName() {
        return name;
    }

    public void setName(EventName name) {
        this.name = name;
    }

    @Column(name = "NAME")
    public Integer getEventNameCode() {
        if (Objects.nonNull(name)) {
            return name.getType();
        } else return null;
    }

    public void setEventNameCode(Integer type) {
        name = EventName.of(type);
    }

    @Column(name = "ACCOUNT_NO")
    public String getAccountNo() {
        return accountNo;
    }

    public void setAccountNo(String accountNo) {
        this.accountNo = accountNo;
    }

    @Column(name = "DATE_TIME")
    public LocalDateTime getDateTime() {
        return dateTime;
    }

    public void setDateTime(LocalDateTime dateTime) {
        this.dateTime = dateTime;
    }
}

AccountRequestStatus


@Entity
@Table(name = "T_ACCOUNT_REQUEST_STATUS")
@SequenceGenerator(
        name = "SEQ_T_ACCOUNT_REQUEST_STATUS",
        sequenceName = "SEQ_T_ACCOUNT_REQUEST_STATUS",
        allocationSize = 1
)
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class AccountRequestStatus extends AbstractAuditingEntity {

    private Long id;
    private AccountRequestStatusEnum accountRequestStatusEnum;
    private AccountRequest accountRequest;

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "SEQ_T_ACCOUNT_REQUEST_STATUS")
    @Column(name = "ID", nullable = true, insertable = true, updatable = true, precision = 0)
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    @Transient
    public AccountRequestStatusEnum getAccountRequestStatusEnum() {
        return accountRequestStatusEnum;
    }

    public void setAccountRequestStatusEnum(AccountRequestStatusEnum accountRequestStatusEnum) {
        this.accountRequestStatusEnum = accountRequestStatusEnum;
    }


    @Column(name = "ACCOUNT_REQUEST_STATUS")
    public Integer getAccountRequestStatusCode() {
        if (Objects.nonNull(accountRequestStatusEnum)) {
            return accountRequestStatusEnum.getType();
        } else return null;
    }

    public void setAccountRequestStatusCode(Integer type) {
        accountRequestStatusEnum = AccountRequestStatusEnum.of(type);
    }

    @ManyToOne(targetEntity = AccountRequest.class)
    @JoinColumn(name = "ACCOUNT_REQUEST", referencedColumnName = "ID")
    public AccountRequest getAccountRequest() {
        return accountRequest;
    }

    public void setAccountRequest(AccountRequest accountRequest) {
        this.accountRequest = accountRequest;
    }
}

The first time that an account request comes from MQ my to application, I save the initial code of AccountRequestStatusEnum in service like below. This status persists properly and there is no problem, but when I want to update the AccountRequestStatus and add a new success code of AccountRequestStatusEnum (in another service) it won't be saved in DB.

This is the first service that is called after receiving the account request and saving the initial code.

@Service
@Transactional(readOnly = true)
public class AccountRequestServiceImpl implements IAccountRequestService {

    @Value("${mq.event_argument_key}")
    private String eventArgumentKey;

    private final AccountRequestRepository accountRequestRepository;
    private final AccountRequestStatusServiceImpl mqRequestStatusService;
    private final EventToAccountRequestEntityMapper eventMapper;
    private final AccountRequestMapper accountRequestMapper;

    @Autowired
    public AccountRequestServiceImpl(AccountRequestRepository accountRequestRepository,
                                     AccountRequestStatusServiceImpl mqRequestStatusService,
                                     EventToAccountRequestEntityMapper eventMapper,
                                     AccountRequestMapper accountRequestMapper) {
        this.accountRequestRepository = accountRequestRepository;
        this.mqRequestStatusService = mqRequestStatusService;
        this.eventMapper = eventMapper;
        this.accountRequestMapper = accountRequestMapper;
    }

    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)// to prevent rollback for whole receive method in mq service
    public void saveAccountRequest(Event event) {
        AccountRequest accountRequest = eventMapper.eventToAccountRequest(event, eventArgumentKey);
        accountRequestRepository.save(accountRequest);
        AccountRequestDto accountRequestDto = accountRequestMapper.toDto(accountRequest);
        saveAccountRequestStatus(accountRequestDto, AccountRequestStatusEnum.INITIAL);
    }

    private void saveAccountRequestStatus(AccountRequestDto accountRequestDto, AccountRequestStatusEnum status) {
        AccountRequestStatusDto accountRequestStatusDto = new AccountRequestStatusDto();
        accountRequestStatusDto.setAccountRequestStatusEnum(status);
        accountRequestStatusDto.setAccountRequestDto(accountRequestDto);
        mqRequestStatusService.saveAccountRequestStatus(accountRequestStatusDto);
    }
}

This is the second service that should save the success code of AccountRequestStatus.


@Service
@Transactional(readOnly = true)
public class SyncLegacyAccountServiceImpl implements ISyncLegacyAccountService {

    @Value("${mq.event_argument_key}")
    private String eventArgumentKey;
    @Value("${range.account_title_code}")
    private String accountTitleCode;

    private static final Logger log = LoggerFactory.getLogger(SyncLegacyAccountServiceImpl.class);

    private final AccountMapRepository accountMapRepository;
    private final AccountRequestRepository accountRequestRepository;
    private final CustomerRepository customerRepository;
    private final CustomerPersonRepository customerPersonRepository;
    private final CustomerCompanyRepository customerCompanyRepository;
    private final IMQService iMQService;
    private final AccountRequestStatusServiceImpl accountRequestStatusServiceImpl;
    private final GalaxyApi galaxyApi;
    private final RangeApi rangeApi;
    private final CustomerMapper customerMapper;
    private final InquiryMapper inquiryMapper;
    private final AccountRequestMapper accountRequestMapper;
    private final EventToAccountRequestEntityMapper eventToAccountRequestMapper;

    @Override
    public void handleSyncRequest(Event event) {
        saveSuccessfulAccountStatus(event); // ****** This is the main issue******
        try {
            CustomerAccountResponseDto galaxyData = getGalaxyData(event);
            Optional<AccountMapEntity> optAccountMapEntity = accountMapRepository.findByNewAccountNo(event.getArgument().get(eventArgumentKey).toString());
            if (!optAccountMapEntity.isPresent()) {
                //openAccount(event);
            } else {
                AccountMapEntity accountMapEntity = optAccountMapEntity.get();
                CustomerAccountResponseDto customerData = getCustomerData(accountMapEntity);
                // save in legacy

            }
        } catch (Exception exception) {
            handleEventRequestException(exception, event); 
        }
    }

    private void handleEventRequestException(Exception exception, Event event) {
        if (exception instanceof RangeServiceException) {
            log.error("Something went wrong with the Range service!");
            throw new RangeServiceException();
        } else if (exception instanceof GalaxySystemException) {
            log.error("Something went wrong with the Galaxy service!");
            NotifyAccountChangeResponse notifyAccountChangeResponse = MQUtil.buildAccountChangeResponse(new GalaxySystemException(), null, event.getMessageId());
            iMQService.send(notifyAccountChangeResponse);
            throw new GalaxySystemException();
        }
    }

    public void saveSuccessfulAccountStatus(Event event) {
        AccountRequest accountRequest = eventToAccountRequestMapper.eventToAccountRequest(event, eventArgumentKey);
        AccountRequestDto accountRequestDto = accountRequestMapper.toDto(accountRequest);
        saveAccountRequestStatus(accountRequestDto, AccountRequestStatusEnum.SUCCESS);
    }

    public void saveAccountRequestStatus(AccountRequestDto accountRequestDto, AccountRequestStatusEnum status) {
        AccountRequestStatusDto accountRequestStatusDto = new AccountRequestStatusDto();
        accountRequestStatusDto.setAccountRequestStatusEnum(status);
        accountRequestStatusDto.setAccountRequestDto(accountRequestDto);
        accountRequestStatusServiceImpl.saveAccountRequestStatus(accountRequestStatusDto);
    }
 
}

  • UPDATE

AccountRequestStatusServiceImpl

@Service
@Transactional(readOnly = true)
public class AccountRequestStatusServiceImpl implements IAccountRequestStatusService {

    private final AccountRequestStatusRepository accountRequestStatusRepository;
    private final AccountRequestStatusMapper accountRequestStatusMapper;

    @Autowired
    public AccountRequestStatusServiceImpl(AccountRequestStatusRepository accountRequestStatusRepository,
                                           AccountRequestStatusMapper accountRequestStatusMapper) {
        this.accountRequestStatusRepository = accountRequestStatusRepository;
        this.accountRequestStatusMapper = accountRequestStatusMapper;
    }

    @Override
    @Transactional
    public void saveAccountRequestStatus(AccountRequestStatusDto accountRequestStatusDto) {
        AccountRequestStatus accountRequestStatus = accountRequestStatusMapper.toAccountRequestStatus(accountRequestStatusDto);
        accountRequestStatusRepository.save(accountRequestStatus);
    }
}

AccountRequestDto

@Data
public class AccountRequestDto {
    private Long id;
    private String messageId;
    private String issuer;
    private EventType type;
    private EventName name;
    private String accountNo;
    private LocalDateTime dateTime;
}

AccountRequestStatusDto

@Data
public class AccountRequestStatusDto {

    private Long id;
    private AccountRequestStatusEnum accountRequestStatusEnum;
    private AccountRequestDto accountRequestDto;

}

AccountRequestStatusMapper

@Mapper(componentModel = "spring")
public interface AccountRequestStatusMapper extends EntityToDtoMapper<AccountRequestStatusDto, AccountRequestStatus>, DtoToEntityMapper<AccountRequestStatusDto, AccountRequestStatus> {
 

    @Mapping(target = "accountRequest.id", source = "accountRequestDto.id")
    @Mapping(target = "accountRequest.messageId", source = "accountRequestDto.messageId")
    @Mapping(target = "accountRequest.issuer", source = "accountRequestDto.issuer")
    @Mapping(target = "accountRequest.type", source = "accountRequestDto.type")
    @Mapping(target = "accountRequest.name", source = "accountRequestDto.name")
    @Mapping(target = "accountRequest.accountNo", source = "accountRequestDto.accountNo")
    @Mapping(target = "accountRequest.dateTime", source = "accountRequestDto.dateTime")
    @Named(value = "toAccountRequestStatus")
    AccountRequestStatus toAccountRequestStatus(AccountRequestStatusDto accountRequestStatusDto);

    @Mapping(target = "accountRequestDto.id", source = "accountRequest.id")
    @Mapping(target = "accountRequestDto.messageId", source = "accountRequest.messageId")
    @Mapping(target = "accountRequestDto.issuer", source = "accountRequest.issuer")
    @Mapping(target = "accountRequestDto.type", source = "accountRequest.type")
    @Mapping(target = "accountRequestDto.name", source = "accountRequest.name")
    @Mapping(target = "accountRequestDto.accountNo", source = "accountRequest.accountNo")
    @Mapping(target = "accountRequestDto.dateTime", source = "accountRequest.dateTime")
    @Named(value = "toAccountRequestStatusDto")
    AccountRequestStatusDto toAccountRequestStatusDto(AccountRequestStatus accountRequestStatus);

}
1 Answers

You are not setting the id field of accountRequestStatus before saving. How is Hibernate supposed to know which entity to update? I'm guessing it is just generating a new id since you don't provide one. You need to somehow get the id of the already created accountRequestStatus and set it before updating. Or alternatively get the persisted accountRequestStatus entity and modify it.

That being said, I think you may be overcomplicating things here. Why does AccountRequestStatus need to be an entity? It does not seem to have an identity itself, it seems more like an attribute of AccountRequest. Can't you just add an AccountRequestStatusEnum field in AccountRequest, being after all a one-to-one relationship? If you are enclosing the status in a class because you foresee it may have more fields in the future, consider making it @Embeddable instead of an @Entity.

The general rule to know if a class must be an @Entity is that it must have an identity. The status of a request does not look like a good candidate, since its identity can simply be that of the request. Having the status be an @Entity means you now have an artificial id, which, surprise, is the root cause of your problem right now. If you want to better understand this topic, I recommend reading into the difference between @Entity and @Embeddable, and if you are feeling theoretical, read a bit about DDD(domain driven design).

Related