简体   繁体   中英

Detect exception in AutoCloseable close()

I want to build a custom AutoCloseable class so I can turn this:

try {
    begin();
    doThings();
    commit();
} finally {
    if (transactionIsActive()) rollback();
}

into the easier

try (Transaction t = begin()) { // too bad I have to store it in t though I don't use it
    doThings();
}

Transaction would be the AutoCloseable here and in close() it would commit or rollback the transaction as appropriate.

But to make that work, I would need to detect in Transaction.close() whether an exception occurred inside the try block or it completed normally. Is this possible at all?

If it requires parsing the stack trace from a new exception, that's OK. Easier programming would be worth the tiny performance hit that brings.

The closest I could come up with still requires marking the success of the transaction manually as the last statement of the block:

class Transaction implements AutoCloseable {
    private boolean rollback = true;

    public void success() {
        rollback = false;
    }

    public void close() {
        if (rollback) doRollback();
        else doCommit();
        // …
    }
}

class Main {
    public static void main(String[] args) {
        try (Transaction t = new Transaction()) {
            doThings();
            t.success();
        }
    }
}

Although my code is different from yours, I had a similar need to automatically commit (most) transactions and rollback on errors.

Most of the time my code is sprinkled with simple queries that get rolled back automatically, like this:

try(Transaction t : database.beginTransaction()) {
  return t.selectUnique(Employee.class, "id=?", 200);
}  // implicit rollback here

Some databases donot like queries being rolled back like this, so I've solved this by distinguishing between "write" and "read" transactions. If it is a read transaction, close() will commit it, otherwise rollback. It will also check you are not doing any write actions when you created a read-only transaction. So now I can write:

try(Transaction t : database.beginReadOnlyTransaction()) {
  return t.selectUnique(Employee.class, "id=?", 200);
}  // implicit commit here

Write transactions still need to call commit themselves at the end, but this is a minority of cases.

I realize this is not what you asked for, but perhaps it is useful still.

The closest I've been able to get is to explicitly call commit(), and assume that any code that exits the transaction block without doing so should be rolled back. This is consistent with transactions in other languages. While you can forget to call commit() (as I often do), at least this part of the code is very likely to get tested. And, it is impossible to forget to rollback on exception, which is less likely to have test coverage.

This is similar to millimoose's idea of setting a flag:

try (Transaction t = new Transaction()) {
    doThings();
    t.success();
}

except that you just use the active state as the flag. Same amount of code, no new flag necessary. This assumes that any transaction for which commit() has not been explicitly called should be rolled back, resulting in code like this:

try (Transaction t = new Transaction()) {
    doThings();
    t.commit(); // marks the transaction as successful...
}

class Transaction implements AutoCloseable {
    public void close() {
        if (isActive())
            doRollback();
    }

    ...
}

I still can't believe there is not a cleaner solution to this in the core language.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM