1. 概要

このチュートリアルでは、JPA/Hibernateでのカスケードとは何かについて説明します。 次に、使用可能なさまざまなカスケードタイプとそのセマンティクスについて説明します。

2. カスケードとは何ですか?

エンティティの関係は、多くの場合、別のエンティティの存在に依存します。たとえば、 Person Addressの関係です。 Person がないと、Addressエンティティはそれ自体の意味を持ちません。 Person エンティティを削除すると、Addressエンティティも削除されるはずです。

カスケードはこれを実現する方法です。 ターゲットエンティティに対して何らかのアクションを実行すると、同じアクションが関連付けられたエンティティに適用されます。

2.1. JPAカスケードタイプ

すべてのJPA固有のカスケード操作は、エントリを含むjavax.persistence.CascadeType列挙型で表されます。

  • 全て
  • パーシスト
  • マージ
  • 削除する
  • リフレッシュ
  • デタッチ

2.2. Hibernateカスケードタイプ

Hibernateは、JPAによって指定されたものに加えて、3つの追加のカスケードタイプをサポートします。 これらのHibernate固有のカスケードタイプは、org.hibernate.annotations.CascadeTypeで利用できます。

  • 複製する
  • SAVE_UPDATE
  • ロック

3. カスケードタイプの違い

3.1. CascadeTypeALL

CascadeType.ALL Hibernate固有の操作を含むすべての操作を親エンティティから子エンティティに伝播します。

例でそれを見てみましょう:

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

OneToMany アソシエーションでは、アノテーションでカスケードタイプについて言及していることに注意してください。

次に、関連するエンティティAddressを見てみましょう。

@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. CascadeTypePERSIST

永続化操作により、一時インスタンスが永続化されます。 カスケードタイプPERSISTは、永続化操作を親エンティティから子エンティティに伝播します。 person エンティティを保存すると、addressエンティティも保存されます。

永続化操作のテストケースを見てみましょう。

@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();
}

上記のテストケースを実行すると、次のSQLが表示されます。

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

3.3. CascadeTypeMERGE

マージ操作は、指定されたオブジェクトの状態を同じ識別子を持つ永続オブジェクトにコピーします。 CascadeType.MERGEは、マージ操作を親エンティティから子エンティティに伝播します。

マージ操作をテストしてみましょう。

@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();
}

テストケースを実行すると、マージ操作によって次の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=?

ここでは、マージ操作が最初にaddressエンティティとpersonエンティティの両方をロードし、次にCascadeType.MERGEの結果として両方を更新していることがわかります。

3.4. CascadeType.REMOVE

名前が示すように、削除操作は、エンティティに対応する行をデータベースおよび永続コンテキストから削除します。

CascadeType.REMOVEは、削除操作を親エンティティから子エンティティに伝播します。 JPAのCascadeType.REMOVEと同様に、Hibernateに固有のCascadeType.DELETEがあります。 2。

次に、CascadeType.Removeをテストします。

@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();
}

テストケースを実行すると、次のSQLが表示されます。

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

person に関連付けられているアドレスも、CascadeType.REMOVEの結果として削除されました。

3.5. CascadeType.DETACH

デタッチ操作は、永続コンテキストからエンティティを削除します。 CascadeType.DETACHを使用すると、子エンティティも永続コンテキストから削除されます。

実際の動作を見てみましょう。

@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();
}

ここで、 person をデタッチした後、personaddressも永続コンテキストに存在しないことがわかります。

3.6. CascadeTypeLOCK

直感的ではありませんが、 CascadeType.LOCK は、エンティティとそれに関連付けられた子エンティティを永続コンテキストに再度アタッチします。

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();
}

ご覧のとおり、 CascadeType.LOCK を使用する場合、エンティティpersonとそれに関連付けられたaddressを永続コンテキストにアタッチし直しました。

3.7. CascadeTypeREFRESH

更新操作データベースから特定のインスタンスの値を再読み取りします。場合によっては、データベースに永続化した後にインスタンスを変更することがありますが、後でそれらの変更を元に戻す必要があります。

そのようなシナリオでは、これが役立つ場合があります。 カスケードタイプREFRESHでこの操作を使用すると、親エンティティが更新されるたびに、子エンティティもデータベースから再ロードされます。

理解を深めるために、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);
}

ここでは、保存されたエンティティpersonaddressにいくつかの変更を加えました。 person エンティティを更新すると、addressも更新されます。

3.8. CascadeType.REPLICATE

レプリケート操作は、複数のデータソースがあり、データを同期する必要がある場合に使用されます。 CascadeType.REPLICATE を使用すると、同期操作は、親エンティティ。

次に、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);
}

CascadeTypeREPLICATEのため、 person エンティティを複製すると、関連するaddressも設定した識別子で複製されます。

3.9. CascadeType.SAVE_UPDATE

CascadeType.SAVE_UPDATE は、関連付けられた子エンティティに同じ操作を伝播します。 使うときに便利です save、update、saveOrUpdateなどのHibernate固有の操作。 

CascadeType。SAVE_UPDATEの動作を見てみましょう。

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

CascadeType.SAVE_UPDATE のため、上記のテストケースを実行すると、personaddressの両方が保存されていることがわかります。

結果のSQLは次のとおりです。

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

4. 結論

この記事では、カスケードと、JPAおよびHibernateで使用可能なさまざまなカスケードタイプのオプションについて説明しました。

この記事のソースコードは、GitHubから入手できます。