Навіщо потрібні транзакції

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

Наприклад, ми пишемо банківське ПЗ, яке має зробити три речі:

  • Списати гроші з рахунку клієнта
  • Додати гроші на рахунок отримувача
  • Записати дані про проведення у “журнал проводок”

Якщо під час виконання будь-якої з цих дій виникне помилка, то решту двох потрібно також скасовувати. Не можна ж списати гроші у клієнта та не додати їх одержувачу? Ну чи додати одержувачу, але не списати у клієнта?

Так от, таке логічне угруповання різних дій в одне називається транзакцією . Іншими словами, транзакція – це група дій, які мають бути виконані лише всі разом . Якщо будь-яка дія не виконалася або виконалася з помилкою, всі інші дії повинні бути скасовані.

У транзакції зазвичай є три стани:

  • initial state - стан системи перед виконанням групи дій
  • success state - стан після виконання групи дій
  • failed state - щось пішло не так

При цьому зазвичай є три команди:

  • begin/start - виконується перед початком логічної групи дій
  • commit - виконується після групи дій транзакції
  • rollback - запускає процес повернення системи з failed state в initial state

Працює так.

Спочатку потрібно відкрити транзакцію – викликати метод begin() або start() . Виклик цього методу означає стан системи, якого ми спробуємо повернутися, якщо щось піде негаразд.

Потім виконуються всі дії, які об'єднані в логічну групу транзакцію.

Потім викликається метод commit() . Його виклик позначає кінець логічної групи дій, і навіть зазвичай запускає процес реалізації цих дій практично.

Згадай, як ми писали щось у FileWriter: спочатку все, що ми написали, зберігається у пам'яті, та був за виклику методу flush() всі дані з буфера у пам'яті пишуться на диск. Ось цей flush() — це коміт транзакції.

Ну, а якщо під час роботи транзакції сталася помилка, то потрібно ініціювати процес повернення до стартового стану. Цей процес називається rollback() і за нього зазвичай відповідає однойменний метод.

Грубо кажучи, є два способи завершити транзакцію:

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