Doma入門 - Criteria APIのWHERE句でANDやORなどの論理演算子を使う

## はじめに

SQLのWHERE句を組み立てる際、複数の条件を指定するにはANDやORといった論理演算子で連結することになります。この記事では、そのような論理演算子の連結を[Doma](https://github.com/domaframework/doma) 3.6.0のCriteria APIではどのように表現できるのかを示します。

論理演算子の連結はSQLで表現するにはシンプルな機能ですが、プログラミング言語としてどのように実現するのかは検討しがいのある問題です。Domaではラムダ式を使って表現できるのが特徴的です。


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

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

データベースには従業員を表す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;
}
```

`EmployeeRepository`を用意し、このクラスの`select`メソッドを書き換えていくつかの例を示していきます。

```java
public class EmployeeRepository {

  private final queryDsl queryDsl;

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

  public List<Employee> select() {
    // ここを様々に書き換えてコード例を示します
  }
}
```

## 暗黙的なAND論理演算子

検索条件（この例だと`eq(..., ...)`）を並べると暗黙的にANDで連結されます。

```java
    Employee_ e = new Employee_();
    return queryDsl.from(e).where(c -> {
      c.eq(e.id, 1);
      c.eq(e.name, "aaa");
      c.eq(e.age, 20);
    }).fetch();
```

生成されるSQLは次のようなものになります。
（以降ではバインドされた値を埋め込んだ状態のSQLを示しますが、実際にはバインド変数の`?`を使ったSQLを発行しています。）

```sql
select t0_.id, t0_.name, t0_.age, t0_.version from Employee t0_ where t0_.id = 1 and t0_.name = 'aaa' and t0_.age = 20
```

## 明示的なAND論理演算子

先ほどの例ではANDに相当するメソッドを呼び出しませんでしたが、次のようにANDに相当するメソッド`and`を明示的に呼び出すことも可能です。`and`メソッドはラムダ式を受け取ります。

```java
    Employee_ e = new Employee_();
    return queryDsl.from(e).where(c -> {
      c.eq(e.id, 1);
      c.and(() -> c.eq(e.name, "aaa"));
      c.and(() -> c.eq(e.age, 20));
    }).fetch();
```

生成されるSQLは次のようになり先ほどの例と同じSQLが生成されます（ただし、カッコの有無といった差異はあります）。

```sql
select t0_.id, t0_.name, t0_.age, t0_.version from Employee t0_ where t0_.id = 1 and (t0_.name = 'aaa') and (t0_.age = 20)
```

## OR論理演算子

ORで連結するには、次のように`or`メソッドを呼び出します。`or`メソッドはラムダ式を受け取ります。

```java
    Employee_ e = new Employee_();
    return queryDsl.from(e).where(c -> {
      c.eq(e.id, 1);
      c.or(() -> {
        c.eq(e.name, "aaa");
        c.eq(e.age, 20);
      });
    }).fetch();
```

生成されるSQLは次のようになります。

```sql
select t0_.id, t0_.name, t0_.age, t0_.version from Employee t0_ where t0_.id = 1 or (t0_.name = 'aaa' and t0_.age = 20)
```

## 先頭のANDやORの自動除去機能

直前の例と似ていますが、次のコードはどのようなSQLを生成するでしょうか？

```java
    Employee_ e = new Employee_();
    return queryDsl.from(e).where(c -> {
      c.or(() -> {
        c.eq(e.name, "aaa");
        c.eq(e.age, 20);
      });
      c.eq(e.id, 1);
    }).fetch();
 ```

WHERE句の直後に余分なORが生成されてしまうと思うかもしれませんが、DomaはWHERE句の先頭のANDやORを自動除去するので次のようなSQLが生成されます。

```sql
select t0_.id, t0_.name, t0_.age, t0_.version from Employee t0_ where (t0_.name = 'aaa' and t0_.age = 20) and t0_.id = 1
```

この機能は、WHERE句に条件分岐が入るなどでANDやORの要不要が静的に決まらない場合に便利です。

```java
    Employee_ e = new Employee_();
    return queryDsl.from(e).where(c -> {
      if (何かの条件) {
        c.or(() -> {
          c.eq(e.name, "aaa");
          c.eq(e.age, 20);
        });
      }
      if (何かの条件2) {
        c.or(() -> {
          c.eq(e.name, "bbb");
          c.eq(e.age, 30);
        });
      }
    }).fetch();
 ```

## ANDやORのネスト

`and`や`or`は制限なくいくつもネストできます。

```java
    Employee_ e = new Employee_();
    return queryDsl.from(e).where(c -> {
      c.eq(e.id, 1);
      c.or(() -> {
        c.eq(e.name, "aaa");
        c.eq(e.age, 20);
        c.or(() -> {
          c.eq(e.name, "bbb");
          c.eq(e.age, 30);
        });
      });
    }).fetch();
```

上記のコードから次のようなSQLが生成されます。

```sql
select t0_.id, t0_.name, t0_.age, t0_.version from Employee t0_ where t0_.id = 1 or (t0_.name = 'aaa' and t0_.age = 20 or (t0_.name = 'bbb' and t0_.age = 30))
```

## おわりに

論理演算子の連結をDomaのCriteria APIではどのように表現できるのかを示しました。

冒頭でも述べたようにDomaのCriteira APIでは、論理演算子の連結をラムダ式で表現しています。他の表現方法としてはメソッドチェーンがありますが、個人的にはメソッドーチェーンには弱点があると思っています。それは条件分岐に弱いということです。
条件分岐があるとチェーンが切れてしまい一挙に煩雑さが増大します。チェーンをつなぐためにチェーンする対象のオブジェクトを変数に保存する必要があるからです。
一方、DomaのCriteria APIのようなラムダ式を使う方法では、条件分岐が入る場合と入らない場合で書き方に差が出ないので（ただifのブロックで囲まれかどうかなので）シンプルで可読性が高いと思います。