W trakcie pisania kawałka kodu wykorzystującego JPA natrafiłem na następującą sytuację. Z innej warstwy aplikacji otrzymywałem nowy obiekt, którego jeden z atrybutów był obiektem zapisanym już wcześniej w bazie danych. Jednak był on w stanie detached i należałoby go włączyć do persistence context przed zapisaniem głównego obiektu.

class A { // nowy obiekt, bez ustawionego id
 
    @Id
    int id;
    @ManyToOne
    B b; // obiekt już zapisany w bazie danych, detached
}

Rozwiązałem to w ten sposób, że relacji do encji B ustawiłem atrybut cascade na CascadeType.MERGE (np. @ManyToOne(cascade = CascadeType.MERGE)) i użyłem metody merge() do zapisania nowego obiektu. Zadziałało, ale zacząłem się zastanawiać, jaka właściwie jest różnice pomiędzy persist() a merge().

Zacznijmy od dokumentacji. Poniższe punkty stanowią tłumaczenie specyfikacji JPA:

3.2.1 Zapisywanie encji

Nowa instancja encji zostaje zapisana w bazie danych i staje się zarządzaną poprzez jawne lub kaskadowe wywołanie na niej operacji persist().

  • Jeśli X jest nową encją, staje się zarządzaną. Zapis do bazy danych następuje przed lub w momencie zatwierdzenia transakcji albo w wyniku opróżnienia bufora operacji.
  • Jeśli X jest encją, która znajduje się już w bazie danych i jest zarządzana, to nie bierze udziału w zapisywaniu. Jednak operacja ta jest wywoływana kaskadowo na wszystkich encjach będących w relacji do X, jeśli relacje te są opisane adnotacjami cascade = CascadeType.PERSIST albo cascade = CascadeType.ALL. Ustawienia te można też umieścić w odpowiednim elemencie deskryptora.
  • Jeśli encja X została usunięta, staje się zarządzaną.
  • Jeśli X jest encją w stanie detached (ang. odłączona), wyjątek EntityExistsException może zostać zgłoszony po wywołaniu metody persist() albo w momencie zatwierdzenia transakcji lub opróżnienia bufora operacji może zostać zgłoszony wyjątek EntityExistsException lub inny PersistenceException.
  • Dla wszystkich encji Y będących w relacji do X, jeśli relacje są opisane adnotacjami z atrybutem cascade = CascadeType.PERSIST albo cascade = CascadeType.ALL, to operacja zapisywania odnosi się również do Y.

3.4.2.1 Scalanie stanu odłączonej encji

Operacja scalania propaguje stan odłączonej encji do instancji zarządzanej przez Entity Manager.

  • Jeśli X jest odłączoną encją, to jej stan jest kopiowany do już istniejącej i zarządzanej instancji X’ o tej samej tożsamości albo tworzona jest jej nowa, zarządzana kopia X’.
  • Jeśli X jest nową instancją, to nowa, zarządzana instancja X’ zostanie utworzona a stan encji X jest kopiowany do X’.
  • Jeśli X została usunięta, wyjątek IllegalArgumentException zostanie zgłoszony albo transakcja zakończy się niepowodzeniem.
  • Jeśli X jest już zarządzana, to operacja merge() jest ignorowana. Jest ona jednak wywoływana kaskadowo na wszystkich encjach będących w relacji do X, jeśli relacje te są opisane adnotacjami z atrybutem cascade = CascadeType.MERGE albo cascade = CascadeType.ALL.
  • Dla wszystkich encji Y będących w relacji do X i relacje te są opisane adnotacją z atrybutami cascade = CascadeType.MERGE albo cascade = CascadeType.ALL, Y jest scalana rekurencyjnie do Y’. Dla wszystkich relacji X z Y, jest tworzona relacja pomiędzy odpowiadającymi X’ i Y’ (jeśli X jest już zarządzana, to X jest tym samym obiektem co X’).
  • Jeśli X jest scalona do X’, z relacją do encji Y a relacja nie jest opisana adnotacją z atrybutem cascade = CascadeType.MERGE albo cascade = CascadeType.ALL, to trawersowanie tej samej relacji z encji X’ da w wyniku referencję do zarządzanego obiektu Y’ o tej samej tożsamości co Y.

Tyle teorii…

Co z niej wynika, jeśli chodzi o pisanie kodu?

E e = new E();
em.persist(e);
e.setField("newValue"); // ta zmiana zostanie zapisana w bazie danych przy zatwierdzeniu transakcji,
                        // gdyż w tym momencie encja 'e' jest już zarządzana
E e = new E();
em.merge(e);
e.setField("newValue"); // ta zmiana nie zostanie zapisana w bazie danych,
                        // gdyż encja 'e' nie jest zarządzana
E e = new E();
E ep = em.merge(e);
ep.setField("newValue"); // ta zmiana zostanie zapisana w bazie danych przy zatwierdzeniu transakcji,
                         // gdyż encja 'ep' jest zarządzana i tożsama z e
E e = getEntityFromWebTier(E.class); // encja 'e' jest 'detached'
em.persist(e); // wyjątek

Jeśli znacie inne przykłady na różnice pomiędzy persist() a merge(), zachęcam do podzielenia się nimi w komentarzach.