Mechanizm transakcji umożliwia zbudowanie skomplikowanej operacji biznesowej składającej się z wielu kroków, której wynik ma być spójny. W przypadku wystąpienia błędu na którymkolwiek etapie możemy przywrócić obiekty do stanu sprzed rozpoczęcia operacji. W skrócie można opisać to tak: w przypadku powodzenia zapiszą się wszystkie zmiany, w przypadku błędu – wszystkie zmiany zostaną wycofane.
Programista tworząc kod operacji składającej się z wielu etapów może też zdecydować aby w przypadku wystąpienia błędu nie wszystkie zmiany były wycofywane. Zależy to od wymagań biznesowych stawianych przed aplikacją. Do tego celu może on wykorzystać odpowiednią propagację transakcji.
Typy propagacji:
- REQUIRED – jeżeli transakcja istnieje – użycie jej, w przeciwnym przypadku utworzenie nowej,
- SUPPORT – jeżeli transakcja istnieje – użycie jej, w przeciwnym przypadku metoda jest wykonywania bez transakcji,
- MANDATORY – jeżeli transakcja istnieje – użycie jej, w przeciwnym przypadku rzucenie wyjątku,
- REQUIRES_NEW – tworzy nową transakcję, jeżeli istnieje inna transakcja – jest ona zawieszana,
- NOT_SUPPORTED – wykonanie metody bez transakcji. Jeżeli transakcja istnieje – jest ona zawieszana,
- NEVER – wykonanie metody bez transakcji. Jeżeli transakcja istnieje – rzucenie wyjątku,
- NESTED – wykonanie metody w zagnieżdżonej transakcji, jeżeli taka istnieje. W przeciwnym przypadku zachowanie jest podobne to REQUIRED.
Przykładowa aplikacja
W tym artykule przedstawiam aplikację w której użyłem propagacji typu REQUIRED (kod dostępny na githubie). Aplikacja zawiera następujące obiekty:
- produkty
- magazyny
- stany w magazynach
- zamówienia
- dokumenty sprzedaży
Aplikacja zawiera tylko metody niezbędne do przedstawienia działania mechanizmu transakcji, dlatego nie ma w niej np. metod CRUD dotyczących produktów. Wejściem aplikacji jest metoda wykonująca operację realizacji zamówienia. Operacja ta składa się z następujących etapów:
- wczytanie danych zamówienia
- utworzenie nagłówka dokumentu sprzedaży
- dla każdej pozycji zamówienia utworzenie odpowiedniej pozycji dokumentu sprzedaży i zmniejszenie stanu magazynowego produktu
- ustawienie statusu w zamówieniu na wartość ZREALIZOWANE

W celu sprawdzenia poprawnego działania aplikacji utworzyłem dwa scenariusze testowe:
Scenariusz pierwszy to sytuacja kiedy zamówienie jest zrealizowane poprawnie – zmiany są zapisywane do bazy. W teście sprawdzam czy dane po zakończonej operacji są zgodne z oczekiwanymi wartościami.
void orderCompletedSuccessfully() throws Exception {
//given
long orderId=1;
OrderStatus orderStatusBefore = orderRepository.findById(orderId).get().getStatus();
OrderStatus expectedOrderStatus = OrderStatus.COMPLETED;
int expectedStockQuantityProductId1 = 8;
int expectedStockQuantityProductId2 = 10;
int expectedStockQuantityProductId3 = 10;
long orderItemNumber = orderItemRepository.count();
long expectedInvoiceItemNumber = 3;
long expectedInvoiceNumber = 1;
//when
ResultActions perform = mockMvc.perform(
MockMvcRequestBuilders.get("/clientorder/"+orderId+"/completed")
.contentType(MediaType.TEXT_PLAIN))
.andExpect(status().isOk());
//then
assertAll(
() -> assertEquals(
orderItemNumber,
orderItemRepository.count(), "order items count"),
() -> assertEquals(
OrderStatus.APPROVED,
orderStatusBefore),
() -> assertEquals(
expectedOrderStatus,
orderRepository.findById(orderId).get().getStatus(), "order status"),
() -> assertEquals(
expectedInvoiceItemNumber,
invoiceItemRepository.count(), "invoice items count"),
() -> assertEquals(
expectedInvoiceNumber,
invoiceRepository.count(), "invoice count"),
() -> assertEquals(
expectedStockQuantityProductId1,
stockQuantityRepository.findByStockIdAndProductId(1L, 1L).get().getQuantity(),
"stock quantity for product 1"),
() -> assertEquals(
expectedStockQuantityProductId2,
stockQuantityRepository.findByStockIdAndProductId(1L, 2L).get().getQuantity(),
"stock quantity for product 2"),
() -> assertEquals(
expectedStockQuantityProductId3,
stockQuantityRepository.findByStockIdAndProductId(1L, 3L).get().getQuantity(),
"stock quantity for product 3")
);
}
Drugi scenariusz to sytuacja kiedy ilość produktu w pozycji numer 2 zamówienia jest większa niż stan na magazynie. W tej sytuacji jest wyrzucany wyjątek a wszystkie wprowadzone zmiany są wycofywane. W teście sprawdzam, czy po zakończonej operacji dane powróciły do stanu pierwotnego.
void insufficientQuantityInOneOrderPosition() throws Exception {
//given
long orderId=1;
OrderStatus orderStatus = orderRepository.findById(orderId).get().getStatus();
int stockQuantityProductId1 = stockQuantityRepository
.findByStockIdAndProductId(1L, 1L).get().getQuantity();
int stockQuantityProductId2 = stockQuantityRepository
.findByStockIdAndProductId(1L, 2L).get().getQuantity();
int stockQuantityProductId3 = stockQuantityRepository
.findByStockIdAndProductId(1L, 3L).get().getQuantity();
long orderItemNumber = orderItemRepository.count();
long invoiceItemNumber = invoiceItemRepository.count();
long invoiceNumber = invoiceRepository.count();
//when
ResultActions perform = mockMvc.perform(
MockMvcRequestBuilders.get("/clientorder/"+orderId+"/completed")
.contentType(MediaType.TEXT_PLAIN))
.andExpect(status().isUnprocessableEntity());
//then
assertAll(
() -> assertEquals(
orderItemNumber,
orderItemRepository.count(), "order items count"),
() -> assertEquals(
orderStatus,
orderRepository.findById(orderId).get().getStatus(), "order status"),
() -> assertEquals(
invoiceItemNumber,
invoiceItemRepository.count(), "invoice items count"),
() -> assertEquals(
invoiceNumber,
invoiceRepository.count(), "invoice count"),
() -> assertEquals(
stockQuantityProductId1,
stockQuantityRepository.findByStockIdAndProductId(1L, 1L).get().getQuantity(),
"stock quantity for product 1"),
() -> assertEquals(
stockQuantityProductId2,
stockQuantityRepository.findByStockIdAndProductId(1L, 2L).get().getQuantity(),
"stock quantity for product 2"),
() -> assertEquals(
stockQuantityProductId3,
stockQuantityRepository.findByStockIdAndProductId(1L, 3L).get().getQuantity(),
"stock quantity for product 3")
);
}
Kod aplikacji zamieściłem na githubie.