6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2020-10-06

はじめに

Doma 2.43.0のCriteria APIで結合を使う方法を説明します。

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

DomaやCriteria APIの概要についてはDoma入門の他の記事もお読みください。

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

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

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

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

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

  String name;

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

  // getter, setter
}
Employee.java
@Entity(metamodel = @Metamodel)
public class Employee {

  @Id
  Integer id;

  String name;

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

  @Transient Department department;

  // getter, setter
}

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

EmployeeRepository.java
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問題は発生しません。

上記の例ではleftJoinassociateメソッドをそれぞれ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メソッドのラムダ式で得られるエンティティを全く別のオブジェクトに持たせることも可能です。

6
4
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?