Przegląd typów kaskadowych JPA / hibernacji

1. Wstęp

W tym samouczku omówimy, czym jest kaskadowanie w JPA / Hibernate. Następnie omówimy różne dostępne typy kaskad, wraz z ich semantyką.

2. Co to jest kaskadowanie?

Relacje między encjami często zależą od istnienia innej encji - na przykład relacji Osoba - Adres . Bez Osoba The Address jednostka nie ma żadnego znaczenia własnych. Kiedy usuwamy encję Osoba , nasza encja Adres również powinna zostać usunięta.

Sposób na osiągnięcie tego jest kaskadowy. Kiedy wykonamy jakąś akcję na encji docelowej, ta sama akcja zostanie zastosowana do skojarzonej jednostki.

2.1. JPA Cascade Type

Wszystkie operacje kaskadowe specyficzne dla JPA są reprezentowane przez wyliczenie javax.persistence.CascadeType zawierające wpisy:

  • WSZYSTKO
  • TRWAĆ
  • ŁĄCZYĆ
  • USUNĄĆ
  • ODŚWIEŻAĆ
  • ODŁĄCZYĆ

2.2. Typ kaskady hibernacji

Hibernate obsługuje trzy dodatkowe typy kaskadowe wraz z tymi określonymi przez JPA. Te specyficzne dla Hibernate typy kaskadowe są dostępne w org.hibernate.annotations.CascadeType :

  • REPLIKA
  • SAVE_UPDATE
  • ZAMEK

3. Różnica między typami kaskad

3.1. CascadeType . WSZYSTKO

Cascade.ALL propaguje wszystkie operacje - w tym operacje specyficzne dla Hibernate - od rodzica do podmiotu podrzędnego.

Zobaczmy to na przykładzie:

@Entity public class Person { @Id @GeneratedValue(strategy = GenerationType.AUTO) private int id; private String name; @OneToMany(mappedBy = "person", cascade = CascadeType.ALL) private List addresses; }

Zwróć uwagę, że w skojarzeniach OneToMany wspomnieliśmy o typie kaskadowym w adnotacji.

Teraz zobaczmy powiązany adres encji :

@Entity public class Address { @Id @GeneratedValue(strategy = GenerationType.AUTO) private int id; private String street; private int houseNumber; private String city; private int zipCode; @ManyToOne(fetch = FetchType.LAZY) private Person person; }

3.2. CascadeType . TRWAĆ

Operacja utrwalania sprawia, że ​​przejściowe wystąpienie jest trwałe. CascadeType PERSIST propaguje operację utrwalania z jednostki nadrzędnej do jednostki podrzędnej . Gdy zapiszemy podmiot będący osobą, jednostka adresowa również zostanie zapisana.

Zobaczmy przypadek testowy dla trwałej operacji:

@Test public void whenParentSavedThenChildSaved() { Person person = new Person(); Address address = new Address(); address.setPerson(person); person.setAddresses(Arrays.asList(address)); session.persist(person); session.flush(); session.clear(); }

Kiedy uruchomimy powyższy przypadek testowy, zobaczymy następujący SQL:

Hibernate: insert into Person (name, id) values (?, ?) Hibernate: insert into Address ( city, houseNumber, person_id, street, zipCode, id) values (?, ?, ?, ?, ?, ?)

3.3. CascadeType . ŁĄCZYĆ

Operacja scalania kopiuje stan danego obiektu na trwały obiekt o tym samym identyfikatorze. CascadeType.MERGE propaguje operację scalania z jednostki nadrzędnej do jednostki podrzędnej .

Przetestujmy operację scalania:

@Test public void whenParentSavedThenMerged() { int addressId; Person person = buildPerson("devender"); Address address = buildAddress(person); person.setAddresses(Arrays.asList(address)); session.persist(person); session.flush(); addressId = address.getId(); session.clear(); Address savedAddressEntity = session.find(Address.class, addressId); Person savedPersonEntity = savedAddressEntity.getPerson(); savedPersonEntity.setName("devender kumar"); savedAddressEntity.setHouseNumber(24); session.merge(savedPersonEntity); session.flush(); }

Kiedy uruchamiamy powyższy przypadek testowy, operacja scalania generuje następujący kod SQL:

Hibernate: select address0_.id as id1_0_0_, address0_.city as city2_0_0_, address0_.houseNumber as houseNum3_0_0_, address0_.person_id as person_i6_0_0_, address0_.street as street4_0_0_, address0_.zipCode as zipCode5_0_0_ from Address address0_ where address0_.id=? Hibernate: select person0_.id as id1_1_0_, person0_.name as name2_1_0_ from Person person0_ where person0_.id=? Hibernate: update Address set city=?, houseNumber=?, person_id=?, street=?, zipCode=? where id=? Hibernate: update Person set name=? where id=?

Tutaj widzimy, że operacja scalania najpierw ładuje zarówno encje adresowe, jak i osobowe, a następnie aktualizuje oba w wyniku CascadeType MERGE .

3.4. CascadeType.REMOVE

Jak sama nazwa wskazuje, operacja usuwania usuwa wiersz odpowiadający jednostce z bazy danych, a także z trwałego kontekstu.

CascadeType.REMOVE propaguje operację usuwania z jednostki nadrzędnej do jednostki podrzędnej. Podobnie jak CascadeType.REMOVE JPA , mamy CascadeType.DELETE , który jest specyficzny dla Hibernate . Nie ma różnicy między tymi dwoma.

Teraz pora przetestować CascadeType .

@Test public void whenParentRemovedThenChildRemoved() { int personId; Person person = buildPerson("devender"); Address address = buildAddress(person); person.setAddresses(Arrays.asList(address)); session.persist(person); session.flush(); personId = person.getId(); session.clear(); Person savedPersonEntity = session.find(Person.class, personId); session.remove(savedPersonEntity); session.flush(); }

Kiedy uruchomimy powyższy przypadek testowy, zobaczymy następujący SQL:

Hibernate: delete from Address where id=? Hibernate: delete from Person where id=?

Adres związany z osobą również został usunięty w wyniku CascadeType USUŃ .

3.5. CascadeType.DETACH

The detach operation removes the entity from the persistent context. When we use CascaseType.DETACH, the child entity will also get removed from the persistent context.

Let's see it in action:

@Test public void whenParentDetachedThenChildDetached() { Person person = buildPerson("devender"); Address address = buildAddress(person); person.setAddresses(Arrays.asList(address)); session.persist(person); session.flush(); assertThat(session.contains(person)).isTrue(); assertThat(session.contains(address)).isTrue(); session.detach(person); assertThat(session.contains(person)).isFalse(); assertThat(session.contains(address)).isFalse(); }

Here, we can see that after detaching person, neither person nor address exists in the persistent context.

3.6. CascadeType.LOCK

Unintuitively, CascadeType.LOCK re-attaches the entity and its associated child entity with the persistent context again.

Let's see the test case to understand CascadeType.LOCK:

@Test public void whenDetachedAndLockedThenBothReattached() { Person person = buildPerson("devender"); Address address = buildAddress(person); person.setAddresses(Arrays.asList(address)); session.persist(person); session.flush(); assertThat(session.contains(person)).isTrue(); assertThat(session.contains(address)).isTrue(); session.detach(person); assertThat(session.contains(person)).isFalse(); assertThat(session.contains(address)).isFalse(); session.unwrap(Session.class) .buildLockRequest(new LockOptions(LockMode.NONE)) .lock(person); assertThat(session.contains(person)).isTrue(); assertThat(session.contains(address)).isTrue(); }

As we can see, when using CascadeType.LOCK, we attached the entity person and its associated address back to the persistent context.

3.7. CascadeType.REFRESH

Refresh operations re-read the value of a given instance from the database. In some cases, we may change an instance after persisting in the database, but later we need to undo those changes.

In that kind of scenario, this may be useful. When we use this operation with CascadeType REFRESH, the child entity also gets reloaded from the database whenever the parent entity is refreshed.

For better understanding, let's see a test case for CascadeType.REFRESH:

@Test public void whenParentRefreshedThenChildRefreshed() { Person person = buildPerson("devender"); Address address = buildAddress(person); person.setAddresses(Arrays.asList(address)); session.persist(person); session.flush(); person.setName("Devender Kumar"); address.setHouseNumber(24); session.refresh(person); assertThat(person.getName()).isEqualTo("devender"); assertThat(address.getHouseNumber()).isEqualTo(23); }

Here, we made some changes in the saved entities person and address. When we refresh the person entity, the address also gets refreshed.

3.8. CascadeType.REPLICATE

The replicate operation is used when we have more than one data source, and we want the data in sync. With CascadeType.REPLICATE, a sync operation also propagates to child entities whenever performed on the parent entity.

Now, let's test CascadeType.REPLICATE:

@Test public void whenParentReplicatedThenChildReplicated() { Person person = buildPerson("devender"); person.setId(2); Address address = buildAddress(person); address.setId(2); person.setAddresses(Arrays.asList(address)); session.unwrap(Session.class).replicate(person, ReplicationMode.OVERWRITE); session.flush(); assertThat(person.getId()).isEqualTo(2); assertThat(address.getId()).isEqualTo(2); }

Because of CascadeTypeREPLICATE, when we replicate the person entity, then its associated address also gets replicated with the identifier we set.

3.9. CascadeType.SAVE_UPDATE

CascadeType.SAVE_UPDATE propagates the same operation to the associated child entity. It's useful when we use Hibernate-specific operations like save, update, and saveOrUpdate.

Let's see CascadeType.SAVE_UPDATE in action:

@Test public void whenParentSavedThenChildSaved() { Person person = buildPerson("devender"); Address address = buildAddress(person); person.setAddresses(Arrays.asList(address)); session.saveOrUpdate(person); session.flush(); }

Z powodu CascadeType.SAVE_UPDATE , kiedy uruchamiamy powyższy przypadek testowy, możemy zobaczyć, że zarówno osoba, jak i adres zostały zapisane. Oto wynikowy kod SQL:

Hibernate: insert into Person (name, id) values (?, ?) Hibernate: insert into Address ( city, houseNumber, person_id, street, zipCode, id) values (?, ?, ?, ?, ?, ?)

4. Wniosek

W tym artykule omówiliśmy kaskadowanie i różne opcje typu kaskadowego dostępne w JPA i Hibernate.

Kod źródłowy artykułu jest dostępny na GitHub.