NetBeans6.8でJPA2.0を試してみる。 - その7

 さて、間隔が開きすぎてしまいましたが、Criteriaでの外部結合です。そういえばNetBeans6.8でやってるんですが、やはりMetamodelの自動生成機能はありません・・・。これは正直かなり悲しいです。というかMetamodelを自力で書くの超メンドクサイです・・・。

外部結合

 まぁ、NetBeansの話は置いておくとして外部結合ですがとりあえず下記のような単純なテーブルとデータを用意します。

・EMPLOYEEテーブル

EMPLOYEE_ID EMPLOYEE_NAME
1 TEST1
2 TEST2
3 TEST3
4 TEST4

・EXPENSEテーブル

EXPENSE_ID EMPLOYEE_ID AMOUNT
1 1 200
2 2 1500

 さて、この状態でEMPLOYEE_IDが4より小さいEMPLOYEEの情報とそれに付随するEXPENSEの情報を取得したいとします。従業員には経費を使用していない会社にとって大変ありがたい人もいるのでSQLでこういう情報を取得する場合は外部結合になります。んで、これをJPA2.0のCriteriaAPIで書くとどうなるかですが、下記のようになります。

・Employeeクラス(Entity)

@Entity
@Table(name = "EMPLOYEE")
public class Employee implements Serializable {
    private static final long serialVersionUID = 1L;
    @Id
    @Column(name = "EMPLOYEE_ID")
    private Integer employeeId;
    @Column(name = "EMPLOYEE_NAME")
    private String employeeName;
    @OneToMany(cascade = CascadeType.ALL, mappedBy = "employee")
    private List<Expense> expenseList;
    //アクセサ省略
}

・Expenseクラス(エンティティ)

@Entity
@Table(name = "Expense")
public class Expense implements Serializable {
    private static final long serialVersionUID = 1L;
    @Id
    @Column(name = "EXPENSE_ID")
    private Integer expenseId;
    @Column(name = "AMOUNT")
    private int amount;
    @JoinColumn(name = "EMPLOYEE_ID", referencedColumnName = "EMPLOYEE_ID")
    @ManyToOne
    private Employee employee;
    //アクセサは省略
}

・Employee_クラス(Employeeに対するMetamodelクラス)

@StaticMetamodel(Employee.class)
public class Employee_ {
    public static volatile SingularAttribute<Employee,Integer> employeeId;
    public static volatile SingularAttribute<Employee,String> employeeName;
    public static volatile ListAttribute<Employee,Expense> expenseList;
}

・Expense_クラス(Expenseに対するMetamodelクラス)

@StaticMetamodel(Expense.class)
public class Expense_ {
    public static volatile SingularAttribute<Expense,Integer> expenseId;
    public static volatile SingularAttribute<Expense,Employee> employee;
    public static volatile SingularAttribute<Expense,Integer> amount;
}

んで、実際にCriteriaを構築すると下記のようになります。

        EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa2PU");
        EntityManager em = emf.createEntityManager();

        CriteriaBuilder b = emf.getCriteriaBuilder();
        CriteriaQuery<Employee> q = b.createQuery(Employee.class);
        Root<Employee> emp = q.from(Employee.class);
        emp.join(Employee_.expenseList, JoinType.LEFT);
        q.select(emp).where(b.lt(emp.get(Employee_.employeeId), 4));
        for(Employee e:em.createQuery(q).getResultList()){
            System.out.println("EmployeeId = " + e.getEmployeeId() + 
                ",EmployeeId = " +e.getEmployeeName());
            for(Expense expense:e.getExpenseList()){
                System.out.println("\tExpenseId = "+expense.getExpenseId()+
                  ", Amount = " + expense.getAmount());
            }
            System.out.println("=============================");
        }

んで、上記の出力結果は下記になります。

EmployeeId = 1,EmployeeId = TEST1
        ExpenseId = 1, Amount = 200
=============================
EmployeeId = 2,EmployeeId = TEST2
        ExpenseId = 2, Amount = 1500
=============================
EmployeeId = 3,EmployeeId = TEST3
=============================

きちんとDepartmentがnullのEmployeeも取れています。

実際に発行されるSQLは下記になります。

SELECT t1.EMPLOYEE_ID, t1.EMPLOYEE_NAME 
FROM EMPLOYEE t1 LEFT OUTER JOIN EXPENSE t0 
ON (t0.EMPLOYEE_ID = t1.EMPLOYEE_ID) 
WHERE (t1.EMPLOYEE_ID < 4)

SELECT EXPENSE_ID, AMOUNT, EMPLOYEE_ID 
FROM EXPENSE 
WHERE (EMPLOYEE_ID = 1)

SELECT EXPENSE_ID, AMOUNT, EMPLOYEE_ID 
FROM EXPENSE 
WHERE (EMPLOYEE_ID = 2)

SELECT EXPENSE_ID, AMOUNT, EMPLOYEE_ID 
FROM EXPENSE 
WHERE (EMPLOYEE_ID = 3)

Lazy FetchになっているのでExpenseに対するSQLの発行はコレクションに対してアクセスした時点になりますが、Employeeに対するSQLの発行はきちんとLeft outer joinになっています。
ポイントは

emp.join(Employee_.expenseList, JoinType.LEFT);

この部分ですね、ここで結合方法は左側外部結合だと明示的に指定しています。

あえて外部結合を使わない方法

とはいえ、このくらいの単純な外部結合の場合、JPA使っていて厳密にエンティティ間の参照を定義していればぶっちゃけこんな面倒くさいことする必要ありません。上記の場合だと条件指定はemployeeのidが4より少なければいいので下記内容と同じになります。

        q.select(emp).where(b.lt(emp.get(Employee_.employeeId),4));//Employeeに対して条件を指定すればいい。
        //下記内容は同じ
        for(Employee e:em.createQuery(q).getResultList()){
            System.out.println("EmployeeId = " + e.getEmployeeId() + 
                               ",EmployeeId = " +e.getEmployeeName());
            for(Expense expense:e.getExpenseList()){
                System.out.println("\tExpenseId = "+expense.getExpenseId()+
                                   ", Amount = " + expense.getAmount());
            }
            System.out.println("=============================");
        }

Fetch Join

 しかし、上記2つの場合でもExpenseの情報を取得するために発行されるSQLはコレクションに対してアクセスした場合になります(Employee#getExpenseList())。パフォーマンスの必要性からSQLを発行する回数を少なくしたい*1場合に使うのがFetch Joinになります。Fetch Joinを使用するにはemp.joinをemp.fetchに替えるだけです。

        emp.fetch(Employee_.expenseList,JoinType.LEFT); //joinをfetchに
        q.select(emp).where(b.lt(emp.get(Employee_.employeeId), 4));

これで発行されるSQLは下記になります。

SELECT 
  t1.EMPLOYEE_ID, 
  t1.EMPLOYEE_NAME, 
  t0.EXPENSE_ID, 
  t0.AMOUNT, 
  t0.EMPLOYEE_ID 
FROM 
  EMPLOYEE t1 LEFT OUTER JOIN EXPENSE t0 ON 
  (t0.EMPLOYEE_ID = t1.EMPLOYEE_ID) 
WHERE (t1.EMPLOYEE_ID < ?)

 結合に関してはこんな感じですかねぇ〜。個人的にはJPAを使うようになってからは外部結合はFetchと絡めたとしてもほぼ使用していません*2SQLを意識しないでオブジェクトを取ってくる感覚で書けるのが個人的に合ってるみたいです。

 次回はCriteriaに関して今まで試してきた感想とかをまとめようかなぁ〜とか思ってます。

*1:EMPLOYEEの情報を取得する段階でEXPENSEの情報も取得する

*2:生でSQL書くときは使いますが