I want to write a very lightweight library method, that will take any number of java lambda functions, execute them with auto-commit turned off. Should an error occur it would rollback. So the lambda functions themselves could have code that deals with db updates among other things.
I have been simply using a function closure and passing it to code that does the db logic to make it transactional. Looking for a way to genericize this, so all I would do is simply pass a set of lambda functions, and expect any db updates in the code be transactional.
Any advise on how to approach this, or if I'm thinking about the problem in the correct fashion?
What you're making already exists - see JDBI for example.
The general idea is good. Some of the key things you should take care of in your lambdas:
.getSQLState()
on the SQLException (which you should catch in your method that accepts a lambda) and figure it out. If it's that, just.. rerun the lambda until it no longer throws that.This is all a bit complicated. Which is why.. you should just use JDBI.
Let's first write a very basic take on your idea so we know what we're talking about:
// Given, from your library:
@FunctionalInterface
public interface DbRunner {
public void proc(Connection con) throws SQLException;
}
public class DbAccess {
private final Connection con = ...;
// con is in SERIALIZABLE, non-auto commit mode.
public void q(DbRunner runner) throws SQLException {
int attempts = 0;
while (true) {
try {
runner.proc(con);
con.commit();
break;
} catch (SQLException e) {
if (!isRetryException(e)) throw e;
if (attempts > 50) throw new SQLException("Too many retries", e);
long wait = rnd.nextInt(20 * attempts);
Thread.sleep(wait);
attempts++;
}
}
}
}
// Note: This is an extremely oversimplified bare bones impl of your idea!
There is no reason for this. Because you can easily construct a single lambda that does multiples. We can give DbRunner
utility method to do this - you can add default
methods whilst still keeping it a functional interface:
@FunctionalInterface
public interface DbRunner {
public void proc(Connection con) throws SQLException;
public default DbRunner andThen(DbRunner next) throws SQLxception {
return con -> {
proc(con);
next.proc(con);
};
}
}
Then you can just make a new dbrunner out of 2 runners:
DbRunner taskA = ...;
DbRunner taskB = ...;
DbRunner doBoth = taskA.andThen(taskB);
That.. doesn't work, and doesn't make sense. Whatever parameters your dbrunner needs will have to come from elsewhere - your db framework has no idea how to supply them. Note that any variables available from 'outside' can be used 'inside' (except non-effectively-final local variables). For example:
void markUserLogin(String username) throws SQLException {
dbAccess.q(db -> {
db.executeStatement("UPDATE users SET lastLogin = NOW() WHERE username = ? ", username);
});
}
Note how the username
param is available 'inside'. No need to have the function do it.
You can't. Not how java works.
The UPDATE statement in the last snippet isn't actually how JDBC works; you need a lot more code. But it can be that simple - if you're already handwriting a retry runner, you'll probably end up adding this too (in which case, your lambda won't receive a java.sql.Connection
object at all, it'll receive some 'Db' representing object from your library which contains the connection but has all sorts of far nicer methods to do DB operations safely. Except.. that, too, already exists - JDBI or JOOQ both do pretty much this. All of this: Runners for, amongst other things, retry purposes, and much nicer API.