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: 0Hibernate: 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: 0Saved
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