0

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?

WesternGun
  • 11,303
  • 6
  • 88
  • 157
  • If it fails again, with the same exception, it means that you're already in a transaction when calling saveUserPurchase(). You shouldn't be: JPA exceptions are not recoverable, so the only thing you can do is to let spring rollback the transaction and close the session. Why don't you use the uuid generator instead of generating IDs by yourself? Also, including the ID in hashCode and equals() is generally incorrect. You'd better have no equals() and hashCode() at all. – JB Nizet Oct 29 '18 at 17:41
  • About the UUID generator you are right, this is a point, and the hashcode + equals part I haven't thought about that; about this can you give some references? So, back to the original problem, you mean I cannot do nothing here, if the original calling line is in the same transaction.But how does Spring determine where a transaction begins? Can I throw one level above? But there is no logic above. – WesternGun Oct 30 '18 at 08:35
  • https://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html#mapping-model-pojo-equalshashcode, https://docs.spring.io/spring/docs/current/spring-framework-reference/data-access.html#transaction – JB Nizet Oct 30 '18 at 10:15

0 Answers0