1. 概要

Hibernateを使用しているときに、次のエラーが発生した可能性があります: org.hibernate.LazyInitializationException:プロキシを初期化できませんでした–セッションがありません

このクイックチュートリアルでは、エラーの根本原因を詳しく調べ、エラーを回避する方法を学びます。

2. エラーを理解する

開いているHibernateセッションのコンテキスト外で遅延ロードされたオブジェクトにアクセスすると、この例外が発生します。

セッションレイジー初期化、 プロキシオブジェクトとは何か、およびHibernateでそれらがどのように組み合わされるかを理解することが重要です。 フレームワーク:

  • セッションは、アプリケーションとデータベース間の会話を表す永続コンテキストです。
  • 遅延読み込みは、コードでアクセスされるまで、オブジェクトがセッションコンテキストに読み込まれないことを意味します。
  • Hibernateは動的なProxyObject サブクラスを作成します。このサブクラスは、最初にオブジェクトを使用したときにのみデータベースにアクセスします。

このエラーは、プロキシオブジェクトを使用してデータベースから遅延ロードされたオブジェクトをフェッチしようとしたが、Hibernateセッションがすでに閉じられている場合に発生します。

3. LazyInitializationExceptionの例

具体的なシナリオで例外を見てみましょう。

役割が関連付けられた単純なUserオブジェクトを作成します。 JUnitを使用して、LazyInitializationExceptionエラーを示します。

3.1. Hibernateユーティリティクラス

まず、 HibernateUtil クラスを定義して、構成を使用してSessionFactoryを作成しましょう。

インメモリHSQLDBデータベースを使用します。

3.2. エンティティ

Userエンティティは次のとおりです。

@Entity
@Table(name = "user")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private int id;

    @Column(name = "first_name")
    private String firstName;
    
    @Column(name = "last_name")
    private String lastName;
    
    @OneToMany
    private Set<Role> roles;
    
}

そして、関連する Role エンティティ:

@Entity
@Table(name = "role")
public class Role {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private int id;

    @Column(name = "role_name")
    private String roleName;
}

ご覧のとおり、UserRoleの間には1対多の関係があります。

3.3. ロールを持つユーザーの作成

次に、2つのRoleオブジェクトを作成します。

Role admin = new Role("Admin");
Role dba = new Role("DBA");

また、次の役割を持つユーザーを作成します。

User user = new User("Bob", "Smith");
user.addRole(admin);
user.addRole(dba);

最後に、セッションを開いてオブジェクトを永続化できます。

Session session = sessionFactory.openSession();
session.beginTransaction();
user.getRoles().forEach(role -> session.save(role));
session.save(user);
session.getTransaction().commit();
session.close();

3.4. 役割の取得

この最初のシナリオでは、適切な方法でユーザーロールを取得する方法を説明します。

@Test
public void whenAccessUserRolesInsideSession_thenSuccess() {

    User detachedUser = createUserWithRoles();

    Session session = sessionFactory.openSession();
    session.beginTransaction();
		
    User persistentUser = session.find(User.class, detachedUser.getId());
		
    Assert.assertEquals(2, persistentUser.getRoles().size());
		
    session.getTransaction().commit();
    session.close();
}

ここでは、セッション内のオブジェクトにアクセスするため、エラーは発生しません。

3.5. 役割の取得の失敗

この2番目のシナリオでは、セッション外でgetRolesメソッドを呼び出します。

@Test
public void whenAccessUserRolesOutsideSession_thenThrownException() {
		
    User detachedUser = createUserWithRoles();

    Session session = sessionFactory.openSession();
    session.beginTransaction();
		
    User persistentUser = session.find(User.class, detachedUser.getId());
		
    session.getTransaction().commit();
    session.close();

    thrown.expect(LazyInitializationException.class);
    System.out.println(persistentUser.getRoles().size());
}

この場合、セッションが閉じられた後にロールにアクセスしようとします。その結果、コードは LazyInitializationExceptionをスローします。

4. エラーを回避する方法

次に、エラーを克服するための4つの異なるソリューションを見てみましょう。

4.1. 上位層でセッションを開く

ベストプラクティスは、たとえばDAOパターンを使用して永続層でセッションを開くことです。

上位層でセッションを開いて、関連するオブジェクトに安全にアクセスできます。 たとえば、Viewレイヤーでセッションを開くことができます。

その結果、応答時間が長くなり、アプリケーションのパフォーマンスに影響します。

このソリューションは、関心の分離の原則の観点からはアンチパターンです。 さらに、データの整合性違反や長時間実行されるトランザクションを引き起こす可能性があります。

4.2. enable_lazy_load_no_transプロパティをオンにする

このHibernateプロパティは、遅延ロードされたオブジェクトフェッチのグローバルポリシーを宣言するために使用されます。

デフォルトでは、このプロパティはfalseです。 オンにすると、関連付けられた遅延読み込みエンティティへの各アクセスが、新しいトランザクションで実行される新しいセッションにラップされます。

<property name="hibernate.enable_lazy_load_no_trans" value="true"/>

このプロパティを使用してLazyInitializationExceptionエラーを回避すると、アプリケーションのパフォーマンスが低下するため、お勧めしません。 これは、でn + 1の問題が発生するためです。簡単に言うと、ユーザーに対して1つのSELECTと、各ユーザーの役割をフェッチするためのNの追加SELECTを意味します。

このアプローチは効率的ではなく、アンチパターンとも見なされます。

4.3. FetchType.EAGER戦略の使用

この戦略は、@OneToManyアノテーションと一緒に使用できます。

@OneToMany(fetch = FetchType.EAGER)
@JoinColumn(name = "user_id")
private Set<Role> roles;

これは、ほとんどのユースケースで関連するコレクションをフェッチする必要がある場合の特定の使用法に対する一種の妥協したソリューションです。

さまざまなビジネスフローのほとんどのコレクションを明示的にフェッチするよりも、EAGERフェッチタイプを宣言する方がはるかに簡単です。

4.4. 結合フェッチの使用

JPQLJOINFETCH ディレクティブを使用して、関連するコレクションをオンデマンドでフェッチすることもできます。

SELECT u FROM User u JOIN FETCH u.roles

または、HibernateCriteriaAPIを使用できます。

Criteria criteria = session.createCriteria(User.class);
criteria.setFetchMode("roles", FetchMode.EAGER);

ここでは、同じラウンドトリップで User オブジェクトとともに、データベースからフェッチする必要のある関連コレクションを指定します。 このクエリを使用すると、関連付けられたオブジェクトを個別に取得する必要がなくなるため、反復の効率が向上します。

これは、LazyInitializationExceptionエラーを回避するための最も効率的できめ細かいソリューションです。

5. 結論

この記事では、 org.hibernate.LazyInitializationExceptionに対処する方法を学びました:プロキシを初期化できませんでした–セッションエラーがありません。

パフォーマンスの問題とともに、さまざまなアプローチを検討しました。 また、パフォーマンスへの影響を回避するために、シンプルで効率的なソリューションを使用することの重要性についても説明しました。

最後に、結合フェッチアプローチがエラーを回避するための優れた方法であることを示しました。

いつものように、コードはGitHubから入手できます。