If I have some instances to insert or update in DB, like:
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@EqualsAndHashCode
@Table(name="user_purchases")
public class UserPurchase implements Serializable, Persistable<String> {
@Id
@Column(name="id")
@NotNull
private String id; // an UUID
@Column(name="user_id")
@NotNull
private String userId; // in user_info: "sub"
/**
* Seconds since epoch: when the last purchase happened.
*/
@Column(name="last_date")
private Date lastBuyDate;
@Transient
private boolean isNewObject;
// Add UUID before persisting
@PrePersist
public void prepareForInsert() {
this.id = UUID.randomUUID().toString();
}
@Override
public boolean isNew() {
return isNewObject;
}
// This part for Persistable is not required, because getId(), by accident,
// is the getter for the "id" field and returns a String.
//@Override
//public getId() {
// return id;
//}
}
We know that id
is surrogate id and will be generated before persisting. And userId
is unique in DB.
To know more about the interface Persistable<ID>
, check this answer.
Now, when we have an instance without id, the userId
could or not be duplicated in DB, and there is no way to tell if we are persisting or updating in DB.
I want to save a full-table scanning before every persisting/updating, so I am trying to catch DateIntegrationViolationException
after the first try of repository.save(entity)
.
@Transactional(rollbackFor = DataIntegrityViolationException.class)
public UserPurchase saveUserPurchase(UserPurchase purchase) throws RuntimeException {
UserPurchase saved = null;
try {
saved = repository.saveAndFlush(purchase);
log.debug("UserPurchase was saved/updated with id {}, last buy time: {}", saved.getId(),
DateTimeUtil.formatDateWithMilliPart(saved.getLastBuyDate(), false));
} catch (DataIntegrityViolationException e) {
log.info("Cannot save due to duplication. Rolling back..."); // we don't distinguish userId and id duplication here.
UserPurchase oldPurchase = repository.findByUserId(purchase.getUserId()); // <--------- here we cannot proceed
if (oldPurchase != null) {
purchase.setId(oldPurchase.getId()); // set the existent ID to do updating
purchase.setNewObject(false); // for Persistable<String>
saved = repository.saveAndFlush(purchase); // now should be updating
} else {
log.error("Cannot find ID by user id");
}
}
return saved;
}
This gives me the error of:
ERROR: current transaction is aborted, commands ignored until end of transaction
Because I am doing two things in one transaction, where the transaction shoule be rolled back.
OK, so I throw the exception, and tried to do the operation of updating outside(because Spring shall rollback automatically when it sees an exception is thrown, or so I read):
@Transactional(rollbackFor = DataIntegrityViolationException.class)
public UserPurchase saveUserPurchase(UserPurchase purchase) throws RuntimeException {
UserPurchase saved = null;
try {
saved = repository.saveAndFlush(purchase);
log.debug("UserPurchase was saved/updated with id {}, last buy time: {}", saved.getId(),
DateTimeUtil.formatDateWithMilliPart(saved.getLastBuyDate(), false));
} catch (DataIntegrityViolationException e) {
log.info("Cannot save due to duplication. Rolling back..."); // we don't distinguish userId and id duplication here.
throw e; // or throw new RuntimeException(e); is the same
}
return saved;
}
At where I call save()
:
try {
userPurchaseService.saveUserPurchase(purchase);
} catch (DataIntegrityViolationException e) {
log.info("Transaction rolled back, updating...");
// ... 1. select 2. getId() 3.setId() and save again
}
But, it fails again.
Now, with Spring Data, we have no EntityManager
to rollback()
.
What to do now? Must I do a manual findByUserId()
before every insert/update? My approach of lazy-selection wouldn't work under any circumstance?