JPA 2.1の分かりにくい新機能であるエンティティグラフについて、訳あって調べおり、 カッとなったので解説記事を書きます。(他の解説記事がいまいちわかりづらかった)
エンティティグラフはその名前から推測される「エンティティのグラフ」というよりもエンティティと属性のひとかたまりのグループのようなものです。 findメソッドやクエリ実行時のフェッチの戦略(EAGERにするかLAZYにするか)をオーバーライドするために利用されます。
エンティティグラフはJPA 2.1で導入されました。
エンティティグラフが導入される前は、エンティティの属性毎にFetchType.EAGER
やFetchType.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
アノテーションを用いて定義できます。
このエンティティグラフを絵で描くと↓な感じです(雑)
ここで定義した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)。将来的に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の本を書いています(辛い
出版されたら買ってください。