Przejdź do głównej zawartości

Spring Data - save vs saveAndFlush

Cześć, dzisiaj będzie znowu trochę o warstwie persystencji. Czasami kodzie aplikacji korzystającej ze Spring Data można napotkać użycia metody repozytorium save, a czasami saveAndFlush, a z kolei innym razem brak jakiejkolwiek z nich podczas zapisu obiektu. Wszystkie trzy metody mają swoje zastosowanie choć nieco się różnią.

Teoria

W teorii, różnica jest prosta. Metody save oraz saveAndFlush dodają obiekt do kontekstu persystencji danej sesji i zwracają obiekt zarządzany. Ponadto druga z nich wymusza wymusza wykonanie nagranych przez ORM akcji na bazie danych przez co dane zostają przesłane do silnika bazy. Może się to okazać przydatne w przypadku gdy w ramach jednej transakcji chcemy jeszcze wykonać kolejne zapytania, które mają być świadome wprowadzonych wcześniej zmian. Natychmiastowa synchronizacja może się również przydać gdy wykorzystujemy poziom izolacji READ_UNCOMMITTED.

Czasami jednak w kodzie nie ma żadnej z nich. Wtedy możliwe jest wykonywanie tylko zapytań UPDATE i tylko na obiektach zarządzanych przez ORM (czyli zapisanych przez jedną z opisanych wyżej metod lub pobranych za pomocą ORM - np. metodami find*). Dzieje się to w momencie commit-a transakcji. Tak więc w przypadku zapisu nowych obiektów (zapytania INSERT) należy użyć najpierw jednej z metod save*.

Praktyka

Warto sprawdzić czy opisane wyżej zachowanie jest zgodne z rzeczywistością. W tym celu posłuży oczywiście kod testowy. Poniżej przykładowa encja User.

@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column
    private String name;
}
I klasa testowa (pomijam już definicję repozytorium - po prostu rozszerzenie interfejsu JpaRepository):
@DataJpaTest
@TestPropertySource(locations = "classpath:application-it.properties")
class UserRepositoryIT {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private EntityManager entityManager;

    @Test
    void testUserRepository() {
        SessionImpl session = entityManager.unwrap(SessionImpl.class);
        ActionQueue actionQueue = session.getActionQueue();
        System.out.println("Insertions to sync: " + actionQueue.numberOfInsertions());
        User user = User.builder().name("mario").build();
        User savedUser = userRepository.save(user);
        System.out.println("Saved");
        System.out.println("Insertions to sync: " + actionQueue.numberOfInsertions());
        long count = userRepository.count();
        System.out.println("Insertions to sync: " + actionQueue.numberOfInsertions());
        assertEquals(0L, count);
    }

    @Test
    void testUserRepository2() {
        SessionImpl session = entityManager.unwrap(SessionImpl.class);
        ActionQueue actionQueue = session.getActionQueue();
        System.out.println("Insertions to sync: " + actionQueue.numberOfInsertions());
        User user = User.builder().name("mario").build();
        User savedUser = userRepository.saveAndFlush(user);
        System.out.println("Saved");
        System.out.println("Insertions to sync: " + actionQueue.numberOfInsertions());
        long count = userRepository.count();
        System.out.println("Insertions to sync: " + actionQueue.numberOfInsertions());
        assertEquals(1L, count);
        // savedUser.setName("mario2");
        // TestTransaction.flagForCommit();
    }
}
Pierwszy test wykorzystuje metodę save, a drugi saveAndFlush. Oczekiwany wynik wykonania funkcji na bazie danych w pierwszym wypadku to 0 jako, że nowy wiersz nie powinien zostać jeszcze zapisany po stronie bazy danych. W przypadku drugiego testu oczekiwana wartość to 1.

Niestety, pierwszy test zakończył się niepowodzeniem (a raczej powodzeniem bo wykrył problem 😎)! Wyjście obu testów niczym się nie różni:

Insertions to sync: 0
Hibernate: insert into users (name) values (?)
Saved
Insertions to sync: 0
2020-03-15 17:27:27.854  INFO 7000 --- [           main] o.h.h.i.QueryTranslatorFactoryInitiator  : HHH000397: Using ASTQueryTranslatorFactory
Hibernate: select count(*) as col_0_0_ from users user0_
Insertions to sync: 0

Flush mode

Problem tkwi w domyślnym zachowaniu Hibernate. JPA możliwość określenia trybu zarządzania flushami oferując 2 tryby (AUTO oraz COMMIT). Hibernate oczywiście robi to po swojemu oferując 4 tryby (MANUAL, COMMIT, AUTO, ALWAYS), które zdefiniowane są inaczej (szczególnie zwrócić należy uwagę w różnicy pomiędzy trybami AUTO). Domyślnym trybem w Hibernate jest tryb AUTO. W jego przypadku nie wiemy dokładnie kiedy wystąpi flush. To Hibernate decyduje kiedy go wykonać wybierając najbardziej sprzyjający moment, który zapewni, że dane będą aktualne. Jedną z alternatyw jest tryb COMMIT. Pozwala on odłożyć moment operacji flush do chwili gdy nastąpi commit transakcji, chyba, że został on wymuszony ręcznie. Tryb możemy zmienić za pomocą properties-a:
spring.jpa.properties.org.hibernate.flushMode=COMMIT
Wyjście pierwszego testu:
Insertions to sync: 0
Saved
Insertions to sync: 1
2020-03-15 17:31:31.921  INFO 13088 --- [           main] o.h.h.i.QueryTranslatorFactoryInitiator  : HHH000397: Using ASTQueryTranslatorFactory
Hibernate: select count(*) as col_0_0_ from users user0_
Insertions to sync: 1

Bonus

Aby przekonać się, że nie zawsze konieczne jest używanie metod save* zapraszam do przeprowadzenia testu odkomentowując linie 37 i 38 z kodu testowego. Domyślnie testy oznaczone adnotacją @DataJpaTest są wykonywane wewnątrz transakcji, która kończy się rollbackiem przez co update nie zostanie wykonany. W linii 38 zmieniamy to zachowanie oznaczając transakcję jako przeznaczoną do zacommitowania.

Wartość zwracana

Jako, że działanie metody save opiera się na wywołaniu metod EntityManager-a (persist lub merge) to warto zwrócić uwagę, że obiekt przez nią zwracany może nie być tym samym obiektem co podany jako parametr wywolania (to samo dotyczy saveAndFlush. Wynika to ze specyfiki metody merge.

Koniec

Mam nadzieję, że wyjaśniłem dość jasno podstawową różnicę między sposobami zapisu encji przy pomocy Spring Data. Zachęcam do poczytania więcej o trybach flushowania. Np tu: https://dzone.com/articles/dark-side-hibernate-auto-flush

Komentarze

Popularne posty z tego bloga

Wzorzec Open Session in View w Spring Boot

Dzisiejszy post będzie z cyklu: "Wtf? Dlaczego to działa?". A dotyczy on pewnego wzorca, którego implementacja jak się okazuje jest we frameworku Spring Boot domyślnie włączona, czego nie wszyscy programiści mogą się spodziewać. Geneza Zaczęło się od tego, że pisałem kolejny test integracyjny do kolejnego kontrolera. Test napisany, uruchamiam, zielono. No i przeglądam sobie jeszcze raz kod zanim wypchnę zmiany do review. I rzuciło mi się w oczy, że zapomniałem dodać adnotacji @Transactional (do tej pory uważałem, że to jest zawsze wymagane). I wtedy zadałem sobie przytoczone na wstępie pytanie. Dlaczego test przeszedł skoro mój serwis dociąga sobie obiekt oznaczony jako LAZY ? Dużo się nagimnastykowałem by wpisać odpowiednie query do Google'a i znaleźć odpowiedź: Open Session in View. Ale o tym za chwilę. Jak działa Hibernate Żeby lepiej zrozumieć opisywane zagadnienie warto przypomnieć jakie kroki w uproszczeniu musi wykonać Hibernate podczas komunikacji z bazą d

One-to-one i lazy loading

Hibernate jest frameworkiem, który nie przestaje zaskakiwać. Kryje się w nim mnóstwo tajemnic, pułapek i zagadek. Jedną taką pułapkę postaram się opisać w tym wpisie, a dotyczy ona nieoczywistego na pierwszy rzut oka zachowania adnotacji @OneToOne z parametrem fetchMode=LAZY . One-to-one Adnotacja @OneToOne służy w JPA do oznaczania pól encji, które odnoszą się do obiektów będących z nią w relacji jeden-do-jeden. W znormalizowanym schemacie relacyjnej bazy danych oznacza to sytuację, w której mamy do czynienia z dwoma tabelami A oraz B , a jedna z nich posiada kolumnę, której wartości wskazują na klucz identyfikujący krotkę w drugiej z nich. Np: Lazy fetch Adnotacja @OneToOne zawiera również atrybut fetch , który może przyjąć wartości EAGER lub LAZY (domyślnie EAGER ). O ile ustawienie wartości EAGER oznacza, że Hibernate musi pobrać dodatkowy wiersz natychmiast (za pomocą klauzuli JOIN bądź dodatkowego zapytania) to zastosowanie LAZY może ale nie musi spowodowa