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 metodsave*
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 metodysave
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
.
Komentarze
Prześlij komentarz