Spring Data - save vs saveAndFlush

blog.maczkowski.dev 4 lat temu
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 dzięki 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, iż 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, iż 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, iż został on wymuszony manualnie. Tryb możemy zmienić dzięki properties-a: spring.jpa.properties.org.hibernate.flushMode=COMMITWyjś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ę, iż 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, iż działanie metody save opiera się na wywołaniu metod EntityManager-a (persist lub merge) to warto zwrócić uwagę, iż 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ę, iż 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
Idź do oryginalnego materiału