Зачем нужны транзакции

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

Например, мы пишем банковское ПО, которое должно сделать три вещи:

  • Списать деньги со счета клиента
  • Добавить деньги на счет получателя
  • Записать данные о проводке в “журнал проводок”

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

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

У транзакции обычно есть три состояния:

  • 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 в играх.

undefined
1
Задача
Модуль 4. Работа с БД, 8 уровень, 0 лекция
Недоступна
Работа с транзакциями
В методе main создай подключение к БД с помощью метода getConnection(String, String, String) класса DriverManager. Используй URL "jdbc:mysql://localhost:3306/test", пользователя "root" и такой же пароль. Отключи AutoCommit.
undefined
1
Задача
Модуль 4. Работа с БД, 8 уровень, 0 лекция
Недоступна
Откат транзакции
В методе main создай подключение к БД с помощью метода getConnection(String, String, String) класса DriverManager. Используй URL "jdbc:mysql://localhost:3306/test", пользователя "root" и такой же пароль. Отключи AutoCommit.