Zaczniemy od zdefiniowania czym są transakcje i standardowego przykładu z bankomatem. Najprościej rzecz ujmując transakcja to zestaw kroków danego algorytmu, który musi być wykonany w całości lub, jeśli wystąpi błąd w którymkolwiek z kroków, każda operacja musi zostać „cofnięta”, a stan systemu zostanie przywrócony do momentu sprzed rozpoczęcia pierwszego kroku w transakcji. Zacznijmy od oklepanego przykładu konta bankowego i wypłaty środków z bankomatu.
![]()
I tak j,eśli dwa kroki z tego algorytmu – zmiana stanu konta i wypłata gotówki – nie byłyby „owinięte” w transakcje to mogłoby dojść do dość nieprzyjemnej sytuacji, jeśli w bankomacie nie byłoby gotówki – stan konta zostałby zmniejszony, natomiast nie otrzymalibyśmy gotówki z bankomatu.
![]()
Jeśli użyjemy transakcji i wywołamy w niej te dwa kroki to, gdy nie będziemy mieli środków na koncie, bądź w bankomacie zabraknie gotówki – transakcja zostanie oznaczona jako 'failed’ i stan naszego konta wróci do momentu kiedy to deklarowaliśmy, że chcemy wypłacić gotówkę – czyli nic z naszego konta nie ubędzie.
Zaimplementujmy ten prosty przykład.
Utwórzmy w IntelliJ (czy jakimkolwiek innym IDE) nowy projekt javowy, wspierany przez maven’a.
Zacznijmy od utworzenia klasy dla konta bankowego
package domain;
public class Account {
private int balance;
public Account(int balance) {
this.balance = balance;
}
public void withdraw(int amount) {
this.balance-=amount;
}
public void deposit(int amount) {
this.balance+=amount;
}
@Override
public String toString() {
return "Aktualny stan konta: " + this.balance;
}
}
oraz dla bankomatu
package domain;
public class CashMachine {
private int cashAmount;
public CashMachine(int cashAmount) {
this.cashAmount = cashAmount;
}
public void withdraw(int amount) {
if(this.cashAmount-amount<0) {
throw new IllegalStateException("Brak dostatecznej ilosci gotówki.");
}
this.cashAmount-=amount;
}
@Override
public String toString() {
return "Pozostała gotówka: " + this.cashAmount;
}
}
Następnie w klasie głównej programu odegrajmy przypadek pozytywny
import domain.Account;
import domain.CashMachine;
public class App {
public static void main(String[] args) {
Account account = new Account(100);
CashMachine machine = new CashMachine(50);
System.out.println(account + System.lineSeparator() + machine);
account.withdraw(20);
machine.withdraw(20);
System.out.println(account + System.lineSeparator() + machine);
}
}
Wynik operacji jest dokładnie taki jaki byśmy chcieli
Aktualny stan konta: 100 Pozostała gotówka: 50 Aktualny stan konta: 80 Pozostała gotówka: 30
Czyli zmniejszył nam się stan konta, w bankomacie zostało mniej gotówki, a my mamy grubszy portfel. Co natomiast jeśli podamy zbyt dużą ilość gotówki do wypłaty?
import domain.Account;
import domain.CashMachine;
public class App {
public static void main(String[] args) {
Account account = new Account(100);
CashMachine machine = new CashMachine(50);
System.out.println(account + System.lineSeparator() + machine);
try {
account.withdraw(60);
machine.withdraw(60);
} catch (Exception e) {
System.out.println(e.getMessage());
}
System.out.println(account + System.lineSeparator() + machine);
}
}
Efekt działania tego przypadku jest taki, że zmienia nam się bilans konta, jednak gotówki nie otrzymujemy, zostanie ona w bankomacie.
Aktualny stan konta: 100 Pozostała gotówka: 50 Brak dostatecznej ilosci gotówki. Aktualny stan konta: 40 Pozostała gotówka: 50
Jak już wspomniałem mogą nam tu pomóc transakcje. Chcemy, by w wypadku niepowodzenia tej operacji stan systemu wrócił do stanu przed wykonaniem operacji.
W tym wypadku stan naszego systemu jest dość banalny – są to dwa obiekty. Jeden odpowiadający za konto, drugi za bankomat. Wystarczy więc, że zapamiętamy ich stan przed wykonaniem tych metod (czyli ile pieniędzy jest gdzie) i przywrócimy go w razie wystąpienia wyjątku. Rozbudujmy klasy Account i CashMachine o takie zwracające kopie danego obiektu oraz odtwarzają stan obiektu na podstawie tejże kopii.
package domain;
public class Account {
//...
public Account copy() {
return new Account(this.balance);
}
public void recreate(Account account) {
this.balance = account.balance;
}
}
I podobnie dla CashMachine
package domain;
public class CashMachine {
//...
public CashMachine copy() {
return new CashMachine(this.cashAmount);
}
public void recreate(CashMachine machine) {
this.cashAmount = machine.cashAmount;
}
}
Zaimplementujmy teraz proces wypłaty z bankomatu tak by był transakcyjny, czyli wykonał się w całości bądź wcale.
import domain.Account;
import domain.CashMachine;
public class App {
public static void main(String[] args) {
Account account = new Account(100);
CashMachine machine = new CashMachine(50);
System.out.println(account + System.lineSeparator() + machine);
transactionalWithdrawal(account, machine, 60);
System.out.println(account + System.lineSeparator() + machine);
}
private static void transactionalWithdrawal(Account account, CashMachine machine, int amount) {
Account accountBackup = account.copy(); //1
CashMachine machineBackup = machine.copy(); //2
try { //3
account.withdraw(amount);
machine.withdraw(amount);
} catch (Exception e) {
System.out.println(e.getMessage()); //4
System.out.println("Coś poszło nie tak. Przywracam stan systemu");
account.recreate(accountBackup);
machine.recreate(machineBackup);
}
}
}
Dla większej przejrzystości kodu przeniosłem te procedurę do osobnej metody.
W pierwszych dwóch krokach (1,2) tworzymy kopie obiektów, na których będziemy działać tak by gdyby coś poszło nie tak mieć stan systemu sprzed rozpoczęcia wykonywania operacji transakcyjnej.
W cześci 'try’ (3) bloku 'try – catch’ wykonujemy operacje, które chcemy, by spięte były transakcją – w naszym wypadku zmiana stanu konta i wypłata gotówki z bankomatu. Jeśli wszystko pójdzie dobrze i nie zostanie rzucony żaden wyjątek wykonywanie metody 'transactionalWithdrawal’ tutaj się zakończy.
Natomiast jeśli pojawi się jakikolwiek wyjątek (zauważ, że łapiemy każdy 'Exception’, a nie tylko IllegalStateException rzucany przez klase CashMachine – więc gdyby oprogramować dodatkowe problemy np. brak środków już na koncie bankowym to również zostaną one przechwycone) to odpalony zostanie blok catch (4) i przywrócimy stan naszego konta bankowego i bankomatu z naszych 'backupów’ wykonanych zanim zaczęliśmy cokolwiek zmieniać.
I to tyle jeśli chodzi o transakcje w czystej postaci. Bez żadnych baz danych, JPA, JTA, Springa czy innych skomplikowanych rzeczy, o których w kolejnych wpisach.
Kod przykładu znajduje się na Githubie
hej 🙂 szczerze nie podoba mi się ten kod na stronie strasznie chaotycznie to wygląda może jakieś formatowanie tego tekstu się przyda??
Cześć! Dzięki za zwrócenie uwagi. Formatowanie postu nie przetrwało zmian w wersjach wordpresa/pluginu i jak zauważyłeś – rozjechało się zupełnie. Już poprawione. Dzięki jeszcze raz!