I want to increment and return a counter from a database table.
The java code is as follows:
String sqlUpdate = "UPDATE mytable SET col3 = col3 + 1 WHERE colpk1 = ? AND colpk2 = ?";
Query queryUpdate = manager.createNativeQuery(sqlUpdate);
queryUpdate.setParameter(1, ...);
queryUpdate.setParameter(2, ...);
int num = queryUpdate.executeUpdate();
if (num == 0) {
long count = 1;
String sqlInsert = "INSERT INTO mytable (colpk1, colpk2, col3) VALUES (?,?,?)";
Query queryInsert = manager.createNativeQuery(sqlInsert);
queryInsert.setParameter(1, ...);
queryInsert.setParameter(2, ...);
queryInsert.setParameter(3, count);
queryInsert.executeUpdate();
return count;
} else {
String sqlSelect = "SELECT col3 FROM mytable WHERE colpk1 = ? AND colpk2 = ?";
Query querySelect = manager.createNativeQuery(sqlSelect);
querySelect.setParameter(1, ...);
querySelect.setParameter(2, ...);
Object result = querySelect.getSingleResult();
return Long.parseLong(result.toString());
}
This works well also concurrently used (creates a lock) in case there is already a row with the given primary key. However, in case that row does not exist yet (num == 0), the UPDATE does not lock, and a concurrent access can happen in between the two queries, then leading to a Unique Constraint validation when executing the INSERT as the new row was already created in the meantime.
What’s the best way to solve this problem? Would it be better to use a SELECT FOR UPDATE first and then depending on the result doing an UPDATE or INSERT?
As Merge can throw the Unique Constraint exception in concurrent execution, the best solution was to catch the exception when executing the insert, then the row must be there already, and continue with the update then.
Getting this transaction to commit in case of container managed transactions was the next problem, as the exception lead to
isRollBackOnly == true. The way that worked was to use a new bean call for trying the insert within a new transaction, see Commit transaction after exception – undo setRollbackOnly