JDBC транзакции

transaction-guysКаждый, кто работал с базами данных, слышал про ACID и транзакции. Если кто не работал и не знает что это такое, то всё просто: транзакция это набор операций, которые могут быть либо целиком и успешно выполнены, либо полностью не выполнены. В IBM DB2 транзакции называют более логично Unit of Work (UoW) — единица работы.

Транзакции в базах данных соответствуют свойствам ACID:

  • Атомарность — транзакция может быть либо целиком выполнена, либо целиком отменена.
  • Согласованность — состояние данных должно быть логически согласованным после выполнения (или отмены) транзации
  • Изолированность — в процессе работы транзакции другие выполняющиеся в это время транзакции не влияют на неё.
  • наДёжность — что-бы не произошло, транзакция останется атомарной.

Если вдруг транзакции в вашей базе не соответствуют этим принципам, то это плохие, негодные транзакции и они не заслуживают чести носить это имя.

Использование транзакций в дикой природе проще показать на примере: положим у нас есть система, в которой клиенты размещают какие-то заказы. За каждый заказ со счёта клиента снимается какая-то сумма. Таким образом, одна логическая единица работы разбивается на три операции в базе данных:

  • Добавить заказ.
  • Получить текущее значение счёта клиента.
  • Уменьшить счёт клиента и обновить значение в базе.

Оборачивание этих трёх операций в одну транзакцию гарантирует нам, что либо мы добавим заказ и спишем деньги, либо ничего не произойдёт вообще. Третьего не дано.

Commit и Rollback

Пора переходить к коду. По умолчанию все соединения в устанавливаются в режиме auto commit, когда каждый отдельный запрос выполняется в своей собственной транзакции, которая автоматически подтверждается после завершения запроса. Если мы хотим управлять транзакциями вручную, а мы хотим, то в первую очередь надо выключить этот режим:

Первый же запрос, отправленный к базе после этой команды автоматически начнёт новую транзакцию. Чтобы её подтвердить, необходимо позвать метод commit() объекта соединения:

метод addOrder() делает ровно то, что я описал в примере выше: добавляет что-то к заказу и списывает деньги:

Откатывается транзакция методом rollback()

Изменения, внёсённые всеми запросами с начала транзакции и до вызова rollback(), откатываются на состояние до начала транзакции. Начинает транзакцию первый запрос, выполненный после commit(), rollback() или setAutoCommit(false)

Savepoints

Некоторые базы данных поддерживают Savepoints — транзакции внутри транзакций. Savepoint позволяет сохранить какое-либо состояние внутри транзакции и, при необходимости, откатиться к нему, не откатывая всю транзакцию. Подтвердить savepoint нельзя, они все автоматически подтверждаются с подтверждением транзакции.

Savepoint можно использовать, если у пользователя есть какое-то сложное взаимодействие с данными в пределах одной логической единицы работы. Например трёхшаговое подтверждение заказа: пользователь формирует заказ и в этот момент создаётся транзакция. Потом пользователь добавляет, например, адрес доставки и в этот момент создаётся Savepoint и со счёта пользователя сразу списываются деньги за доставку. Потом выбираются какие-нибудь дополнительные услуги, опять создаётся Savepoint и списываются деньги за эти услуги. Потом заказ подтверждается пользователем или отменяется пользователем, а мы делаем соответственно commit() или rollback(). Но, если пользователь решает отступить на шаг назад и изменить доставку, мы не отменяя всей транзакции отменяем только этот шаг.

Savepoint создаётся вызовом метода setSavepoint(), которому можно опционально передать какую-нибудь метку. Откатывается Savepoint методом rollback(). У откатывания Savepoint есть несколько стратегий, зависящих от конкретной реализации. Обычно все Savepoint внутри транзакции представляют собой стек, и если мы откатывает Savepoint из середины этого стека, то есть не верхний, то в зависимости от реализации в базе данных может:

  • Откатиться всё равно только верхний Savepoint, как в примере выше.
  • Откатиться все Savepoint’ы предшествующие заданному
  • Прилететь SQLException.

Старый, ненужный Savepoint можно закрыть методом releaseSavepoint(). Закрытый Savepoint нельзя откатывать и, в зависимости от реализации в базе данных, предшевствующие Savepoints тоже могут закрыться.

Код примера доступен на github.