--- title: JPA 2.1のエンティティグラフについて tags: ["JPA", "Java", "Java EE 7"] categories: ["Programming", "Java", "JPA"] date: 2015-07-20T16:59:24Z updated: 2015-07-20T16:59:24Z --- JPA 2.1の分かりにくい新機能であるエンティティグラフについて、訳あって調べおり、 カッとなったので解説記事を書きます。(他の解説記事がいまいちわかりづらかった) ---------- エンティティグラフはその名前から推測される「エンティティのグラフ」というよりもエンティティと属性のひとかたまりのグループのようなものです。 findメソッドやクエリ実行時のフェッチの戦略(EAGERにするかLAZYにするか)をオーバーライドするために利用されます。 エンティティグラフはJPA 2.1で導入されました。 エンティティグラフが導入される前は、エンティティの属性毎に`FetchType.EAGER`や`FetchType.LAZY`を付けるか、 クエリ内でフェッチ結合を用いることでしかフェッチ戦略を決めることができず、柔軟性に欠けました。 エンティティグラフを用いることで、グループ化したエンティティと属性の集合にフェッチ戦略を決めることができ、 ユースケース毎にEAGER/LAZYの有効・無効を切り替えられます。 エンティティグラフでは次の3のオブジェクトが登場します。 * **エンティティグラフノード** ...エンティティグラフのルートとなるノードであり、すべての属性やルートに属するサブグラフを含みます。 * **属性ノード** ...エンティティの属性を表すノード。 基本型(プリミティブやそのラッパー、String、Dateなど)の属性ノードはサブグラフを持ちませんが、ネストした関連エンティティや組み込み型の属性ノードはサブグラフを持ちます。 * **サブグラフノード** ...ルートでないことを除き、エンティティグラフノードと同じです。 エンティティグラフはエンティティにアノテーションをつけて静的に定義するか、APIを用いて動的に定義します。 エンティティグラフの読み込みには「フェッチ」と「ロード」の2種類あり、 **フェッチはエンティティグラフ内に定義された属性がすべてEAGERで読み込まれ、定義されていない属性はLAZYで読み込まれます**。 **ロードはエンティティグラフ内に定義された属性がすべてEAGERで読み込まれ、定義されていない属性はデフォルトのフェッチ方式(エンティティグラフを使わない場合と同じ)で読み込まれます**。 ### アノテーションによる静的なエンティティグラフの定義 次にアノテーションによるエンティティグラフを定義する方法を説明します。 エンティティグラフノードは`@NamedEntityGraph`アノテーションを用いて定義します。 以下に簡単な例を示します。 ``` java @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`のついた属性)は自動で属性ノードになります。 全属性をエンティティグラフノードに含める場合は、 ``` java @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`アノテーションを用いて、一つのエンティティに複数のエンティティグラフノードを定義することができます。 もう少し複雑な例を以下に示します。 ``` java @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; @ManyToOne(fetch = FetchType.LAZY) private Employee supervisor; // ... } // Address, Departmentクラスに@NamedEntityGraphは必要ありません ``` サブグラフノードの定義には`@NamedSubgraph`アノテーションを使用します。 サブグラフの名前をルートノードの`@NamedAttributeNodeのsubgraph`属性に指定する必要があります。 `@NamedSubgraph`でもサブグラフノード内の属性ノードを`@NamedAttributeNode`アノテーションを用いて定義できます。 このエンティティグラフを絵で描くと↓な感じです(雑) ![image](https://qiita-image-store.s3.amazonaws.com/0/1852/72a82830-19b8-0e3c-e597-9688b6acf88a.png) ここで定義した`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`で定義しなかった属性の扱いです。 `department`、`supervisor.department`はフェッチの場合はLAZYになりますが、デフォルトではEAGERの設定なので、ロード時にはEAGERになります。 `supervisor.address`はデフォルトがLAZYなため、フェッチの場合もロードの場合もLAZYになります。 *1 ... なんと、Hibernate 4.3の場合はEAGERになります([HHH-8776](https://hibernate.atlassian.net/browse/HHH-8776))。将来的にLAZYになる可能性があります。 ### エンティティグラフの使用方法 エンティティグラフは、`EntityManager`のメソッドにヒントを与える形で利用可能です。 ヒント名はフェッチに場合は`javax.persistence.fetchgraph`、ロードの場合は`javax.persistence.loadgraph`です。 ``` java EntityGraph graph = entityManager.getEntityGraph("employee.graph"); Map hints = new HashMap<>(); hints.put("javax.persistence.fetchgraph", graph); // フェッチするためのエンティティグラフを指定 Employee employee = entityManager.find(Employee.class, employeeId, hints); ``` ロードの場合は、 ``` java hints.put("javax.persistence.loadgraph", graph); // ロードするためのエンティティグラフを指定 ``` です。 属性がロードされているかどうかは`PersistenceUtil.isLoaded`でチェックできます。 ``` java 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 ``` が出力されます。 クエリを実行する場合にエンティティグラフを指定する方法は以下の通りです。 ``` java EntityGraph graph = entityManager.getEntityGraph("employee.graph"); TypedQuery query = entityManager.createQuery("SELECT x FROM Employee x", Employee.class) .setHint("javax.persistence.fetchgraph", graph); ``` ### Entity Graph APIによる動的なエンティティグラフの定義 エンティティグラフはAPIでプログラマティックに定義することもできます。 前述のアノテーションで定義したエンティティグラフをAPIを使って作成する方法を以下に示します。 ``` java EntityGraph graph = entityManager.createEntityGraph(Employee.class); graph.addAttributeNodes("name"); Subgraph
addressSubgraph = graph.addSubgraph("address"); addressSubgraph.addAttributeNodes("street", "city", "zip"); Subgraph supervisorSubgraph = graph.addSubgraph("supervisor"); supervisorSubgraph.addAttributeNodes("name"); ``` 動的なエンティティグラフはMetaModel APIを用いて、タイプセーフに作成することもできます。 ``` java EntityGraph graph = entityManager.createEntityGraph(Employee.class); graph.addAttributeNodes(Employee_.name); Subgraph
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の本を書いています(辛い 出版されたら買ってください。