Doma入門 - Criteria APIで動的にWHERE句を組み立てる

## はじめに

データベースアクセスライブラリの評価項目はいくつかありますが、その中でも動的なSQLをどれだけ組み立てやすいかというのは優先度が高い項目なのではと思います。静的なSQLは簡単に書けるのに、動的なSQLとなると途端に大変になってしまうというのはありがちですね。特に業務系のアプリケーションの場合、画面に複数の検索項目があり、指定されていたら検索条件に含め指定されていなければ含めないというような要件がありますが、ライブラリが提供する機能によってはアプリケーションのコードが条件分岐だらけになり可読性が低下してしまいます。

この記事では[Doma](https://github.com/domaframework/doma) 3.6.0のCriteria APIを使って動的にSQLのWHERE句を組み立てる機能を紹介し、Domaでは上述した問題をどのように解決できるかを示します。

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

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

データベースには従業員を表すemployeeテーブルが1つだけあります。

```sql:schema.sql
create table employee (
    id integer not null primary key,
    name varchar(255) not null,
    age integer not null, 
    version integer not null);
```

employeeテーブルに対応するエンティティクラスとして`Employee`クラスを用意します。

```java:Employee.java
@Entity(metamodel = @Metamodel)
public class Employee {
  @Id
  public Integer id;
  public String name;
  public Integer age;
  @Version public Integer version;
}
```

従業員を年齢で検索するRepositoryとして`EmployeeRepository`を用意します。
検索する際、年齢の上限と下限をオプションで指定できる（指定されていれば検索条件に含めるし、指定されなければ含めない）ものとし、この機能は`selectByAgeRange`メソッドで実装します。

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

  private final QueryDsl queryDsl;

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

  public List<Employee> selectByAgeRange(Integer min, Integer max) {
    // ここをCriteria APIでどのように実装するかがこの記事の主眼です
  }
}
```

## nullと比較する検索条件の自動除去機能

DomaのCriteria APIは、nullと比較するような検索条件を指定するとその条件を自動でWHERE句から除外する機能を持ちます。したがって、この機能を利用すると`selectByAgeRange`の実装では条件分岐を記述する必要がありません。次のように実装できます。

```java:EmployeeRepository.javaの一部
  public List<Employee> selectByAgeRange(Integer min, Integer max) {
    Employee_ e = new Employee_();
    return queryDsl
        .from(e)
        .where(
            c -> {
              c.ge(e.age, min);
              c.le(e.age, max);
            })
        .fetch();
  }
```

以下では、このメソッドをいくつかのパターンで呼び出した時に生成されるSQLを見てみます。

### minにもmaxにも非nullな値を渡す場合

以下、`repository`は`EmployeeRepository`クラスのインスタンスを表します。

```java
List<Employee> list = repository.selectByAgeRange(30, 40);
```

この時生成されるSQLは次のようなものになります。年齢の上限と下限を指定した検索条件がちゃんとWHERE句に現れていますね。

```sql
select t0_.id, t0_.name, t0_.age, t0_.version from Employee t0_ where t0_.age >= ? and t0_.age <= ?
```

### minに非null、maxにnullを渡す場合

```java
List<Employee> list = repository.selectByAgeRange(30, null);
```

上限を指定する検索条件がWHERE句に現れません。

```sql
select t0_.id, t0_.name, t0_.age, t0_.version from Employee t0_ where t0_.age >= ?
```

### minにnull、maxに非nullを渡す場合

```java
List<Employee> list = repository.selectByAgeRange(null, 40);
```

今度は下限を指定する検索条件がWHERE句に現れません。

```sql
select t0_.id, t0_.name, t0_.age, t0_.version from Employee t0_ where t0_.age <= ?
```

### minにもmaxにもnullを渡す場合

```java
List<Employee> list = repository.selectByAgeRange(null, null);
```

どういう結果になるか予想できると思います。そう、上限と下限を指定する検索条件がどちらも現れなくなります。

```sql
select t0_.id, t0_.name, t0_.age, t0_.version from Employee t0_
```

## ブロック内での明示的な条件分岐

では、nullではない値を見て検索条件に含めるかどうか判断したい場合はどのように書くべきでしょうか？例えば、年齢の下限が0以下の時は条件に含めない（0より大きい場合にのみ条件に含める）という要件があったとします。その場合は次のように明示的な条件分岐を書けます。

```java:EmployeeRepository.javaの一部
  public List<Employee> selectByAgeRange(Integer min, Integer max) {
    Employee_ e = new Employee_();
    return queryDsl
        .from(e)
        .where(
            c -> {
              if (min != null && min > 0) {
                c.ge(e.age, min);
              }
              c.le(e.age, max);
            })
        .fetch();
  }
```

単にwhereメソッドに渡すラムダ式のブロックの中で条件分岐をしています。条件が評価されない限りは検索条件に含まれません。

```java
List<Employee> list = repository.selectByAgeRange(-1, 40);
```

上記のように呼び出すと、次のようなSQLが生成され、下限を指定する検索条件がWHERE句に登場しないことがわかります。

```sql
select t0_.id, t0_.name, t0_.age, t0_.version from Employee t0_ where t0_.age <= ?
```

## おわりに

この記事ではDomaのCriteria APIを使って簡潔にSQLの動的なWHERE句を組み立てられることを紹介しました。

ここに示したの全く同じではないのですが、似たようなコードが下記のプロジェクトにあり動かして試すことができます。

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