Hibernateで発生する A different object with the same identifier value was already associated with the session (NonUniqueObjectException)エラー。
このエラーは、Hibernate のセッション内で「同じ ID(主キー)」を持つ 2 つの異なるオブジェクトが存在している場合に発生します。
Hibernate は「1つのセッション内では、同じ ID のエンティティは1つだけ」しか管理できません。
つまり、同じ主キーを持つオブジェクトが複数存在すると、どちらを保存すべきかわからなくなり、例外を投げて処理を止めます。
springを古いバージョンからバージョンアップさせた際にも発生する可能性があるので注意が必要です。
よくある発生例
例えば以下のようなケースです。
// ID=1 のProductをセッションから取得 Product productA = session.get(Product.class, 1L); Product productB = new Product(); // 別インスタンスとしてID=1をセット productB.setId(1L); // ここでエラー! session.saveOrUpdate(productB);
このように、同じIDを持つ別インスタンス(productA と productB)をセッションに関連付けようとすると、Hibernateは混乱します。
原因まとめ
- 同じセッション内で、同じ主キーを持つエンティティを複数生成・関連付けてしまっている
- DB上では同じ行を指していても、Java上では異なるオブジェクトインスタンスになっている
- Hibernate セッションは、一意のIDに対して一意のオブジェクトしか扱えないという制約がある
解決策
このエラーは、Hibernateセッションが「同じIDを持つ別インスタンス」を2つ以上同時に管理しようとしたときに起こるものでした。
では、具体的にどう対処すれば良いのでしょうか?代表的な3つの方法を紹介します。
1.すでにセッションにあるオブジェクトを再利用する
最もシンプルなのは、既にセッションに紐づいているオブジェクトを再利用することです。
// 既に存在するオブジェクトを取得 Product product = session.get(Product.class, 1L); product.setName("新しい名前"); // 問題なく動作 session.saveOrUpdate(product);
新しいインスタンスを手動で作るのではなく、session.get()やsession.load()でHibernateに管理させたインスタンスを使用すれば、セッションの整合性は維持されます。
「どうせ同じIDだから大丈夫だろう」と思って new するとハマるので、取得して使い回す方法もあります。
2.セッションから一度切り離す
もし既にセッションに登録されているオブジェクト(productAなど)を使わずに、新しいインスタンス(productB)を保存したい場合は、先に古い方をセッションから切り離す(detach)という手もあります。
// セッションから切り離す session.evict(productA); // 新しいインスタンスを登録 session.saveOrUpdate(productB);
これにより、HibernateはproductAの管理をやめ、新しく渡されたproductBをセッションに関連付けます。
この方法は「今後productAはもう使わない」という前提で行うべきです。中途半端に両方を扱うと、データの整合性が取れなくなる恐れがあります。
3.merge() を使って Hibernate にマージさせる
Hibernateの merge() メソッドを使うと、新しく渡されたオブジェクトの状態を、セッションが持つ既存のオブジェクトと自動的に統合してくれます。
session.merge(productB); //注意点:merge() は 新しいインスタンスを返す ので、それを使い回さないと変更が反映されません。 Product managedProduct = session.merge(productB);
merge()は、セッションがすでに管理しているエンティティがあればそこに統合し、なければ新たに管理を開始します。
選び方
方法 | 特徴 | 向いているケース |
---|---|---|
再利用する | 安全で明快 | 既に取得済みなら |
切り離す | 明示的に上書きしたい時に便利 | 強制的に置き換えるとき |
merge() を使う |
柔軟かつスマートに統合できる | どちらが存在するか不明なとき |
補足
よくあるのは、別のエンティティ(例:B)に、同じ主キーを持つ別のオブジェクト(例:C)を設定しているケースです。
// Cの代わりに別インスタンスを使っている b.setCategory(new Category(1L));
このような場合も、Cがすでにセッションに存在していればエラーになります。
→ 同じエンティティは同じインスタンスで参照するように心がけることが、Hibernateを正しく使う上で重要です。
まとめ
このエラーは、Hibernateのセッション管理に対する理解がないと、つまずきやすい落とし穴ですが、一度原理を知ってしまえば、
-
「同じIDを持つエンティティはセッションに1つだけ」
-
「必要ならevictかmergeを使う」
というルールを意識するだけで、安定したデータ操作が可能になります。
ポイント
- このエラーは「同じIDを持つ異なるオブジェクトがセッションに関連付けられている」ときに発生
- Hibernateセッションは「ID=1」になるよう1つのオブジェクトしか許容しない
- 原因は、同じデータの別インスタンスを作ってしまったことが多い
- 解決には、get()で取得したオブジェクトの再利用、evict()での切り離し、またはmerge()の使用が効果的