はじめに
Doma 2.43.0のCriteria APIで結合を使う方法を説明します。
DomaのCriteira APIでは、「単純に結合をするだけの場合」と「結合した上で関連エンティティを取得する場合」の2つのパターンをサポートします。後者については意味合いとしてはJPAのjoin fetchに少し似ています。
DomaやCriteria APIの概要についてはDoma入門の他の記事もお読みください。
この記事で使うサンプルコード
ここでは、簡略化したコードで説明します。
完全なサンプルコードをについては下記のプロジェクトを参照してください。
データベースには部署を表すdepartmentテーブルと従業員を表すemployeeテーブルがあります。
create table department (
id integer not null primary key,
name varchar(255) not null);
create table employee (
id integer not null primary key,
name varchar(255) not null,
department_id integer);
departmentテーブルに対応するDepartmentクラスとemployeeテーブルに対応するEmployeeクラスを用意します。双方向関連を表せるようにDepartmentクラスにはemployeesプロパティを、Employeeクラスにはdepartmentプロパティを用意し@Transient
を注釈します。@Transient
は、そのプロパティをデータベースのカラムにマッピングしないことを示します。
@Entity(metamodel = @Metamodel)
public class Department {
@Id Integer id;
String name;
@Transient List<Employee> employees = new ArrayList<>();
// getter, setter
}
@Entity(metamodel = @Metamodel)
public class Employee {
@Id
Integer id;
String name;
@Column(name = "DEPARTMENT_ID")
Integer departmentId;
@Transient Department department;
// getter, setter
}
EmployeeRepository
を用意し、このクラスにメソッドを追加して例を示していきます。
public class EmployeeRepository {
private final Entityql entityql;
public EmployeeRepository(Config config) {
this.entityql = new Entityql(config);
}
}
単純な結合
単純な結合の例は以前の記事に書いたのでこちらを参照ください。
単純な結合は関連オブジェクトを取得しないので、@Transient
なプロパティ定義は必須ではありませんが、状況によって関連オブジェクトを取得するしないを使い分けるので、基本は定義しておくで良いと思います。
関連エンティティを取得する結合
関連エンティティを取得するにはleftJoin
もしくはinnerJoin
メソッドを呼び出した後で、associate
メソッドを呼び出します。associate
メソッドには、関連づけを行うラムダ式を渡します。
public List<Employee> selectAllWithAssociation() {
Employee_ e = new Employee_();
Department_ d = new Department_();
return entityql
.from(e)
.leftJoin(d, on -> on.eq(e.departmentId, d.id))
.associate(e, d, (employee, department) -> {
employee.setDepartment(department);
department.getEmployees().add(employee);
})
.fetch();
}
associate
メソッドに渡すラムダ式は全従業員の件数分呼び出されます。
実行されるSQLは次のようなものです。
select
t0_.id,
t0_.name,
t0_.DEPARTMENT_ID,
t1_.id,
t1_.name
from
Employee t0_
left outer join
Department t1_
on
(t0_.DEPARTMENT_ID = t1_.id)
selectリストにemployeeテーブルとdepartmentテーブルの両方のカラムが含まれるのがわかりますね。SQL1発で取得するのでN+1問題は発生しません。
上記の例ではleftJoin
とassociate
メソッドをそれぞれ1度ずつ読んだだけですが、下記のサンプルコードのようにどちらも1度のクエリの中で何度も呼び出すことができます。
public Order selectOrder(int orderId) {
var order_ = new Order_();
var orderStatus_ = new OrderStatus_();
var orderLineItem_ = new OrderLineItem_();
var item_ = new Item_();
var inventory_ = new Inventory_();
var product_ = new Product_();
return entityql
.from(order_)
.innerJoin(orderStatus_, on -> on.eq(order_.orderId, orderStatus_.orderId))
.leftJoin(orderLineItem_, on -> on.eq(order_.orderId, orderLineItem_.orderId))
.leftJoin(item_, on -> on.eq(orderLineItem_.itemId, item_.itemId))
.innerJoin(inventory_, on -> on.eq(item_.itemId, inventory_.itemId))
.innerJoin(product_, on -> on.eq(item_.productId, product_.productId))
.where(c -> c.eq(order_.orderId, orderId))
.associate(order_, orderStatus_, Order::setOrderStatus)
.associate(order_, orderLineItem_, Order::addLineItem)
.associate(orderLineItem_, item_, OrderLineItem::setItem)
.associate(item_, inventory_, Item::setInventory)
.associate(item_, product_, Item::setProduct)
.fetchOne();
}
この例はサンプルアプリであるspring-boot-jpetstoreの一部です。
特に制限なく関連先をいくつも辿って取得できます。例えば、上のサンプルコードで言うと、Order → OrderLineItem → Item → Productと辿って関連エンティティを取得できています。ただし、SQLで取得するデータ量が大きくなる傾向にあるのでその点は注意してください。
関連づけコードの再利用
上記のようなクエリを書いていると関連づけのコード(associate
メソッドに渡すラムダ式)の重複が気になるかもしれません。その場合は、ラムダ式をクラスとして表現してしまうのがお勧めです。
public List<Employee> selectAllWithAssociation() {
Employee_ e = new Employee_();
Department_ d = new Department_();
return entityql
.from(e)
.leftJoin(d, on -> on.eq(e.departmentId, d.id))
.associate(e, d, new Employees_Department())
.fetch();
}
associate
メソッドのラムダ式は単なるBiConsumer
ですので実装クラスを作ればOKです。
public class Employees_Department implements BiConsumer<Employee, Department> {
@Override
public void accept(Employee employee, Department department) {
employee.setDepartment(department);
department.getEmployees().add(employee);
}
}
おわりに
DomaのCriteria APIで結合を使う方法を紹介しました。
Domaでは、JPAのようにエンティティクラスに@OneToMany
など関連を表すアノテーションを静的に記述することは求めません。その分Criteria APIを使うクエリにおいて関連づけの処理を明示的に記述する必要がありますが、クエリごとに柔軟な記述ができます。
この記事の例では、最も一般的な方法ということで@Transient
を使って関連エンティティをエンティティのプロパティで表現しましたが、好みによってはassociate
メソッドのラムダ式で得られるエンティティを全く別のオブジェクトに持たせることも可能です。