Doma入門 - Criteira APIで結合を使う

## はじめに

[Doma](https://github.com/domaframework/doma) 3.6.0のCriteria APIで結合を使う方法を説明します。

DomaのCriteira APIでは、「単純に結合をするだけの場合」と「結合した上で関連エンティティを取得する場合」の2つのパターンをサポートします。後者については意味合いとしてはJPAのjoin fetchに少し似ています。

DomaやCriteria APIの概要については[Doma入門](https://qiita.com/nakamura-to/items/deaff05ba530317f3cf8)の他の記事もお読みください。

## この記事で使うサンプルコード

ここでは、簡略化したコードで説明します。
完全なサンプルコードをについては下記のプロジェクトを参照してください。

- https://github.com/domaframework/simple-examples

データベースには部署を表すdepartmentテーブルと従業員を表すemployeeテーブルがあります。

```sql:schema.sql
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プロパティを用意し`@Association`を注釈します。`@Association`は、そのプロパティをデータベースのカラムにマッピングしないことを示します。

```java:Department.java
@Entity(metamodel = @Metamodel)
public class Department {
  @Id Integer id;

  String name;

  @Association List<Employee> employees = new ArrayList<>();

  // getter, setter
}
```

```java:Employee.java
@Entity(metamodel = @Metamodel)
public class Employee {

  @Id
  Integer id;

  String name;

  @Column(name = "DEPARTMENT_ID")
  Integer departmentId;

  @Association Department department;

  // getter, setter
}
```

`EmployeeRepository`を用意し、このクラスにメソッドを追加して例を示していきます。

```java:EmployeeRepository.java
public class EmployeeRepository {

  private final queryDsl queryDsl;

  public EmployeeRepository(Config config) {
    this.queryDsl = new queryDsl(config);
  }
}
```

## 単純な結合

単純な結合の例は以前の記事に書いたので[こちら](https://qiita.com/nakamura-to/items/77d9e56aff890d7757b3#%E5%90%8C%E7%AD%89%E3%81%AE%E6%A4%9C%E7%B4%A2%E7%B5%90%E6%9E%9C%E3%82%92join%E5%8F%A5%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E5%AE%9F%E7%8F%BE)を参照ください。

単純な結合は関連オブジェクトを取得しないので、`@Association`なプロパティ定義は必須ではありませんが、状況によって関連オブジェクトを取得するしないを使い分けるので、基本は定義しておくで良いと思います。

## 関連エンティティを取得する結合

関連エンティティを取得するには`leftJoin`もしくは`innerJoin`メソッドを呼び出した後で、`associate`メソッドを呼び出します。`associate`メソッドには、関連づけを行うラムダ式を渡します。

```java
  public List<Employee> selectAllWithAssociation() {
    Employee_ e = new Employee_();
    Department_ d = new Department_();
    return queryDsl
        .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は次のようなものです。

```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度のクエリの中で何度も呼び出すことができます。

```java
  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 queryDsl
        .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();
  }
```

特に制限なく関連先をいくつも辿って取得できます。例えば、上のサンプルコードで言うと、Order → OrderLineItem → Item → Productと辿って関連エンティティを取得できています。ただし、SQLで取得するデータ量が大きくなる傾向にあるのでその点は注意してください。

## 関連づけコードの再利用

上記のようなクエリを書いていると関連づけのコード（`associate`メソッドに渡すラムダ式）の重複が気になるかもしれません。その場合は、ラムダ式をクラスとして表現してしまうのがお勧めです。

```java
  public List<Employee> selectAllWithAssociation() {
    Employee_ e = new Employee_();
    Department_ d = new Department_();
    return queryDsl
        .from(e)
        .leftJoin(d, on -> on.eq(e.departmentId, d.id))
        .associate(e, d, new Employees_Department())
        .fetch();
  }
```

`associate`メソッドのラムダ式は単なる`BiConsumer`ですので実装クラスを作ればOKです。

```java
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を使うクエリにおいて関連づけの処理を明示的に記述する必要がありますが、クエリごとに柔軟な記述ができます。

この記事の例では、最も一般的な方法ということで`@Association`を使って関連エンティティをエンティティのプロパティで表現しましたが、好みによっては`associate`メソッドのラムダ式で得られるエンティティを全く別のオブジェクトに持たせることも可能です。