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

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

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

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

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

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

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

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

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

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

Працює це так.

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

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

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

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

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

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

  • COMMIT — підтверджуємо усі внесені зміни
  • ROLLBACK — відкочуємо всі внесені зміни

2. Транзакції в 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();
}

3. Точки збереження

З появою 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 в іграх.