Hibernate:保存、永続化、更新、マージ、saveOrUpdate
1. 前書き
この記事では、Sessionインターフェースのいくつかの方法(save、persist、update、merge、saveOrUpdate)の違いについて説明します。
これはHibernateの概要ではなく、構成、オブジェクトリレーショナルマッピング、およびエンティティインスタンスの操作の基本を既に知っている必要があります。 Hibernateの紹介記事については、Hibernate 4 with Springに関するチュートリアルにアクセスしてください。
参考文献:
2. 永続コンテキスト実装としてのセッション
Sessionインターフェースには、最終的にデータベースにデータを保存するいくつかのメソッドがあります:persist、save、update、merge、saveOrUpdate。 これらのメソッドの違いを理解するには、最初に、永続コンテキストとしてのSessionの目的と、Sessionに関連するエンティティインスタンスの状態の違いについて説明する必要があります。
APIメソッドの一部が重複する原因となったHibernate開発の歴史も理解する必要があります。
2.1. エンティティインスタンスの管理
オブジェクトリレーショナルマッピング自体とは別に、Hibernateが解決しようとしていた問題の1つは、実行中にエンティティを管理する問題です。 「永続コンテキスト」の概念は、この問題に対するHibernateのソリューションです。 永続コンテキストは、セッション中にデータベースにロードまたは保存したすべてのオブジェクトのコンテナまたは一次キャッシュと考えることができます。
セッションは論理トランザクションであり、その境界はアプリケーションのビジネスロジックによって定義されます。 永続コンテキストを介してデータベースを操作し、すべてのエンティティインスタンスがこのコンテキストに関連付けられている場合、セッション中にやり取りしたすべてのデータベースレコードに対して常にエンティティの単一インスタンスが必要です。
Hibernateでは、永続コンテキストはorg.hibernate.Sessionインスタンスで表されます。 JPAの場合、これはjavax.persistence.EntityManagerです。 HibernateをJPAプロバイダーとして使用し、EntityManagerインターフェースを介して操作する場合、このインターフェースの実装は基本的に基礎となるSessionオブジェクトをラップします。 ただし、HibernateSessionは、より多くの可能性を備えたより豊富なインターフェイスを提供するため、Session directlyを操作すると便利な場合があります。
2.2. エンティティインスタンスの状態
アプリケーション内のエンティティインスタンスは、Session永続コンテキストに関連する3つの主要な状態のいずれかで表示されます。
-
transient —このインスタンスはSessionにアタッチされておらず、アタッチされたこともありません。このインスタンスには、データベースに対応する行がありません。通常、データベースに保存するために作成した新しいオブジェクトです。
-
persistent —このインスタンスは一意のSessionオブジェクトに関連付けられています。 Sessionをデータベースにフラッシュすると、このエンティティはデータベースに対応する一貫したレコードを持つことが保証されます。
-
detached —このインスタンスはかつてSession(persistent状態)にアタッチされていましたが、現在はアタッチされていません。インスタンスをコンテキストから削除したり、セッションをクリアまたはクローズしたり、インスタンスをシリアル化/逆シリアル化プロセスにかけたりすると、インスタンスはこの状態になります。
これは、状態遷移を発生させるSessionメソッドに関するコメント付きの簡略化された状態図です。
エンティティインスタンスがpersistent状態の場合、このインスタンスのマップされたフィールドに加えたすべての変更は、Sessionをフラッシュするときに、対応するデータベースレコードとフィールドに適用されます。 persistentインスタンスは「オンライン」と見なすことができますが、detachedインスタンスは「オフライン」になり、変更が監視されていません。
つまり、persistentオブジェクトのフィールドを変更する場合、これらの変更をデータベースに取得するために、save、update、またはこれらのメソッドを呼び出す必要はありません。必要なのはすべてです。トランザクションが完了したら、トランザクションをコミットするか、セッションをフラッシュまたはクローズします。
2.3. JPA仕様への適合
Hibernateは最も成功したJava ORM実装でした。 Java Persistence API(JPA)の仕様がHibernate APIの影響を強く受けていたことも不思議ではありません。 残念ながら、多くの違いもありました。いくつかは大きく、いくつかは微妙です。
JPA標準の実装として機能するには、Hibernate APIを修正する必要がありました。 EntityManagerインターフェースに一致するいくつかのメソッドがSessionインターフェースに追加されました。 これらのメソッドは「元の」メソッドと同じ目的を果たしますが、仕様に準拠しているため、いくつかの違いがあります。
3. 操作の違い
すべてのメソッド(persist、save、update、merge、saveOrUpdate)がすぐに結果になるわけではないことを最初から理解することが重要です。対応するSQLUPDATEまたはINSERTステートメント。 データベースへのデータの実際の保存は、トランザクションのコミット時またはSessionのフラッシュ時に発生します。
前述のメソッドは、基本的に、エンティティインスタンスをライフサイクルに沿って異なる状態間で遷移させることにより、エンティティインスタンスの状態を管理します。
エンティティの例として、単純な注釈マップエンティティPersonを使用します。
@Entity
public class Person {
@Id
@GeneratedValue
private Long id;
private String name;
// ... getters and setters
}
3.1. Persist
persistメソッドは、永続コンテキストに新しいエンティティインスタンスを追加することを目的としています。 インスタンスを一時状態からpersistent状態に移行します。
通常、データベースにレコードを追加する(エンティティインスタンスを保持する)ときに呼び出します。
Person person = new Person();
person.setName("John");
session.persist(person);
persistメソッドが呼び出された後はどうなりますか? personオブジェクトがtransientからpersistent状態に移行しました。 オブジェクトは現在永続コンテキストにありますが、まだデータベースに保存されていません。 INSERTステートメントの生成は、トランザクションのコミット、セッションのフラッシュまたはクローズ時にのみ発生します。
persistメソッドにはvoidの戻り値の型があることに注意してください。 渡されたオブジェクトを「その場で」操作し、その状態を変更します。 person変数は、実際の永続オブジェクトを参照します。
このメソッドは、後でSessionインターフェースに追加されます。 このメソッドの主な差別化機能は、JSR-220仕様(EJB永続性)に準拠していることです。 このメソッドのセマンティクスは仕様で厳密に定義されており、基本的に次のように記述されています。
-
transientインスタンスはpersistentになります(そして操作はcascade=PERSISTまたはcascade=ALLとのすべての関係にカスケードされます)、
-
インスタンスがすでにpersistentである場合、この呼び出しはこの特定のインスタンスには影響しません(ただし、cascade=PERSISTまたはcascade=ALLとの関係にカスケードされます)。
-
インスタンスがdetachedの場合、このメソッドを呼び出すとき、またはセッションをコミットまたはフラッシュするときに、例外が発生する可能性があります。
ここには、インスタンスの識別子に関係するものは何もないことに注意してください。 仕様では、ID生成戦略に関係なく、IDがすぐに生成されるとは記載されていません。 persistメソッドの仕様により、実装はコミットまたはフラッシュ時にidを生成するステートメントを発行できます。また、このメソッドを呼び出した後、idがnullでないことが保証されないため、これに依存しないでください。
すでにpersistentインスタンスでこのメソッドを呼び出すことができますが、何も起こりません。 ただし、detachedインスタンスを永続化しようとすると、実装は例外をスローすることになります。 次の例では、エンティティをpersistし、コンテキストからevictして、detachedになるようにしてから、persistを再試行します。 session.persist()への2回目の呼び出しは例外を引き起こすため、次のコードは機能しません。
Person person = new Person();
person.setName("John");
session.persist(person);
session.evict(person);
session.persist(person); // PersistenceException!
3.2. Save
saveメソッドは、JPA仕様に準拠していない「元の」Hibernateメソッドです。
その目的は基本的にpersistと同じですが、実装の詳細が異なります。 このメソッドのドキュメントには、「最初に生成された識別子を割り当てる」というインスタンスを永続化することが厳密に記載されています。 このメソッドは、この識別子のSerializable値を返すことが保証されています。
Person person = new Person();
person.setName("John");
Long id = (Long) session.save(person);
すでに永続化されているインスタンスを保存する効果は、persistの場合と同じです。 detachedインスタンスを保存しようとすると、違いが生じます。
Person person = new Person();
person.setName("John");
Long id1 = (Long) session.save(person);
session.evict(person);
Long id2 = (Long) session.save(person);
id2変数はid1とは異なります。 detachedインスタンスでsaveを呼び出すと、新しいpersistentインスタンスが作成され、それに新しい識別子が割り当てられます。これにより、コミットまたはフラッシュ時にデータベース内のレコードが重複します。
3.3. Merge
mergeメソッドの主な目的は、persistentエンティティインスタンスをdetachedエンティティインスタンスからの新しいフィールド値で更新することです。
たとえば、JSONシリアル化されたオブジェクトをそのIDで呼び出し元に取得するメソッドと、このオブジェクトの更新されたバージョンを呼び出し元から受信するメソッドを備えたRESTfulインターフェイスがあるとします。 このようなシリアル化/逆シリアル化を通過したエンティティは、detached状態で表示されます。
このエンティティインスタンスを逆シリアル化した後、永続コンテキストからpersistentエンティティインスタンスを取得し、このdetachedインスタンスからの新しい値でそのフィールドを更新する必要があります。 したがって、mergeメソッドはまさにそれを行います。
-
渡されたオブジェクトから取得したIDでエンティティインスタンスを検索します(永続コンテキストから既存のエンティティインスタンスが取得されるか、データベースからロードされた新しいインスタンス)。
-
渡されたオブジェクトからこのインスタンスにフィールドをコピーします。
-
新しく更新されたインスタンスを返します。
次の例では、保存されたエンティティをコンテキストからevict(デタッチ)し、nameフィールドを変更してから、mergeをdetachedエンティティに変更します。
Person person = new Person();
person.setName("John");
session.save(person);
session.evict(person);
person.setName("Mary");
Person mergedPerson = (Person) session.merge(person);
mergeメソッドはオブジェクトを返すことに注意してください。引数として渡したpersonオブジェクトではなく、永続コンテキストにロードされて更新されたのはmergedPersonオブジェクトです。 これらは2つの異なるオブジェクトであり、通常、personオブジェクトは破棄する必要があります(とにかく、永続コンテキストにアタッチされていることを期待しないでください)。
persistメソッドと同様に、mergeメソッドは、信頼できる特定のセマンティクスを持つようにJSR-220によって指定されています。
-
エンティティがdetachedの場合、既存のpersistentエンティティにコピーされます。
-
エンティティがtransientの場合、新しく作成されたpersistentエンティティにコピーされます。
-
この操作は、cascade=MERGEまたはcascade=ALLマッピングを使用するすべての関係に対してカスケードされます。
-
エンティティがpersistentの場合、このメソッド呼び出しはエンティティに影響を与えません(ただし、カスケードは引き続き行われます)。
3.4. Update
persistおよびsaveと同様に、updateメソッドは、mergeメソッドが追加されるずっと前に存在していた「元の」Hibernateメソッドです。 その意味は、いくつかの重要な点で異なります。
-
渡されたオブジェクトに作用します(戻り値の型はvoidです)。 updateメソッドは、渡されたオブジェクトをdetachedからpersistent状態に遷移させます。
-
このメソッドにtransientエンティティを渡すと、このメソッドは例外をスローします。
次の例では、オブジェクトをsaveし、次にコンテキストからevict(デタッチ)してから、そのnameを変更し、updateを呼び出します。 updateはpersonオブジェクト自体で行われるため、update操作の結果を別の変数に入れないことに注意してください。 基本的に、既存のエンティティインスタンスを永続コンテキストに再接続しています - JPA仕様では許可されていません。
Person person = new Person();
person.setName("John");
session.save(person);
session.evict(person);
person.setName("Mary");
session.update(person);
transientインスタンスでupdateを呼び出そうとすると、例外が発生します。 以下は動作しません。
Person person = new Person();
person.setName("John");
session.update(person); // PersistenceException!
3.5. SaveOrUpdate
このメソッドはHibernate APIにのみ表示され、対応する標準化されたメソッドはありません。 updateと同様に、インスタンスの再接続にも使用できます。
実際、updateメソッドを処理する内部DefaultUpdateEventListenerクラスは、DefaultSaveOrUpdateListenerのサブクラスであり、一部の機能をオーバーライドするだけです。 saveOrUpdateメソッドの主な違いは、transientインスタンスに適用されたときに例外をスローしないことです。代わりに、このtransientインスタンスをpersistentにします。 次のコードは、新しく作成されたPersonのインスタンスを永続化します。
Person person = new Person();
person.setName("John");
session.saveOrUpdate(person);
このメソッドは、オブジェクトがtransientであるかdetachedであるかに関係なく、オブジェクトをpersistentにするためのユニバーサルツールと考えることができます。
4. 何を使用しますか?
特別な要件がない場合は、経験則として、persistメソッドとmergeメソッドが標準化されており、JPA仕様に準拠することが保証されているため、これらのメソッドを使用する必要があります。
また、別の永続性プロバイダーに切り替えることにした場合に備えて移植可能ですが、「元の」Hibernateメソッドであるsave、update、およびsaveOrUpdateほど有用ではないように見える場合があります。
5. 結論
実行時の永続エンティティの管理に関連して、さまざまなHibernateSessionメソッドの目的について説明しました。 これらのメソッドがライフサイクルを通じてエンティティインスタンスをどのように移行するか、およびこれらのメソッドの一部で機能が重複している理由を学びました。
記事のソースコードはavailable on GitHubです。