Transakcje w springu

https://docs.spring.io/spring-framework/docs/4.2.x/spring-framework-reference/html/transaction.html#tx-propagation

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.