Зачем нужны транзакции
Очень часто при работе с базой данных возникает ситуация, когда нужно выполнить много разных действий, но смысл они имеют только вместе.
Например, мы пишем банковское ПО, которое должно сделать три вещи:
- Списать деньги со счета клиента
- Добавить деньги на счет получателя
- Записать данные о проводке в “журнал проводок”
Если во время выполнения любого из этих действий возникнет ошибка, то остальные два нужно тоже отменять. Нельзя же списать деньги у клиента и не добавить их получателю? Ну или добавить получателю, но не списать у клиента?
Так вот, такая логическая группировка разных действий в одно называется транзакцией. Другими словами, транзакция — это группа действий, которые должны быть выполнены только все вместе. Если какое-либо действие не выполнилось или выполнилось с ошибкой, то все остальные действия должны быть отменены.
У транзакции обычно есть три состояния:
- initial state — состояние системы перед выполнением группы действий
- success state — состояние после выполнения группы действий
- failed state — что-то пошло не так

При этом обычно есть три команды:
- begin/start — выполняется перед началом логической группы действий
- commit — выполняется после группы действий транзакции
- rollback — запускает процесс возврата системы из failed state в initial state
Работает это так.
Сначала нужно открыть транзакцию — вызвать метод begin() или start(). Вызов этого метода обозначает состояние системы, к которому мы попробуем вернуться, если что-то пойдет не так.
Затем выполняются все действия, которые объединены в логическую группу — транзакцию.
Затем вызывается метод commit(). Его вызов обозначает конец логической группы действий, а также обычно запускает процесс реализации этих действий на практике.
Вспомни, как мы писали что-то в FileWriter: сначала все, что мы написали, сохраняется в памяти, а затем при вызове метода flush() все данные из буфера в памяти пишутся на диск. Вот этот flush() — это и есть коммит транзакции.
Ну а если во время работы транзакции произошла ошибка, то нужно инициировать процесс возврата к стартовому состоянию. Этот процесс называется rollback(), и за него обычно отвечает одноименный метод.
Грубо говоря, есть 2 способа завершить транзакцию:
- COMMIT — подтверждаем все внесенные изменения
- ROLLBACK — откатываем все внесенные изменения
Транзакции в JDBC
Практически каждая СУБД умеет работать с транзакциями. Так что и у JDBC поддержка этого дела тоже есть. Реализовано все очень просто.
Во-первых, каждый вызов метода execute() объекта Statement выполняется в отдельной транзакции. Для этого у Connection есть параметр AutoCommit. Если он выставлен в true, то commit() будет вызываться после каждого вызова метода execute().
Во-вторых, если ты хочешь выполнить несколько команд в одной транзакции, то сделать это можно так:
- отключаем AutoCommit
- вызываем наши команды
- вызываем метод commit() явно
Выглядит это очень просто:
connection.setAutoCommit(false);
Statement statement = connection.createStatement();
int rowsCount1 = statement.executeUpdate("UPDATE employee SET salary = salary+1000");
int rowsCount2 = statement.executeUpdate("UPDATE employee SET salary = salary+1000");
int rowsCount3 = statement.executeUpdate("UPDATE employee SET salary = salary+1000");
connection.commit();
Если во время работы метод commit() на сервере произойдет ошибка, то SQL-сервер отменит все три действия.
Но бывают ситуации, когда ошибка возникает еще на стороне клиента, и мы так и не дошли до вызова метода commit():
connection.setAutoCommit(false);
Statement statement = connection.createStatement();
int rowsCount1 = statement.executeUpdate("UPDATE employee SET salary = salary+1000");
int rowsCount2 = statement.executeUpdate("UPDATE employee SET salary = salary+1000");
int rowsCount3 = statement.executeUpdate("UPDATE несколько опечаток приведут к исключению");
connection.commit();
Если во время работы одного executeUpdate() произойдет ошибка, то метод commit() вызван так и не будет. Чтобы откатить все сделанные действия, нужно вызвать метод rollback(). Обычно это выглядит так:
try{
connection.setAutoCommit(false);
Statement statement = connection.createStatement();
int rowsCount1 = statement.executeUpdate("UPDATE employee SET salary = salary+1000");
int rowsCount2 = statement.executeUpdate("UPDATE employee SET salary = salary+1000");
int rowsCount3 = statement.executeUpdate("UPDATE несколько опечаток приведут к исключению");
connection.commit();
}
catch (Exception e) {
connection.rollback();
}
Точки сохранения
С появлением JDBC 3.0 появилась возможность более эффективно работать с откатом транзакции. Теперь можно устанавливать точки сохранения — save points, а при вызове операции rollback() откатываться к конкретной точке сохранения.
Для того, чтобы сохраниться, нужно создать точку сохранения, делается это командой:
Savepoint save = connection.setSavepoint();
Возврат к точке сохранения делается командой:
connection.rollback(save);
Давай попробуем добавить точку сохранения перед нашей проблемной командой:
try{
connection.setAutoCommit(false);
Statement statement = connection.createStatement();
int rowsCount1 = statement.executeUpdate("UPDATE employee SET salary = salary+1000");
int rowsCount2 = statement.executeUpdate("UPDATE employee SET salary = salary+1000");
Savepoint save = connection.setSavepoint();
try{
int rowsCount3 = statement.executeUpdate("UPDATE несколько опечаток приведут к исключению");
}
catch (Exception e) {
connection.rollback(save);
}
connection.commit();
}
catch (Exception e) {
connection.rollback();
}
Мы организовали как бы вложенные транзакции, добавив save-point перед вызовом проблемного метода, и возврат к сохраненному состоянию с помощью вызова метода rollback(save).
Да, это очень похоже на save/load в играх.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ