1. 概要

JPA仕様は、熱心なフェッチ戦略と怠惰なフェッチ戦略の2つの異なるフェッチ戦略を提供します。 レイジーアプローチは、不要なデータの不必要なロードを回避するのに役立ちますが、閉じた永続コンテキストで最初にロードされていないデータを読み取る必要がある場合があります。さらに、閉じた永続コンテキストでレイジー要素コレクションにアクセスする一般的な問題。

このチュートリアルでは、レイジー要素コレクションからデータをロードする方法に焦点を当てます。 3つの異なるソリューションを検討します。1つはJPAクエリ言語を使用し、もう1つはエンティティグラフを使用し、最後のソリューションはトランザクション伝播を使用します。

2. エレメントコレクションの問題

デフォルトでは、JPAは@ElementCollectionタイプの関連付けでレイジーフェッチ戦略を使用します。したがって、閉じた永続コンテキストでコレクションにアクセスすると、例外が発生します。

問題を理解するために、従業員とその電話リストの関係に基づいてドメインモデルを定義しましょう。

@Entity
public class Employee {
    @Id
    private int id;
    private String name;
    @ElementCollection
    @CollectionTable(name = "employee_phone", joinColumns = @JoinColumn(name = "employee_id"))
    private List phones;

    // standard constructors, getters, and setters
}

@Embeddable
public class Phone {
    private String type;
    private String areaCode;
    private String number;

    // standard constructors, getters, and setters
}

私たちのモデルは、従業員が多くの電話を持つことができることを指定しています。 電話リストは、埋め込み可能なタイプのコレクションです。 このモデルでSpringリポジトリを使用してみましょう。

@Repository
public class EmployeeRepository {

    public Employee findById(int id) {
        return em.find(Employee.class, id);
    }

    // additional properties and auxiliary methods
}

それでは、簡単なJUnitテストケースで問題を再現してみましょう。

public class ElementCollectionIntegrationTest {

    @Before
    public void init() {
        Employee employee = new Employee(1, "Fred");
        employee.setPhones(
          Arrays.asList(new Phone("work", "+55", "99999-9999"), new Phone("home", "+55", "98888-8888")));
        employeeRepository.save(employee);
    }

    @After
    public void clean() {
        employeeRepository.remove(1);
    }

    @Test(expected = org.hibernate.LazyInitializationException.class)
    public void whenAccessLazyCollection_thenThrowLazyInitializationException() {
        Employee employee = employeeRepository.findById(1);
 
        assertThat(employee.getPhones().size(), is(2));
    }
}

このテストは、永続コンテキストが閉じているため、電話リストにアクセスしようとすると例外をスローします。

この問題は、@ElementCollectionのフェッチ戦略を変更して熱心なアプローチを使用することで解決できます。 ただし、データを熱心に取得することは、必ずしも最善の解決策ではありません。電話データは、必要かどうかに関係なく常に読み込まれるためです。

3. JPAクエリ言語を使用したデータのロード

JPAクエリ言語を使用すると、投影される情報をカスタマイズできます。したがって、 EmployeeRepository で新しいメソッドを定義して、従業員とその電話を選択できます。

public Employee findByJPQL(int id) {
    return em.createQuery("SELECT u FROM Employee AS u JOIN FETCH u.phones WHERE u.id=:id", Employee.class)
        .setParameter("id", id).getSingleResult();
}

上記のクエリは、内部結合操作を使用して、返された各従業員の電話リストをフェッチします。

4. エンティティグラフを使用したデータの読み込み

別の可能な解決策は、JPAのエンティティグラフ機能を使用することです。 エンティティグラフを使用すると、JPAクエリによって投影されるフィールドを選択できます。リポジトリにもう1つのメソッドを定義しましょう。

public Employee findByEntityGraph(int id) {
    EntityGraph entityGraph = em.createEntityGraph(Employee.class);
    entityGraph.addAttributeNodes("name", "phones");
    Map<String, Object> properties = new HashMap<>();
    properties.put("javax.persistence.fetchgraph", entityGraph);
    return em.find(Employee.class, id, properties);
}

エンティティグラフには、nameとphonesの2つの属性が含まれていることがわかります。 したがって、JPAがこれをSQLに変換すると、関連する列が投影されます。

5. トランザクションスコープでのデータのロード

最後に、最後の1つの解決策を検討します。 これまでのところ、問題は永続コンテキストのライフサイクルに関連していることがわかりました。

何が起こるかというと、永続コンテキストはトランザクションスコープであり、トランザクションが終了するまで開いたままになります。 トランザクションのライフサイクルは、リポジトリメソッドの実行の開始から終了までに及びます。

それでは、別のテストケースを作成し、テストメソッドによって開始されたトランザクションにバインドするように永続コンテキストを構成しましょう。 テストが終了するまで、永続コンテキストを開いたままにします。

@Test
@Transactional
public void whenUseTransaction_thenFetchResult() {
    Employee employee = employeeRepository.findById(1);
    assertThat(employee.getPhones().size(), is(2));
}

@Transactionalアノテーションは、関連するテストクラスのインスタンスの周りにトランザクションプロキシを構成します。さらに、トランザクションは、それを実行するスレッドに関連付けられます。 デフォルトのトランザクション伝播設定を考慮すると、このメソッドから作成されたすべての永続コンテキストは、この同じトランザクションに参加します。 したがって、トランザクション永続コンテキストは、テストメソッドのトランザクションスコープにバインドされます。

6. 結論

このチュートリアルでは、閉じた永続コンテキストでレイジーアソシエーションからデータを読み取る問題に対処するために、3つの異なるソリューションを評価しました。

まず、JPAクエリ言語を使用して要素コレクションをフェッチしました。 次に、必要なデータを取得するためのエンティティグラフを定義しました。

そして、究極のソリューションでは、Spring Transactionを使用して、永続コンテキストを開いたままにし、必要なデータを読み取りました。

いつものように、このチュートリアルのサンプルコードは、GitHubからで入手できます。