Я хочу написать очень легкий библиотечный метод, который будет принимать любое количество лямбда-функций Java и выполнять их с отключенной автоматической фиксацией. В случае возникновения ошибки произойдет откат. Таким образом, сами лямбда-функции могут иметь код, который, помимо прочего, занимается обновлениями базы данных.
Я просто использовал замыкание функции и передал его коду, который выполняет логику базы данных, чтобы сделать ее транзакционной. Ищу способ обобщить это, поэтому все, что мне нужно сделать, это просто передать набор лямбда-функций и ожидать, что любые обновления базы данных в коде будут транзакционными.
Есть ли какие-нибудь советы о том, как подойти к этому, или правильно ли я думаю о проблеме?
То, что вы делаете, уже существует — см., например, JDBI.
Общая идея хорошая. Некоторые ключевые вещи, о которых вам следует позаботиться в ваших лямбда-выражениях:
.getSQLState()
для исключения SQLException (которое вы должны перехватить в своем методе, принимающем лямбда-выражение) и разобраться в нем. Если это так, просто... повторно запускайте лямбду, пока она не перестанет ее выдавать.Это все немного сложно. Вот почему... вам следует просто использовать JDBI.
Давайте сначала напишем очень простой взгляд на вашу идею, чтобы мы знали, о чем говорим:
// 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!
Для этого нет причин. Потому что вы можете легко создать одну лямбду, которая будет кратна. Для этого мы можем предоставить DbRunner
служебный метод — вы можете добавлять default
методы, сохраняя при этом функциональный интерфейс:
@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);
};
}
}
Тогда вы можете просто сделать новый dbrunner из двух бегунов:
DbRunner taskA = ...;
DbRunner taskB = ...;
DbRunner doBoth = taskA.andThen(taskB);
Это... не работает и не имеет смысла. Какие бы параметры ни нужны вашему dbrunner, они должны будут поступать откуда-то еще — ваша структура базы данных не знает, как их предоставить. Обратите внимание, что любые переменные, доступные «снаружи», могут использоваться «внутри» (кроме неэффективно окончательных локальных переменных). Например:
void markUserLogin(String username) throws SQLException {
dbAccess.q(db -> {
db.executeStatement("UPDATE users SET lastLogin = NOW() WHERE username = ? ", username);
});
}
Обратите внимание, что параметр username
доступен «внутри». Нет необходимости заставлять функцию делать это.
Вы не можете. Не так, как работает Java.
Инструкция UPDATE в последнем фрагменте на самом деле не соответствует тому, как работает JDBC; вам нужно гораздо больше кода. Но это может быть так просто: если вы уже пишете программу повторной попытки, вы, вероятно, в конечном итоге добавите и ее (в этом случае ваша лямбда вообще не получит объект java.sql.Connection
, она получит некоторый 'Db ' представляющий объект из вашей библиотеки, который содержит соединение, но имеет множество гораздо более приятных методов для безопасного выполнения операций с БД. За исключением... того, что тоже уже существует - JDBI или JOOQ почти все это делают: Runners for, среди прочего, цели повторной попытки и гораздо более удобный API.