IK.AM

@making's tech note


JPA 2.1のエンティティグラフについて

🗃 {Programming/Java/JPA}
🏷 JPA 🏷 Java 🏷 Java EE 7 
🗓 Updated at 2015-07-20T16:59:24Z  🗓 Created at 2015-07-20T16:59:24Z   🌎 English Page

JPA 2.1の分かりにくい新機能であるエンティティグラフについて、訳あって調べおり、 カッとなったので解説記事を書きます。(他の解説記事がいまいちわかりづらかった)


エンティティグラフはその名前から推測される「エンティティのグラフ」というよりもエンティティと属性のひとかたまりのグループのようなものです。 findメソッドやクエリ実行時のフェッチの戦略(EAGERにするかLAZYにするか)をオーバーライドするために利用されます。

エンティティグラフはJPA 2.1で導入されました。 エンティティグラフが導入される前は、エンティティの属性毎にFetchType.EAGERFetchType.LAZYを付けるか、 クエリ内でフェッチ結合を用いることでしかフェッチ戦略を決めることができず、柔軟性に欠けました。 エンティティグラフを用いることで、グループ化したエンティティと属性の集合にフェッチ戦略を決めることができ、 ユースケース毎にEAGER/LAZYの有効・無効を切り替えられます。

エンティティグラフでは次の3のオブジェクトが登場します。

  • エンティティグラフノード ...エンティティグラフのルートとなるノードであり、すべての属性やルートに属するサブグラフを含みます。

  • 属性ノード ...エンティティの属性を表すノード。 基本型(プリミティブやそのラッパー、String、Dateなど)の属性ノードはサブグラフを持ちませんが、ネストした関連エンティティや組み込み型の属性ノードはサブグラフを持ちます。

  • サブグラフノード ...ルートでないことを除き、エンティティグラフノードと同じです。

エンティティグラフはエンティティにアノテーションをつけて静的に定義するか、APIを用いて動的に定義します。

エンティティグラフの読み込みには「フェッチ」と「ロード」の2種類あり、

フェッチはエンティティグラフ内に定義された属性がすべてEAGERで読み込まれ、定義されていない属性はLAZYで読み込まれます

ロードはエンティティグラフ内に定義された属性がすべてEAGERで読み込まれ、定義されていない属性はデフォルトのフェッチ方式(エンティティグラフを使わない場合と同じ)で読み込まれます

アノテーションによる静的なエンティティグラフの定義

次にアノテーションによるエンティティグラフを定義する方法を説明します。 エンティティグラフノードは@NamedEntityGraphアノテーションを用いて定義します。

以下に簡単な例を示します。

@Entity
@NamedEntityGraph(
    name = "address.graph",
    attributeNodes = {
        @NamedAttributeNode("street"),
        @NamedAttributeNode("city"),
        @NamedAttributeNode("state"),
        @NamedAttributeNode("zip")
    }
)
public class Address {
    @Id
    private Integer id;
    private String street;
    private String city;
    private String state;
    private String zip;
    // ...
}

属性ノードの定義には@NamedAttributeNodeアノテーションを使用します。 主キー(@Id@EmbeddedIdのついた属性)とバージョン(@Versionのついた属性)は自動で属性ノードになります。

全属性をエンティティグラフノードに含める場合は、

@Entity
@NamedEntityGraph(includeAllAttributes=true)
public class Address {
    @Id
    private Integer id;
    private String street;
    private String city;
    private String state;
    private String zip;
    // ...
}

と定義することもできます。@NamedEntityGraphに何も指定しない場合は、各属性のデフォルトのフェッチ方式に従います。 Addressクラスの属性はすべて基本型なので、何も指定しない場合もincludeAllAttributes=trueを設定する場合も 結果的にはすべてデフォルトでEAGERになります。

なお、@NamedEntityGraphsアノテーションを用いて、一つのエンティティに複数のエンティティグラフノードを定義することができます。

もう少し複雑な例を以下に示します。

@Entity
@NamedEntityGraph(
    name = "employee.graph",
    attributeNodes = {
        @NamedAttributeNode("name"),
        @NamedAttributeNode(value = "address", subgraph = "address"),
        @NamedAttributeNode(value = "supervisor", subgraph = "supervisor")
    }, subgraphs = {
        @NamedSubgraph(name = "address", attributeNodes = {
            @NamedAttributeNode("street"),
            @NamedAttributeNode("city"),
            @NamedAttributeNode("state"),
            @NamedAttributeNode("zip")
        }),
        @NamedSubgraph(name = "supervisor", attributeNodes = {
            @NamedAttributeNode("name")
        })
    }
)
public class Employee implements Serializable {
    @Id
    private Integer id;
    private String name;

    @ManyToOne // デフォルトでEAGER
    private Department department;

    @OneToMany // デフォルトでLAZY
    private List<Address> address;

    @ManyToOne(fetch = FetchType.LAZY)
    private Employee supervisor;
    // ...
}

// Address, Departmentクラスに@NamedEntityGraphは必要ありません

サブグラフノードの定義には@NamedSubgraphアノテーションを使用します。 サブグラフの名前をルートノードの@NamedAttributeNodeのsubgraph属性に指定する必要があります。 @NamedSubgraphでもサブグラフノード内の属性ノードを@NamedAttributeNodeアノテーションを用いて定義できます。

このエンティティグラフを絵で描くと↓な感じです(雑)

image

ここで定義したEmployeeのエンティティグラフを用いて、ロードまたはフェッチを行った場合の各属性のフェッチ方式を次の表に示します。

属性名 属性ノード定義 通常のfind/クエリ フェッチ ロード
name EAGER EAGER EAGER
department EAGER LAZY(*1) EAGER
address LAZY EAGER EAGER
supervisor LAZY EAGER EAGER
supervisor.name - EAGER EAGER
supervisor.department - LAZY(*1) EAGER
supervisor.address - LAZY LAZY

(この表が分かりやすい!)

フェッチとロードの違いは@NamedAttributeNodeで定義しなかった属性の扱いです。 departmentsupervisor.departmentはフェッチの場合はLAZYになりますが、デフォルトではEAGERの設定なので、ロード時にはEAGERになります。 supervisor.addressはデフォルトがLAZYなため、フェッチの場合もロードの場合もLAZYになります。

*1 ... なんと、Hibernate 4.3の場合はEAGERになります(HHH-8776)。将来的にLAZYになる可能性があります。

エンティティグラフの使用方法

エンティティグラフは、EntityManagerのメソッドにヒントを与える形で利用可能です。 ヒント名はフェッチに場合はjavax.persistence.fetchgraph、ロードの場合はjavax.persistence.loadgraphです。

EntityGraph<?> graph = entityManager.getEntityGraph("employee.graph");
Map<String, Object> hints = new HashMap<>();
hints.put("javax.persistence.fetchgraph", graph); // フェッチするためのエンティティグラフを指定
Employee employee = entityManager.find(Employee.class, employeeId, hints);

ロードの場合は、

hints.put("javax.persistence.loadgraph", graph); // ロードするためのエンティティグラフを指定

です。

属性がロードされているかどうかはPersistenceUtil.isLoadedでチェックできます。

PersistenceUtil util = entityManager.getEntityManagerFactory().getPersistenceUnitUtil();
System.out.println("name       = " + util.isLoaded(employee, "name"));
System.out.println("department = " + util.isLoaded(employee, "department"));
System.out.println("address    = " + util.isLoaded(employee, "address"));
System.out.println("supervisor = " + util.isLoaded(employee, "supervisor"));
System.out.println("supervisor.name       = " + util.isLoaded(employee.getSupervisor(), "name"));
System.out.println("supervisor.address    = " + util.isLoaded(employee.getSupervisor(), "address"));
System.out.println("supervisor.department = " + util.isLoaded(employee.getSupervisor(), "department"));

javax.persistence.fetchgraphの場合は、

name       = true
department = false
address    = true
supervisor = true
supervisor.name       = true
supervisor.address    = false
supervisor.department = false

が出力されます(EclipseLinkのみ)。

javax.persistence.loadgraphの場合は、

name       = true
department = true
address    = true
supervisor = true
supervisor.name       = true
supervisor.address    = false
supervisor.department = true

が出力されます。

クエリを実行する場合にエンティティグラフを指定する方法は以下の通りです。

EntityGraph<?> graph = entityManager.getEntityGraph("employee.graph");
TypedQuery<Employee> query = entityManager.createQuery("SELECT x FROM Employee x", Employee.class)
                                .setHint("javax.persistence.fetchgraph", graph);

Entity Graph APIによる動的なエンティティグラフの定義

エンティティグラフはAPIでプログラマティックに定義することもできます。 前述のアノテーションで定義したエンティティグラフをAPIを使って作成する方法を以下に示します。

EntityGraph<Employee> graph = entityManager.createEntityGraph(Employee.class);
graph.addAttributeNodes("name");

Subgraph<Address> addressSubgraph = graph.addSubgraph("address");
addressSubgraph.addAttributeNodes("street", "city", "zip");

Subgraph<Employee> supervisorSubgraph = graph.addSubgraph("supervisor");
supervisorSubgraph.addAttributeNodes("name");

動的なエンティティグラフはMetaModel APIを用いて、タイプセーフに作成することもできます。

EntityGraph<Employee> graph = entityManager.createEntityGraph(Employee.class);
graph.addAttributeNodes(Employee_.name);

Subgraph<Address> addressSubgraph = graph.addSubgraph(Employee_.address);
addressSubgraph.addAttributeNodes(Address_.street, Address_.city, Address_.zip);

// ...

用途に応じてエンティティグラフを作成することで、柔軟にフェッチ戦略を選択できることがわかります。 ただし、エンティティグラフはEAGER/LAZYの制御をしてくれるだけで、N+1セレクト問題を解決してくれる訳ではありません。 EAGERの場合にSQLがまとまるかどうかは実装依存です(Hibernateの場合はまとまるけど、EclipseLinkではまとまりません)。

N+1問題を解決したい場合は、引き続きフェッチ結合かNEW式を使ってください。

しかしまあ、実装依存な部分はなんとかして欲しいですね。


こんな感じで、EclipseLinkとHibernateの実装依存部分に悩まされながら、仕様書睨めてJPAの本を書いています(辛い

出版されたら買ってください。


✒️️ Edit  ⏰ History  🗑 Delete