Transactions#
There are no annotations. What you wrapped is the transaction.
try (DBTransaction transaction = new DBTransaction(db)) {
transaction.beginTransaction();
long id = db.insert(
SQL.insert(Post.instance())
.value(Post.title, request.getString("title"))
.value(Post.body, request.getString("body"))
.value(Post.image_name, request.getStringOptional("image_name"))
.value(Post.published, request.getBoolean("published"))
.value(Post.created_at, new Date())
);
if (id <= 0) {
transaction.rollbackEndTransaction();
return -1;
}
new NoticeExecutor().put(db, new Data()
.putData("post_id", id)
.putData("title", request.getString("title")));
transaction.commitEndTransaction();
/*
* コミットしてから流す。
* 先に流すと、ロールバックしたときに
* 「入っていない記事のお知らせ」だけが届く。
*/
PostFeedHandler.notifyNewPost(request.getStringOptional("title"));
return id;
}
Which one to call#
| Method | What it does |
|---|---|
beginTransaction() |
Starts one. Does nothing if one is already open |
commit() |
Commits. The transaction continues |
commitEndTransaction() |
Commits and ends |
rollback() |
Rolls back. The transaction continues |
rollbackEndTransaction() |
Rolls back and ends |
close() |
Rolls back if still open |
Watch the difference between commit() and commitEndTransaction().
commit() means "settle what has happened so far, and carry on".
In the code this was ported from, commit() ended the transaction internally, which
left a hole: everything after it silently became auto-commit. jimble fixes that.
Forgetting to close#
Call beginTransaction() without try-with-resources and return partway through, and
nothing is rolled back and the connection never goes back to the pool.
jimble catches it at the end of the execution (Context).
ERROR コミットもロールバックもされていないトランザクションが残っていました。ロールバックして閉じます: blog_example
Rolling back silently would leave you with "I thought I saved it and it is not there", so it logs an ERROR first and then rolls back. If you see this line, you forgot to wrap something.
This is not limited to DBTransaction. Calling db.beginTransaction() directly is
caught the same way — the DB is what holds the connection, so that is where it is watched.
An unclosed DB#
An ordinary statement does not leak when you forget to close the DB. The connection
goes back to the pool as soon as the statement finishes, which is what makes the throwaway
DBUtil.getMainDB() style fine.
There are only two ways to leave with the connection held, and both are caught at the end of the execution.
| What holds it | When it comes back |
|---|---|
| An open transaction | commitEndTransaction() / rollbackEndTransaction() / close() |
A cursor (selectListWithFetcher) |
db.close() |
ERROR 閉じられていない DB が残っていました。閉じます: blog_example(selectListWithFetcher はカーソルなので、close() までコネクションを返しません)
Trap
This is a last line of defence. By the time it fires an ERROR has been logged, so treat the line as something to fix rather than something to rely on. In a long-running batch the connection stays held until the execution ends.
The short form#
DBTransaction.transaction(db, transaction -> {
db.insert(...);
db.update(...);
});
It starts one, runs the work you passed in, and sees it through commitEndTransaction().
If an exception is thrown, close() rolls back.
Mixing in MQ#
You push onto the queue after the commit.
transaction.commitEndTransaction();
PostFeedHandler.notifyNewPost(title);
Publish first and a rollback leaves you delivering "a notice about an article that is not there". The other way round: pushing onto a DB-backed queue (MQ) belongs inside the transaction. It is the same DB, so a rollback removes what you pushed along with everything else.