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.