JPA 2.1 の新機能 Entity Graphs まとめ


f:id:Naotsugu:20150313213957p:plain

Entity Graphs とは

JPA 2.1 では Entity Graph を使うことで fetch 計画を指定できるようになりました。これにより query や find で取得する対象ををカスタマイズできるようになります。同じ Entity から様々なデータの見せ方が必要で、時にはパフォーマンスの為に最小限のデータセットのみを取得したい場合などに Entity Graph は役に立ちます。

JPA 2.0 で fetch 計画を定義する場合には EAGER / LAZY を Entity の属性に対してしていしますが、これは静的な定義であり実行時に動的に変更することができませんでした。

Entity Graph に相当する機能は Hibernate の Fetch Profiles、EclipseLink の Fetch Groups、OpenJPA の Fetch Groups として実装されていましたが、JPA 2.1 でこれらの実装が標準化された仕様として提供されました。

Entity Graph の構成

Entity Graph は以下の3要素で構成されます。

  • Entity graph nodes : 全ての entity graph のルートで、attribute node や subgraph node を含む
  • Attribute nodes : entity や embeddable 型の属性を表すノード
  • Subgraph nodes : ルート以下に構成されるサブグラフで attribute node や subgraph node を含む

以下のような Entity を例にするとがあった場合、

@Entity
public class Employee {
    @Id private long id;
    private long salary;
    @OneToOne private Address address;
    // ・・・
}

ルートとなる Employee が Entity graph node に該当し、属性である id salary address などが Attribute node に該当します。

そして Address は、Employee から見た場合の Subgraph node となります。

@Entity
public class Address {
    @Id private long id;
    private String street;
    private String city;
    // ・・・
}

Subgraph node の Address にある id street city 属性が Address の Attribute node です。

Graph アノテーションと Graph API

Graph は以下のアノテーションで定義します。

  • @NamedEntityGraph
  • @NamedAttributeNode
  • @NamedSubgraph

動的に定義する場合には、以下のようになります。

EntityGraph<Address> graph = em.createEntityGraph(Address.class);
graph.addAttributeNodes(・・・);
graph.addSubgraph(・・・);
em.getEntityManagerFactory().addNamedEntityGraph("Address", graph);

簡単な利用例

Address の属性に対して Graph 定義する場合は以下のようになります。

@Entity
@NamedEntityGraph(
    attributeNodes={
        @NamedAttributeNode("street"),
        @NamedAttributeNode("city")}
)
public class Address {
    @Id private long id;
    private String street;
    private String city;
    private String state;
    // ・・・
}

定義した Entity Graph は以下のようにして使います。

EntityGraph<?> graph = em.getEntityGraph("Address");
TypedQuery<Address> query = em.createQuery("SELECT ・・・", Address.class);
query.setHint("javax.persistence.fetchgraph", graph);
List<Address> results =query.getResultList();

定義した Entity Graph を setHint() で設定します。この時、javax.persistence.fetchgraph というキーを一緒に渡します。

find() の場合には、Map で定義したプロパティを渡します。

Map<String, Object> props = new HashMap<>();
props.put("javax.persistence.fetchgraph",
          em.getEntityGraph("Address"));
Address address = em.find(Address.class, id, props);

Fetch Graph と Load Graph

Entity Graph を利用するにはヒントとして定義済みのキーを渡す必要があります。このキーは現時点で以下の2種類が用意されています。

  • javax.persistence.fetchgraph
  • javax.persistence.loadgraph

javax.persistence.fetchgraph は Entity Graph で指定した属性が EAGER となり、指定しなかったものは LAZY となります。

一方 javax.persistence.loadgraph は Entity Graph で指定した属性が EAGER となるのは同じですが、指定しなかったものは Entity に事前定義してある FetchType (指定のないものはデフォルトのルール) が適用されます。

Glassfish 4.1 の EclipseLink では javax.persistence.loadgraph が正しく処理されないバグがあるようです。Eclipselink 2.6 で修正されています。

Attribute Node の定義

AttributeNode は @NamedEntityGraph アノテーションの中の attributeNodes に値を設定して定義します。

@Entity
@NamedEntityGraph(
    attributeNodes={
        @NamedAttributeNode("street"),
        @NamedAttributeNode("city"),
        @NamedAttributeNode("state"),
        @NamedAttributeNode("zip")}
)
public class Address {
    @Id private long id;
    private String street;
    private String city;
    private String state;
    private String zip;
    // ・・・
}

@NamedAttributeNode でそれぞれの属性を登録しています。

この例では、作成した Entity Graph に明示的に名前を指定していないので、デフォルトの命名規則が適用され Address という名前が付きます。

単に全ての属性を含める場合には、以下のように指定できます。

@Entity
@NamedEntityGraph(includeAllAttributes=true)
public class Address { }

そもそもこの Entity は 属性のみなのでデフォルトで全てのフィールドが Eager となるので、以下のように定義しても同じことになります。

@Entity
@NamedEntityGraph
public class Address { }

Subgraph の定義

以下のような Entity があったとします。

f:id:Naotsugu:20150308164734p:plain

Employee は以下のような定義を考えます。

@Entity
public class Employee {
    @Id 
    private long id;
    
    private String name;
    
    private long salary;
    
    @Temporal(TemporalType.DATE)
    private Date srartDate;
    
    @OneToOne
    private Address address;
    
    @OneToMany(mappedBy="employee")
    private Collection<Phone> phones = new ArrayList<>();
    
    @ManyToOne
    private Department department;
    
    @ManyToOne
    private Employee department;
    
    @OneToMany(mappedBy="manager")
    private Collection<Employee> directs = new ArrayList<>();
    
    @ManyToMany(mappedBy="employees")
    private Collection<Project> projects = new ArrayList<>();
    
    // ・・・
}

この Entity に Subgraph を定義すると以下のようになります。

@Entity
@NamedEntityGraph(name="Employee.graph1",
    attributeNodes={
        @NamedAttributeNode("name"),
        @NamedAttributeNode("salary"),
        @NamedAttributeNode(value="address"),
        @NamedAttributeNode(value="phones", subgraph="phone"),
        @NamedAttributeNode(value="department", subgraph="dept")
    },
    subgraphs={
        @NamedSubgraph(name="phone",
            attributeNodes={
                @NamedAttributeNode("number"),
                @NamedAttributeNode("type")
            }
        ),
        @NamedSubgraph(name="dept",
            attributeNodes={
                @NamedAttributeNode("name")
            }
        )
    }
)
public class Employee { ・・・ }

Employee.graph1 という名前で NamedEntityGraph を定義し、phonedept という名前の Subgraph を @NamedSubgraph にて定義しています。

定義した attributeNodes の phonesdepartment に subgraph を名前で指定します。address に対してはデフォルトを利用するので subgraph 定義はしていません。 この定義にて attribute と subgraph 中の attribute が fetch 対象として扱えるようになります。

Qritria API を使った Entity Graph の定義は以下のようになります。

EntityGraph<Employee> graph = em.createEntityGraph(Employee.class);
graph.addAttributeNodes("name", "salary", "address");

Subgraph<Phone> phone = graph.addSubgraph("phones");
phone.addAttributeNodes("number", "type");

Subgraph<Department> dept = graph.addSubgraph("department");
dept.addAttributeNodes("name");

createEntityGraph() で 作成し、作成した graph に対して add していきます。

Subgraph の複数参照

subgraph は、名前で参照することで、複数の AttributeNode から同じものを参照できます。 以下の例は namedEmp という名前の subgraph を複数箇所で参照する例です。

@Entity
@NamedEntityGraph(name="Employee.graph2",
    attributeNodes={
        @NamedAttributeNode("name"),
        @NamedAttributeNode("salary"),
        @NamedAttributeNode(value="address"),
        @NamedAttributeNode(value="phones", subgraph="phone"),
        @NamedAttributeNode(value="manager", subgraph="namedEmp"),
        @NamedAttributeNode(value="department", subgraph="dept")
    },
    subgraphs={
        @NamedSubgraph(name="phone",
            attributeNodes={
                @NamedAttributeNode("number"),
                @NamedAttributeNode("type"),
                @NamedAttributeNode(value="employee", subgraph="namedEmp")
            }
        ),
        @NamedSubgraph(name="namedEmp",
            attributeNodes={
                @NamedAttributeNode("name")
            }
        ),
        @NamedSubgraph(name="dept",
            attributeNodes={
                @NamedAttributeNode("name")
            }
        )
    }
)
public class Employee { ・・・ }

manager 属性と phone の employee が同じ namedEmp という subgraph を参照しています。

Criteria API を利用した例は以下のようになります。

EntityGraph<Employee> graph = em.createEntityGraph(Employee.class);
graph.addAttributeNodes("name", "salary", "address");

Subgraph<Phone> phone = graph.addSubgraph("phones");
phone.addAttributeNodes("number", "type");
Subgraph<Employee> namedEmp = phone.addSubgraph("employee");
namedEmp.addAttributeNodes("name");

Subgraph<Employee> mgrNamedEmp = graph.addSubgraph("manager");
mgrNamedEmp.addAttributeNodes("name");

Subgraph<Department> dept = graph.addSubgraph("department");
dept.addAttributeNodes("name");

こちらの方が直感的に理解しやすいですね。

継承構造の Graph 定義

継承構造に対する定義は type で指定することで fetch 対象を特定することができます。

@Entity
@NamedEntityGraph(name="Employee.graph3",
    attributeNodes={
        @NamedAttributeNode("name"),
        @NamedAttributeNode(value="projects", subgraph="project")
    },
    subgraphs={
        @NamedSubgraph(name="project", type=Project.class,
            attributeNodes={
                @NamedAttributeNode("name")
            }
        ),
        @NamedSubgraph(name="project", type=QualityProject.class,
            attributeNodes={
                @NamedAttributeNode("qaRating")
            }
        )
    }
)
public class Employee {  }

Project の子クラスである QualityProject の場合には "qaRating" を fetch 対象としています。

API での定義は以下のようになります。

EntityGraph<Employee> graph = em.createEntityGraph(Employee.class);
graph.addAttributeNodes("name", "salary", "address");

Subgraph<Project> project = graph.addSubgraph("projects", Project.class);
project.addAttributeNodes("name");

Subgraph<QualityProject> qaProject = graph.addSubgraph("projects", QualityProject.class);
qaProject.addAttributeNodes("qaRating");

ルート継承構造の Graph

前の例では Subgraph が継承構造に例でしたが、ルート要素自体が継承構造を持つ場合には、subclassSubgraphs にて定義する必要があります。

@Entity
@NamedEntityGraph(name="Employee.graph4",
    attributeNodes={
        @NamedAttributeNode("name"),
        @NamedAttributeNode("address"),
        @NamedAttributeNode(value="department", subgraph="dept")
    },
    subgraphs={
        @NamedSubgraph(name="dept",
            attributeNodes={
                @NamedAttributeNode("name")
            }
        )
    },
    subclassSubgraphs={
        @NamedSubgraph(name="notUsed", type=ContractEmployee.class,
            attributeNodes={
                @NamedAttributeNode("hourlyRate")
            }
        )
    }
)
public class Employee {  }

この例ではEmployee の子として ContractEmployee があり、 ContractEmployee の属性の hourlyRate を取得する定義となっています。 @NamedSubgraph には name を省略できないため、この名前を利用していない意味で "notUsed" という名前を仮でつけています。

API による定義は以下となります。

EntityGraph<Employee> graph = em.createEntityGraph(Employee.class);
graph.addAttributeNodes("name", "address");
graph.addSubgraph("department").addAttributeNodes("name");
graph.addSubclassSubgraph(ContractEmployee.class).addAttributeNode("hourlyRate");

Map Key Subgraphs

関連が @MapKey で定義してある場合には keySubgraph という指定が必要です。

以下のような Embeddable なオブジェクトがあり、

@Embeddable
public class EmployeeName {
    private String firstName;
    private String lastName;
    // ・・・
}

@MapKeyマッピングされていた場合

@Entity
public class Department {
    @Id private long id;
    private String name;
    @OneToMany(mappedBy="department")
    @MapKey(name="name")
    private Map<EmployeeName, Employee> employees;
    // ・・・
}

keySubgraph にて subgraph を指定します。

@Entity
@NamedEntityGraph(name="Department.graph1",
    attributeNodes={
        @NamedAttributeNode("name"),
        @NamedAttributeNode(value="employees",
            subgraph="emp",
            keySubgraph="empName"
        )
    },
    subgraphs={
        @NamedSubgraph(name="emp",
            attributeNodes={
                @NamedAttributeNode(value="name",
                    subgraph="empName"),
                @NamedAttributeNode("salary")
            }
        ),
        @NamedSubgraph(name="empName",
            attributeNodes={
                @NamedAttributeNode("firstName"),
                @NamedAttributeNode("lastName")
            }
        )
    }
)
public class Department { ・・・ }

API で定義する場合は以下です。

EntityGraph<Department> graph = em.createEntityGraph(Department.class);
graph.addAttributeNodes("name");
graph.addSubgraph("employees").addAttributeNodes("salary");
graph.addKeySubgraph("employees").addAttributeNodes("firstName", "lastName");

タイプセーフな属性定義

今までの例は属性の定義を文字列で指定していましたが、

EntityGraph<Address> graph = em.createEntityGraph(Address.class);
graph.addAttributeNodes("street", "city", "state", "zip");

もちろん static meta model で指定することもできます。

graph.addAttributeNodes(Address_.street, Address_.city, Address_.state, Address_.zip);

Entity Graph の取得

名前付きの Entity Graph は以下のように名前を指定して取得します。

EntityGraph<?> graph = em.getEntityGraph("Employee.graph2");

取得した Entity Graph は以下のように中身を巡ることができます。

List<EntityGraph<? super Employee>> egList =
        em.getEntityGraphs(Employee.class);
for (EntityGraph<? super Employee> graph : egList) {
    System.out.println("EntityGraph:" + graph.getName());
    List<AttributeNode<?>> attribs = graph.getAttributeNodes();
    for (AttributeNode<?> attr : attribs) {
        System.out.println("  Attribute:" + attr.getAttributeName());        
    }
}

名前付きの Entity Graph への追加

Entity Graph は動的に生成して名前付きとして登録することもできます。

EntityGraph<?> graph = em.createEntityGraph("Employee.graph2");
graph.addAttributeNodes("projects");
em.getEntityManagerFactory().addNamedEntityGraph("Employee.newGraph", graph);

createEntityGraph で取得した EntityGraph に追加して EntityManagerFactory へ追加しています。

createEntityGraph で取得した EntityGraph は変更可能な コピー なので安全に変更して保存できます。

EntityGraph<?> createEntityGraph(String graphName)

Return a mutable copy of the named EntityGraph. If there is no entity graph with the specified name, null is returned.

Entity Graph の利用

前述しましたが、Entity Graph は javax.persistence.fetchgraph または javax.persistence.loadgraph のキーを指定して使います。

Map<String, Object> props = new HashMap<>();
props.put("javax.persistence.fetchgraph", em.getEntityGraph("Employee.graph2"))
Employee emp = em.find(Employee.class, empId, props);

query の場合は以下のようになります。

EntityGraph<Employee> graph = em.createEntityGraph(Employee.class);
graph.addAttributeNodes("name", "salary", "address");
// ・・・

TypedQuery<Employee> query = em.createQuery(
    "SELECT e FROM Employee e WHERE e.salary > 50000", graph);
query.setHint("javax.persistence.fetchgraph", graph);
List<Employee> results = query.getResultList();

fetch は ID と VERSION 属性があれば必ず EAGER で fetch されます。

Entity Graph に属性を定義し、そのマッピング先の Subgraph の定義を省略した場合には Entity に定義されたデフォルトの FetchType が適用されます。

fetch した結果が LAZY でまだ読み込まれていないかどうかは PersistenceUnitUtil.isLoaded() でテストできます。これによりプロバイダ実装でによる挙動の違いが確認できます。

ある特定の属性値のみを取得したい場合、javax.persistence.fetchgraph が便利ですが、大部分の属性を取得したいが幾つかは取得したくない場合、Entity Graph に大部分の属性を明示的する必要があります。将来的には https://java.net/jira/browse/JPA_SPEC-65 で語られているように、javax.persistence.lazygraph が使えるようになるかも知れません。

javax.persistence.lazygraph は LAZY で取得したい属性を指定し、指定がないものはデフォルト定義に従うというものになります。

Pro JPA 2 (Expert's Voice in Java)

Pro JPA 2 (Expert's Voice in Java)