Unified Criteria API

イントロダクション

Unified Criteria APIは、Classic Criteria API の Entityql と NativeSql DSLを統合することで、明確で直感的なインターフェイスを提供します。

次のエンティティクラスは以下の例で使用されます。

@Entity(metamodel = @Metamodel)
public class Employee {

  @Id private Integer employeeId;
  private Integer employeeNo;
  private String employeeName;
  private Integer managerId;
  private LocalDate hiredate;
  private Salary salary;
  private Integer departmentId;
  private Integer addressId;
  @Version private Integer version;
  @OriginalStates private Employee states;
  @Association private Department department;
  @Association private Employee manager;
  @Association private Address address;

  // getters and setters
}
@Entity(metamodel = @Metamodel)
public class Department {

  @Id private Integer departmentId;
  private Integer departmentNo;
  private String departmentName;
  private String location;
  @Version private Integer version;
  @OriginalStates private Department originalStates;
  @Association private List<Employee> employeeList = new ArrayList<>();

  // getters and setters
}
@Entity(immutable = true, metamodel = @Metamodel)
@Table(name = "EMPLOYEE")
public class Emp {

  @Id private final Integer employeeId;
  private final Integer employeeNo;
  private final String employeeName;
  private final Integer managerId;
  private final LocalDate hiredate;
  private final Salary salary;
  private final Integer departmentId;
  private final Integer addressId;
  @Version private final Integer version;
  @Association private final Dept department;
  @Association private final Emp manager;

  // constructor and getters
}
@Entity(immutable = true, metamodel = @Metamodel)
@Table(name = "DEPARTMENT")
public class Dept {

  @Id private final Integer departmentId;
  private final Integer departmentNo;
  private final String departmentName;
  private final String location;
  @Version private final Integer version;

  // constructor and getters
}

これらのクラスに対する @Entity(metamodel = @Metamodel) アノテーションは、型安全なクエリ作成を可能にする対応するメタモデルクラスの生成をDomaのアノテーションプロセッサに指示します。

例として、生成されたメタモデルクラスはEmployee_Department_Emp_、そしてDept_です。

Metamodel アノテーションの要素を使用して、メタモデル名をカスタマイズできます。

すべてのメタモデルを一括でカスタマイズするには、アノテーションプロセッサのオプションを使用できます。アノテーション処理 を参照して、次のオプションを確認してください。

  • doma.metamodel.enabled

  • doma.metamodel.prefix

  • doma.metamodel.suffix

Query DSL

Unified Criteria API の実体はQuery DSLです。

Query DSL は、エンティティをクエリして関連付けることができます。エントリポイントは org.seasar.doma.jdbc.criteria.QueryDsl クラスです。このクラスには次のメソッドがあります。

  • from

  • insert

  • delete

  • update

QueryDsl クラスを以下のようにインスタンス化します。

QueryDsl queryDsl = new QueryDsl(config);

例えば、EmployeeDepartment エンティティを照会し、それらを関連付けるには、次のようにします。

Employee_ e = new Employee_();
Department_ d = new Department_();

List<Employee> list =
    queryDsl
        .from(e)
        .innerJoin(d, on -> on.eq(e.departmentId, d.departmentId))
        .where(c -> c.eq(d.departmentName, "SALES"))
        .associate(
            e,
            d,
            (employee, department) -> {
              employee.setDepartment(department);
              department.getEmployeeList().add(employee);
            })
        .fetch();

上記のクエリは、次の SQL ステートメントを生成します。

select t0_.EMPLOYEE_ID, t0_.EMPLOYEE_NO, t0_.EMPLOYEE_NAME, t0_.MANAGER_ID,
t0_.HIREDATE, t0_.SALARY, t0_.DEPARTMENT_ID, t0_.ADDRESS_ID, t0_.VERSION,
t1_.DEPARTMENT_ID, t1_.DEPARTMENT_NO, t1_.DEPARTMENT_NAME, t1_.LOCATION, t1_.VERSION
from EMPLOYEE t0_ inner join DEPARTMENT t1_ on (t0_.DEPARTMENT_ID = t1_.DEPARTMENT_ID)
where t1_.DEPARTMENT_NAME = ?

注釈

Kotlin では、QueryDsl の代わりに org.seasar.doma.kotlin.jdbc.criteria.KQueryDsl を使用します。KQueryDsl は、doma-kotlin モジュールに含まれています。Kotlin-specific Criteria API を参照してください。

Select ステートメント

Select 設定

次の設定をサポートしています。

  • allowEmptyWhere

  • comment

  • fetchSize

  • maxRows

  • queryTimeout

  • sqlLogType

すべてのこれらの設定はオプションです。以下のように適用できます。

Employee_ e = new Employee_();

List<Employee> list = queryDsl.from(e, settings -> {
  settings.setAllowEmptyWhere(false);
  settings.setComment("all employees");
  settings.setFetchSize(100);
  settings.setMaxRows(100);
  settings.setSqlLogType(SqlLogType.RAW);
  settings.setQueryTimeout(1000);
}).fetch();

フェッチ処理

Query DSLは以下のデータフェッチのメソッドを提供します。

  • fetch

  • fetchOne

  • fetchOptional

  • stream

Employee_ e = new Employee_();

// The fetch method returns results as a list.
List<Employee> list = queryDsl.from(e).fetch();

// The fetchOne method returns a single result, possibly null.
Employee employee = queryDsl.from(e).where(c -> c.eq(e.employeeId, 1)).fetchOne();

// The fetchOptional method returns a single result as an Optional object.
Optional<Employee> optional = queryDsl.from(e).where(c -> c.eq(e.employeeId, 1)).fetchOptional();

// The stream method returns results as a stream.
Stream<Employee> stream = queryDsl.from(e).stream();

ストリーム処理

Query DSLでは、以下のストリーム処理のメソッドをサポートしています。

  • mapStream

  • collect

  • openStream

void doSomething() {
    Employee_ e = new Employee_();
    
    // mapStream allows processing of a stream.
    Map<Integer, List<Employee>> map = queryDsl
        .from(e)
        .mapStream(stream -> stream.collect(groupingBy(Employee::getDepartmentId)));
    
    // collect is a shorthand for mapStream.
    Map<Integer, List<Employee>> map2 = queryDsl.from(e).collect(groupingBy(Employee::getDepartmentId));
    
    // openStream returns a stream. You MUST close the stream explicitly.
    try (Stream<Employee> stream = queryDsl.from(e).openStream()) {
        stream.forEach(employee -> {
            // do something
        });
    }
}

これらのメソッドは、大きな結果セットに対して効率的な処理を提供します。

Select 式

エンティティの検索

デフォルトでは、結果エンティティの型は from メソッドで指定された型と同じです。

Employee_ e = new Employee_();
Department_ d = new Department_();

List<Employee> list = queryDsl
    .from(e)
    .innerJoin(d, on -> on.eq(e.departmentId, d.departmentId))
    .fetch();

上記のクエリは、次の SQL ステートメントを生成します。

select t0_.EMPLOYEE_ID, t0_.EMPLOYEE_NO, t0_.EMPLOYEE_NAME, t0_.MANAGER_ID,
t0_.HIREDATE, t0_.SALARY, t0_.DEPARTMENT_ID, t0_.ADDRESS_ID, t0_.VERSION
from EMPLOYEE t0_
inner join DEPARTMENT t1_ on (t0_.DEPARTMENT_ID = t1_.DEPARTMENT_ID)

結果のエンティティタイプとして結合されたエンティティタイプを選択するには、project または select を使用します。

Employee_ e = new Employee_();
Department_ d = new Department_();

List<Department> list = queryDsl
    .from(e)
    .innerJoin(d, on -> on.eq(e.departmentId, d.departmentId))
    .project(d)
    .fetch();

このクエリは以下のSQLを生成します。

select t1_.DEPARTMENT_ID, t1_.DEPARTMENT_NO, t1_.DEPARTMENT_NAME, t1_.LOCATION, t1_.VERSION
from EMPLOYEE t0_
inner join DEPARTMENT t1_ on (t0_.DEPARTMENT_ID = t1_.DEPARTMENT_ID)

注釈

project メソッドは重複したエンティティを削除しますが、select は削除しません。どちらのメソッドも呼び出さない場合、重複はデフォルトで削除されます。

複数のエンティティの検索

複数のエンティティタイプを指定し、タプルとしてフェッチします。

Employee_ e = new Employee_();
Department_ d = new Department_();

List<Tuple2<Department, Employee>> list = queryDsl
    .from(d)
    .leftJoin(e, on -> on.eq(d.departmentId, e.departmentId))
    .where(c -> c.eq(d.departmentId, 4))
    .select(d, e)
    .fetch();

このクエリは次のように生成されます。

select t0_.DEPARTMENT_ID, t0_.DEPARTMENT_NO, t0_.DEPARTMENT_NAME, t0_.LOCATION,
t0_.VERSION, t1_.EMPLOYEE_ID, t1_.EMPLOYEE_NO, t1_.EMPLOYEE_NAME, t1_.MANAGER_ID,
t1_.HIREDATE, t1_.SALARY, t1_.DEPARTMENT_ID, t1_.ADDRESS_ID, t1_.VERSION
from DEPARTMENT t0_ left outer join EMPLOYEE t1_ on (t0_.DEPARTMENT_ID = t1_.DEPARTMENT_ID)
where t0_.DEPARTMENT_ID = ?

タプル内では、すべてのプロパティが null の場合、エンティティは null になります。

注釈

select メソッドは重複を削除しません。

カラムの射影

カラムを選択するには、select を使用します。1つのカラムの選択は次のようにします。

Employee_ e = new Employee_();

List<String> list = queryDsl.from(e).select(e.employeeName).fetch();

このクエリは次のように生成されます。

select t0_.EMPLOYEE_NAME from EMPLOYEE t0_

複数のカラムを選択する場合は次のようにします。

Employee_ e = new Employee_();

List<Tuple2<String, Integer>> list = queryDsl
    .from(e)
    .select(e.employeeName, e.employeeNo)
    .fetch();

このクエリは次のように生成されます。

select t0_.EMPLOYEE_NAME, t0_.EMPLOYEE_NO from EMPLOYEE t0_

9 列までは Tuple2 から Tuple9 で保持されます。それ以外は Row で保持されます。

Row リストには selectAsRow を使用します。

Employee_ e = new Employee_();

List<Row> list = queryDsl.from(e).selectAsRow(e.employeeName, e.employeeNo).fetch();

カラムの射影とマッピング

カラムを選択しエンティティにマップするには、projectTo または selectTo メソッドを使用します。

Employee_ e = new Employee_();

List<Employee> list = queryDsl.from(e).selectTo(e, e.employeeName).fetch();

このクエリは次のように生成されます。

select t0_.EMPLOYEE_ID, t0_.EMPLOYEE_NAME from EMPLOYEE t0_

SQL の select 句にはプライマリキー "EMPLOYEE_ID" が含まれていることに注意してください。 projectToselectTo メソッドは、明示的に指定されていない場合でも、エンティティの ID プロパティを常に含めます。

注釈

projectTo メソッドは結果から重複したエンティティIDを削除しますが、selectTo メソッドは削除しません。

Where 式

以下の演算子と述語がサポートされています。

  • eq - (=)

  • ne - (<>)

  • ge - (>=)

  • gt - (>)

  • le - (<=)

  • lt - (<)

  • isNull - (is null)

  • isNotNull - (is not null)

  • like

  • notLike - (not like)

  • between

  • in

  • notIn - (not in)

  • exists

  • notExists - (not exists)

注釈

右側のオペランドが null の場合、WHEREまたはHAVING句は演算子を除外します。詳細は [WhereDeclaration] と [HavingDeclaration] の javadoc を参照してください。

次のユーティリティ演算子もサポートしています。

  • eqOrIsNull - ("=" or "is null")

  • neOrIsNotNull - ("<>" or "is not null")

さらに、以下の論理演算子がサポートされています。

  • and

  • or

  • not

Employee_ e = new Employee_();

List<Employee> list = queryDsl
    .from(e)
    .where(c -> {
        c.eq(e.departmentId, 2);
        c.isNotNull(e.managerId);
        c.or(() -> {
            c.gt(e.salary, new Salary("1000"));
            c.lt(e.salary, new Salary("2000"));
        });
    })
    .fetch();

このクエリは次のように生成されます。

select t0_.EMPLOYEE_ID, t0_.EMPLOYEE_NO, t0_.EMPLOYEE_NAME, t0_.MANAGER_ID, t0_.HIREDATE,
t0_.SALARY, t0_.DEPARTMENT_ID, t0_.ADDRESS_ID, t0_.VERSION
from EMPLOYEE t0_
where t0_.DEPARTMENT_ID = ? and t0_.MANAGER_ID is not null or (t0_.SALARY > ? and t0_.SALARY < ?)

サブクエリは以下のように記述できます。

Employee_ e = new Employee_();
Employee_ e2 = new Employee_();

List<Employee> list = queryDsl
    .from(e)
    .where(c -> c.in(e.employeeId, c.from(e2).select(e2.managerId)))
    .orderBy(c -> c.asc(e.employeeId))
    .fetch();

上記のクエリは以下のものを生成します。

select t0_.EMPLOYEE_ID, t0_.EMPLOYEE_NO, t0_.EMPLOYEE_NAME, t0_.MANAGER_ID, t0_.HIREDATE,
t0_.SALARY, t0_.DEPARTMENT_ID, t0_.ADDRESS_ID, t0_.VERSION
from EMPLOYEE t0_
where t0_.EMPLOYEE_ID in (select t1_.MANAGER_ID from EMPLOYEE t1_)
order by t0_.EMPLOYEE_ID asc

動的な Where 式

WHERE式はWHERE句を構築するために評価された演算子のみを使用します。 式で演算子が評価されない場合は、WHERE句は省略されます。

例えば、条件付き式を指定します。

Employee_ e = new Employee_();

List<Employee> list = queryDsl
    .from(e)
    .where(c -> {
        c.eq(e.departmentId, 1);
        if (enableNameCondition) {
            c.like(e.employeeName, name);
        }
    })
    .fetch();

enableNameConditionfalse の場合、like 式は無視されます。

select t0_.EMPLOYEE_ID, t0_.EMPLOYEE_NO, t0_.EMPLOYEE_NAME, t0_.MANAGER_ID, t0_.HIREDATE,
t0_.SALARY, t0_.DEPARTMENT_ID, t0_.ADDRESS_ID, t0_.VERSION
from EMPLOYEE t0_ where t0_.DEPARTMENT_ID = ?

Join 式

以下のjoin式をサポートします。

  • innerJoin - (inner join)

  • leftJoin - (left outer join)

innerJoinの例:

Employee_ e = new Employee_();
Department_ d = new Department_();

List<Employee> list = queryDsl
    .from(e)
    .innerJoin(d, on -> on.eq(e.departmentId, d.departmentId))
    .fetch();

このクエリは次のように生成されます。

select t0_.EMPLOYEE_ID, t0_.EMPLOYEE_NO, t0_.EMPLOYEE_NAME, t0_.MANAGER_ID, t0_.HIREDATE,
t0_.SALARY, t0_.DEPARTMENT_ID, t0_.ADDRESS_ID, t0_.VERSION
from EMPLOYEE t0_
inner join DEPARTMENT t1_ on (t0_.DEPARTMENT_ID = t1_.DEPARTMENT_ID)

leftJoinの例:

Employee_ e = new Employee_();
Department_ d = new Department_();

List<Employee> list = queryDsl
    .from(e)
    .leftJoin(d, on -> on.eq(e.departmentId, d.departmentId))
    .fetch();

このクエリは次のように生成されます。

select t0_.EMPLOYEE_ID, t0_.EMPLOYEE_NO, t0_.EMPLOYEE_NAME, t0_.MANAGER_ID, t0_.HIREDATE,
t0_.SALARY, t0_.DEPARTMENT_ID, t0_.ADDRESS_ID, t0_.VERSION
from EMPLOYEE t0_
left outer join DEPARTMENT t1_ on (t0_.DEPARTMENT_ID = t1_.DEPARTMENT_ID)

関連付け

joinと一緒に associate を使用し、エンティティを関連付けることができます

Employee_ e = new Employee_();
Department_ d = new Department_();

List<Employee> list = queryDsl
    .from(e)
    .innerJoin(d, on -> on.eq(e.departmentId, d.departmentId))
    .where(c -> c.eq(d.departmentName, "SALES"))
    .associate(
        e,
        d,
        (employee, department) -> {
          employee.setDepartment(department);
          department.getEmployeeList().add(employee);
        })
    .fetch();

このクエリは次のように生成されます。

select t0_.EMPLOYEE_ID, t0_.EMPLOYEE_NO, t0_.EMPLOYEE_NAME, t0_.MANAGER_ID,
t0_.HIREDATE, t0_.SALARY, t0_.DEPARTMENT_ID, t0_.ADDRESS_ID, t0_.VERSION,
t1_.DEPARTMENT_ID, t1_.DEPARTMENT_NO, t1_.DEPARTMENT_NAME, t1_.LOCATION, t1_.VERSION
from EMPLOYEE t0_ inner join DEPARTMENT t1_ on (t0_.DEPARTMENT_ID = t1_.DEPARTMENT_ID)
where t1_.DEPARTMENT_NAME = ?

複数のエンティティを関連づける場合は次のようにします。

Employee_ e = new Employee_();
Department_ d = new Department_();
Address_ a = new Address_();

List<Employee> list = queryDsl
    .from(e)
    .innerJoin(d, on -> on.eq(e.departmentId, d.departmentId))
    .innerJoin(a, on -> on.eq(e.addressId, a.addressId))
    .where(c -> c.eq(d.departmentName, "SALES"))
    .associate(
        e,
        d,
        (employee, department) -> {
          employee.setDepartment(department);
          department.getEmployeeList().add(employee);
        })
    .associate(e, a, Employee::setAddress)
    .fetch();

不変エンティティの関連付け

イミュータブルなエンティティを関連付けるには、joinと一緒に associateWith を使用します。

Emp_ e = new Emp_();
Emp_ m = new Emp_();
Dept_ d = new Dept_();

List<Emp> list = queryDsl
    .from(e)
    .innerJoin(d, on -> on.eq(e.departmentId, d.departmentId))
    .leftJoin(m, on -> on.eq(e.managerId, m.employeeId))
    .where(c -> c.eq(d.departmentName, "SALES"))
    .associateWith(e, d, Emp::withDept)
    .associateWith(e, m, Emp::withManager)
    .fetch();

このクエリは次のように生成されます。

select t0_.EMPLOYEE_ID, t0_.EMPLOYEE_NO, t0_.EMPLOYEE_NAME, t0_.MANAGER_ID, t0_.HIREDATE,
t0_.SALARY, t0_.DEPARTMENT_ID, t0_.ADDRESS_ID, t0_.VERSION,
t1_.DEPARTMENT_ID, t1_.DEPARTMENT_NO, t1_.DEPARTMENT_NAME, t1_.LOCATION, t1_.VERSION,
t2_.EMPLOYEE_ID, t2_.EMPLOYEE_NO, t2_.EMPLOYEE_NAME, t2_.MANAGER_ID, t2_.HIREDATE,
t2_.SALARY, t2_.DEPARTMENT_ID, t2_.ADDRESS_ID, t2_.VERSION
from EMPLOYEE t0_
inner join DEPARTMENT t1_ on (t0_.DEPARTMENT_ID = t1_.DEPARTMENT_ID)
left outer join EMPLOYEE t2_ on (t0_.MANAGER_ID = t2_.EMPLOYEE_ID)
where t1_.DEPARTMENT_NAME = ?

動的な Join 式

join式は、評価された演算子のみを使用してJOIN句を構築します。演算子が評価されない場合は、JOIN句は省略されます。

例えば、条件付きjoinを使用する場合は次のようにします。

Employee_ e = new Employee_();
Employee_ e2 = new Employee_();

List<Employee> list = queryDsl
    .from(e)
    .innerJoin(e2, on -> {
        if (join) {
            on.eq(e.managerId, e2.employeeId);
        }
    })
    .fetch();

joinfalse の場合、on 式は無視されます。

select t0_.EMPLOYEE_ID, t0_.EMPLOYEE_NO, t0_.EMPLOYEE_NAME, t0_.MANAGER_ID, t0_.HIREDATE,
t0_.SALARY, t0_.DEPARTMENT_ID, t0_.ADDRESS_ID, t0_.VERSION
from EMPLOYEE t0_

動的な関連付け

動的なjoin式では、関連付けを任意にすることができます。AssociationOption.optional()associate メソッドで使用します。

Employee_ e = new Employee_();
Department_ d = new Department_();

List<Employee> list = queryDsl
    .from(e)
    .innerJoin(d, on -> {
        if (join) {
            on.eq(e.departmentId, d.departmentId);
        }
    })
    .associate(
        e,
        d,
        (employee, department) -> {
          employee.setDepartment(department);
          department.getEmployeeList().add(employee);
        },
        AssociationOption.optional())
    .fetch();

集約関数

次の集約関数がサポートされています。

  • avg(property)

  • avgAsDouble(property)

  • count()

  • count(property)

  • countDistinct(property)

  • max(property)

  • min(property)

  • sum(property)

これらは org.seasar.doma.jdbc.criteria.expression.Expressions クラスで定義されており、静的インポートで使用できます。

たとえば、 sum 関数を select メソッドに渡すことができます。

Employee_ e = new Employee_();

Salary salary = queryDsl.from(e).select(sum(e.salary)).fetchOne();

このクエリは次のように生成されます。

select sum(t0_.SALARY) from EMPLOYEE t0_

Group by 式

Group by式を使うと、指定された列に基づいて結果をグループ化できます。

Employee_ e = new Employee_();

List<Tuple2<Integer, Long>> list = queryDsl
    .from(e)
    .groupBy(e.departmentId)
    .select(e.departmentId, count())
    .fetch();

上記のコードは以下のものを生成します。

select t0_.DEPARTMENT_ID, count(*) from EMPLOYEE t0_ group by t0_.DEPARTMENT_ID

group by 式を指定しない場合、group by 式は select 式から自動的に推測されます。したがって、次のコードは上記と同じ SQL ステートメントを発行します。

Employee_ e = new Employee_();

List<Tuple2<Integer, Long>> list = queryDsl.from(e).select(e.departmentId, count()).fetch();

Having 式

以下の演算子は、having 式でサポートされています。

  • eq - (=)

  • ne - (<>)

  • ge - (>=)

  • gt - (>)

  • le - (<=)

  • lt - (<)

論理演算子もサポートされています。

  • and

  • or

  • not

Employee_ e = new Employee_();
Department_ d = new Department_();

List<Tuple2<Long, String>> list = queryDsl
    .from(e)
    .innerJoin(d, on -> on.eq(e.departmentId, d.departmentId))
    .having(c -> c.gt(count(), 3L))
    .orderBy(c -> c.asc(count()))
    .select(count(), d.departmentName)
    .fetch();

上記のクエリは以下のものを生成します。

select count(*), t1_.DEPARTMENT_NAME
from EMPLOYEE t0_
inner join DEPARTMENT t1_ on (t0_.DEPARTMENT_ID = t1_.DEPARTMENT_ID)
group by t1_.DEPARTMENT_NAME having count(*) > ?
order by count(*) asc

動的な Having 式

having 式は評価された演算子のみを含み、どの演算子も評価されない場合は HAVING 句を省略します。

例えば、HAVING句の条件式は以下の通りです。

Employee_ e = new Employee_();
Department_ d = new Department_();

List<Tuple2<Long, String>> list = queryDsl
    .from(e)
    .innerJoin(d, on -> on.eq(e.departmentId, d.departmentId))
    .groupBy(d.departmentName)
    .having(c -> {
        if (countCondition) {
            c.gt(count(), 3L);
        }
    })
    .select(count(), d.departmentName)
    .fetch();

countConditionfalse の場合、SQL文では having 句は無視されます。

Order by 式

サポートされている順序操作は次のとおりです。

  • asc

  • desc

Employee_ e = new Employee_();

List<Employee> list = queryDsl
    .from(e)
    .orderBy(c -> {
        c.asc(e.departmentId);
        c.desc(e.salary);
    })
    .fetch();

上記のクエリは以下のものを生成します。

select t0_.EMPLOYEE_ID, t0_.EMPLOYEE_NO, t0_.EMPLOYEE_NAME, t0_.MANAGER_ID, t0_.HIREDATE,
t0_.SALARY, t0_.DEPARTMENT_ID, t0_.ADDRESS_ID, t0_.VERSION
from EMPLOYEE t0_
order by t0_.DEPARTMENT_ID asc, t0_.SALARY desc

動的な Order by 式

order by式は、評価された演算子のみを使用してORDER BY句を構築します。演算子が評価されない場合は、ORDER BY句は省略されます。

Distinct 式

重複のない行を検索するには、 distinct() を使用します。

List<Department> list = queryDsl
    .from(d)
    .distinct()
    .leftJoin(e, on -> on.eq(d.departmentId, e.departmentId))
    .fetch();

このクエリは次のように生成されます。

select distinct t0_.DEPARTMENT_ID, t0_.DEPARTMENT_NO, t0_.DEPARTMENT_NAME,
t0_.LOCATION, t0_.VERSION
from DEPARTMENT t0_
left outer join EMPLOYEE t1_ on (t0_.DEPARTMENT_ID = t1_.DEPARTMENT_ID)

Limit および Offset 式

行数を制限し、オフセットを指定するには、次のようにします。

Employee_ e = new Employee_();

List<Employee> list = queryDsl
    .from(e)
    .limit(5)
    .offset(3)
    .orderBy(c -> c.asc(e.employeeNo))
    .fetch();

このクエリは次のように生成されます。

select t0_.EMPLOYEE_ID, t0_.EMPLOYEE_NO, t0_.EMPLOYEE_NAME, t0_.MANAGER_ID, t0_.HIREDATE,
t0_.SALARY, t0_.DEPARTMENT_ID, t0_.ADDRESS_ID, t0_.VERSION
from EMPLOYEE t0_
order by t0_.EMPLOYEE_NO asc
offset 3 rows fetch first 5 rows only

動的な Limit および Offset 式

limit および offset 式は SQL 内で null でない値のみを含みます。null の場合、対応する FETCH FIRST または OFFSET 句は省略されます。

For update 式

forUpdate メソッドはSQLで行をロックできます。

Employee_ e = new Employee_();

List<Employee> list = queryDsl
    .from(e)
    .where(c -> c.eq(e.employeeId, 1))
    .forUpdate()
    .fetch();

上記のクエリは以下のものを生成します。

select t0_.EMPLOYEE_ID, t0_.EMPLOYEE_NO, t0_.EMPLOYEE_NAME, t0_.MANAGER_ID, t0_.HIREDATE,
t0_.SALARY, t0_.DEPARTMENT_ID, t0_.ADDRESS_ID, t0_.VERSION
from EMPLOYEE t0_
where t0_.EMPLOYEE_ID = ?
for update

Union 式

サポートされているUNION操作は次のとおりです。

  • union

  • unionAll - (union all)

Employee_ e = new Employee_();
Department_ d = new Department_();

List<Tuple2<Integer, String>> list = queryDsl
    .from(e)
    .select(e.employeeId, e.employeeName)
    .union(queryDsl.from(d)
        .select(d.departmentId, d.departmentName))
    .fetch();

このクエリは次のように生成されます。

select t0_.EMPLOYEE_ID, t0_.EMPLOYEE_NAME from EMPLOYEE t0_
union
select t0_.DEPARTMENT_ID, t0_.DEPARTMENT_NAME from DEPARTMENT t0_

UNIONクエリでインデックスを指定してorder byを使用するには次のようにします。

List<Tuple2<Integer, String>> list = queryDsl
    .from(e)
    .select(e.employeeId, e.employeeName)
    .union(queryDsl.from(d)
        .select(d.departmentId, d.departmentName))
    .orderBy(c -> c.asc(2))
    .fetch();

派生テーブル式

派生テーブルを使用したサブクエリがサポートされていますが、派生テーブルに対応するエンティティクラスが必要です。

派生テーブルに対応するエンティティ クラスを次のように定義します。

@Entity(metamodel = @Metamodel)
public class NameAndAmount {
  private String name;
  private Integer amount;

  public NameAndAmount() {}

  public NameAndAmount(String accounting, BigDecimal bigDecimal) {
    this.name = accounting;
    this.amount = bigDecimal.intValue();
  }

  public String getName() { return name; }
  public void setName(String name) { this.name = name; }
  public Integer getAmount() { return amount; }
  public void setAmount(Integer amount) { this.amount = amount; }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    NameAndAmount that = (NameAndAmount) o;
    return Objects.equals(name, that.name) && Objects.equals(amount, that.amount);
  }

  @Override
  public int hashCode() { return Objects.hash(name, amount); }
}

派生テーブルを使用したサブクエリは次のように記述できます。

Department_ d = new Department_();
Employee_ e = new Employee_();
NameAndAmount_ t = new NameAndAmount_();

SetOperand<?> subquery = queryDsl
    .from(e)
    .innerJoin(d, c -> c.eq(e.departmentId, d.departmentId))
    .groupBy(d.departmentName)
    .select(d.departmentName, Expressions.sum(e.salary));

List<NameAndAmount> list = queryDsl
    .from(t, subquery)
    .orderBy(c -> c.asc(t.name))
    .fetch();

このクエリは次のように生成されます。

select
    t0_.NAME,
    t0_.AMOUNT
from
    (
        select
            t2_.DEPARTMENT_NAME AS NAME,
            sum(t1_.SALARY) AS AMOUNT
        from
            EMPLOYEE t1_
        inner join
            DEPARTMENT t2_ on (t1_.DEPARTMENT_ID = t2_.DEPARTMENT_ID)
        group by
            t2_.DEPARTMENT_NAME
    ) t0_
order by
    t0_.NAME asc

共通テーブル式

共通テーブル式(CTE)はサポートされています。CTEを使用するには、対応するエンティティクラスを定義する必要があります。

CTEに対応するエンティティクラスを次のように定義します:

@Entity(metamodel = @Metamodel)
public record AverageSalary(Salary salary) {}

CTEを使用したクエリは次のように記述できます。

var a = new AverageSalary_();
var e = new Employee_();

var cteQuery =
    dsl.from(e)
        .select(Expressions.avg(e.salary));

var list =
    dsl.with(a, cteQuery)
        .from(e)
        .innerJoin(a, on -> on.ge(e.salary, a.salary))
        .select(e.employeeId, e.employeeName, e.salary)
        .fetch();

上記のクエリは以下のSQLを生成します:

with AVERAGE_SALARY(SALARY) as (
    select
        avg(t0_.SALARY)
    from
        EMPLOYEE t0_
)
select
    t0_.EMPLOYEE_ID,
    t0_.EMPLOYEE_NAME,
    t0_.SALARY from EMPLOYEE t0_
inner join
    AVERAGE_SALARY t1_ on (t0_.SALARY >= t1_.SALARY)

Delete ステートメント

DELETEステートメントは、Where Expression と同じルールに従います。

Delete 設定

次の設定がサポートされています。

  • allowEmptyWhere

  • batchSize

  • comment

  • ignoreVersion

  • queryTimeout

  • sqlLogType

  • suppressOptimisticLockException

すべてオプションで、以下のように適用できます。

Employee_ e = new Employee_();

int count = queryDsl.delete(e, settings -> {
  settings.setAllowEmptyWhere(true);
  settings.setBatchSize(20);
  settings.setComment("delete all");
  settings.setIgnoreVersion(true);
  settings.setQueryTimeout(1000);
  settings.setSqlLogType(SqlLogType.RAW);
  settings.setSuppressOptimisticLockException(true);
})
.where(c -> {})
.execute();

注釈

空のWHERE句でDELETEステートメントを許可するには、 allowEmptyWhere 設定を有効にしてください。

エンティティによるレコードの削除

Employee_ e = new Employee_();

Employee employee = queryDsl.from(e).where(c -> c.eq(e.employeeId, 5)).fetchOne();

Result<Employee> result = queryDsl.delete(e).single(employee).execute();

このクエリは次のように生成されます。

delete from EMPLOYEE where EMPLOYEE_ID = ? and VERSION = ?

バッチ削除もサポートされています。

List<Employee> employees = queryDsl.from(e).where(c -> c.in(e.employeeId, Arrays.asList(5, 6))).fetch();

BatchResult<Employee> result = queryDsl.delete(e).batch(employees).execute();

execute メソッドによって投げられる例外は次のとおりです。

  • OptimisticLockException: エンティティにバージョンプロパティがあり、更新件数が 0 の場合

エンティティによるレコードの削除と削除されたレコードの取得

returning メソッドを呼び出すことで、エンティティを削除すると同時に削除したエンティティを取得できます。

Department result = queryDsl.delete(d).single(department).returning().fetchOne();

このSQLを生成します。

delete from DEPARTMENT where DEPARTMENT_ID = ? and VERSION = ?
returning DEPARTMENT_ID, DEPARTMENT_NO, DEPARTMENT_NAME, LOCATION, VERSION

returning メソッドで返すプロパティを指定することもできます。

結果を Optional として受け取るには、fetchOne の代わりに fetchOptional メソッドを使用します。

注釈

この機能は、H2 Database、PostgreSQL、SQL Server、およびSQLiteのダイアレクトのみがサポートしています。

Where 式によるレコードの削除

条件で削除するには次のようにします。

int count = queryDsl.delete(e).where(c -> c.ge(e.salary, new Salary("2000"))).execute();

このクエリは次のように生成されます。

delete from EMPLOYEE t0_ where t0_.SALARY >= ?

すべてのレコードを削除するには、all メソッドを使用します。

int count = queryDsl.delete(e).all().execute();

Insert ステートメント

INSERT文の実行中に一意制約違反が発生すると、UniqueConstraintException がスローされます。

Insert 設定

サポートされている追加の設定は次のとおりです:

  • comment

  • queryTimeout

  • sqlLogType

  • batchSize

  • excludeNull

  • include

  • exclude

  • ignoreGeneratedKeys

すべてオプションで、以下のように適用できます。

Department_ d = new Department_();

int count = queryDsl.insert(d, settings -> {
    settings.setComment("insert department");
    settings.setQueryTimeout(1000);
    settings.setSqlLogType(SqlLogType.RAW);
    settings.setBatchSize(20);
    settings.excludeNull(true);
})
.values(c -> {
    c.value(d.departmentId, 99);
    c.value(d.departmentNo, 99);
    c.value(d.departmentName, "aaa");
    c.value(d.location, "bbb");
    c.value(d.version, 1);
})
.execute();

除外するカラムを指定できます。

Department department = createDepartment();

Result<Department> result = queryDsl.insert(d, settings ->
    settings.exclude(d.departmentName, d.location)
).single(department).execute();

エンティティによるレコードの追加

single

単一のエンティティの追加:

void doSomething() {
    Department department = new Department();
    department.setDepartmentId(99);
    department.setDepartmentNo(99);
    department.setDepartmentName("aaa");
    department.setLocation("bbb");
    
    Result<Department> result = queryDsl.insert(d).single(department).execute();
}

このクエリは次のように生成されます。

insert into DEPARTMENT (DEPARTMENT_ID, DEPARTMENT_NO, DEPARTMENT_NAME, LOCATION, VERSION)
values (?, ?, ?, ?, ?)

INSERT ... ON CONFLICT に相当する機能がサポートされています。

重複したキーが見つかった場合、既存のレコードを更新するために onDuplicateKeyUpdate メソッドを使用します。

Result<Department> result = queryDsl
    .insert(d)
    .single(department)
    .onDuplicateKeyUpdate()
    .execute();

重複したキーが見つかった場合、何もしないことを表すには onDuplicateKeyIgnore メソッドを使用します。

Result<Department> result = queryDsl
    .insert(d)
    .single(department)
    .onDuplicateKeyIgnore()
    .execute();

batch

バッチ追加もサポートされています。

void doSomething() {
    Department department = createDepartment();
    Department department2 = createDepartment();
    List<Department> departments = Arrays.asList(department, department2);
    
    BatchResult<Department> result = queryDsl.insert(d).batch(departments).execute();
}

INSERT ... ON CONFLICT に相当する機能がサポートされています。

重複したキーが見つかった場合、既存のレコードを更新するために onDuplicateKeyUpdate メソッドを使用します。

BatchResult<Department> result = queryDsl
    .insert(d)
    .batch(departments)
    .onDuplicateKeyUpdate()
    .execute();

重複したキーが見つかった場合、何もしないことを表すには onDuplicateKeyIgnore メソッドを使用します。

BatchResult<Department> result = queryDsl
    .insert(d)
    .batch(departments)
    .onDuplicateKeyIgnore()
    .execute();

multi

複数行追加もサポートされています。

MultiResult<Department> result = queryDsl.insert(d).multi(departments).execute();

このクエリは次のように生成されます。

insert into DEPARTMENT (DEPARTMENT_ID, DEPARTMENT_NO, DEPARTMENT_NAME, LOCATION, VERSION)
values (?, ?, ?, ?, ?), (?, ?, ?, ?, ?)

INSERT ... ON CONFLICT に相当する機能がサポートされています。

重複したキーが見つかった場合、既存のレコードを更新するために onDuplicateKeyUpdate メソッドを使用します。

MultiResult<Department> result = queryDsl
    .insert(d)
    .multi(departments)
    .onDuplicateKeyUpdate()
    .execute();

重複したキーが見つかった場合、何もしないことを表すには onDuplicateKeyIgnore メソッドを使用します。

MultiResult<Department> result = queryDsl
    .insert(d)
    .multi(departments)
    .onDuplicateKeyIgnore()
    .execute();

エンティティでレコードを追加し、追加されたレコードを取得する

returning メソッドを呼び出すことで、エンティティを追加すると同時に追加したエンティティを取得できます。

Department result = queryDsl.insert(d).single(department).returning().fetchOne();

このSQLを生成します。

insert into DEPARTMENT (DEPARTMENT_ID, DEPARTMENT_NO, DEPARTMENT_NAME, LOCATION, VERSION)
values (?, ?, ?, ?, ?) returning DEPARTMENT_ID, DEPARTMENT_NO, DEPARTMENT_NAME, LOCATION, VERSION

returning メソッドで返すプロパティを指定することもできます。

結果を Optional として受け取るには、fetchOne の代わりに fetchOptional メソッドを使用します。

returning メソッドは複数行追加にも対応しています。その場合、 fetch メソッドは、挿入されたエンティティのリストを返します。

List<Department> results = queryDsl.insert(d).multi(departmentList).returning().fetch();

注釈

この機能は、H2 Database、PostgreSQL、SQL Server、およびSQLiteのダイアレクトのみがサポートしています。

指定された値によるレコードの追加

値を指定してレコードを追加するには次のようにします。

int count = queryDsl.insert(d)
    .values(c -> {
        c.value(d.departmentId, 99);
        c.value(d.departmentNo, 99);
        c.value(d.departmentName, "aaa");
        c.value(d.location, "bbb");
        c.value(d.version, 1);
    })
    .execute();

このクエリは次のように生成されます。

insert into DEPARTMENT (DEPARTMENT_ID, DEPARTMENT_NO, DEPARTMENT_NAME, LOCATION, VERSION)
values (?, ?, ?, ?, ?)

INSERT SELECT 構文もサポートしています。

Department_ da = new Department_("DEPARTMENT_ARCHIVE");
Department_ d = new Department_();

int count = queryDsl.insert(da)
    .select(c -> c.from(d).where(cc -> cc.in(d.departmentId, Arrays.asList(1, 2))))
    .execute();

このクエリは次のように生成されます。

insert into DEPARTMENT_ARCHIVE (DEPARTMENT_ID, DEPARTMENT_NO, DEPARTMENT_NAME,
LOCATION, VERSION) select t0_.DEPARTMENT_ID, t0_.DEPARTMENT_NO, t0_.DEPARTMENT_NAME,
t0_.LOCATION, t0_.VERSION from DEPARTMENT t0_ where t0_.DEPARTMENT_ID in (?, ?)

INSERT ... ON CONFLICT に相当する機能がサポートされています。

重複したキーが見つかった場合、既存のレコードを更新するために onDuplicateKeyUpdate メソッドを使用します。

int count = queryDsl
    .insert(d)
    .values(c -> {
        c.value(d.departmentId, 1);
        c.value(d.departmentNo, 60);
        c.value(d.departmentName, "DEVELOPMENT");
        c.value(d.location, "KYOTO");
        c.value(d.version, 2);
    })
    .onDuplicateKeyUpdate()
    .keys(d.departmentId)
    .set(c -> {
        c.value(d.departmentName, c.excluded(d.departmentName));
        c.value(d.location, "KYOTO");
        c.value(d.version, 3);
    })
    .execute();

重複したキーが見つかった場合、何もしないことを表すには onDuplicateKeyIgnore メソッドを使用します。

int count = queryDsl
    .insert(d)
    .values(c -> {
        c.value(d.departmentId, 1);
        c.value(d.departmentNo, 60);
        c.value(d.departmentName, "DEVELOPMENT");
        c.value(d.location, "KYOTO");
        c.value(d.version, 2);
    })
    .onDuplicateKeyIgnore()
    .keys(d.departmentId)
    .execute();

Update ステートメント

UPDATE文の実行中に一意制約違反が発生すると、UniqueConstraintException がスローされます。

UPDATEステートメントは、Where Expression と同じルールに従います。

Update 設定

次の設定がサポートされています。

  • allowEmptyWhere

  • batchSize

  • comment

  • ignoreVersion

  • queryTimeout

  • sqlLogType

  • suppressOptimisticLockException

  • excludeNull

  • include

  • exclude

すべてオプションで、以下のように適用できます。

Employee_ e = new Employee_();

int count = queryDsl.update(e, settings -> {
  settings.setAllowEmptyWhere(true);
  settings.setBatchSize(20);
  settings.setComment("update all");
  settings.setIgnoreVersion(true);
  settings.setQueryTimeout(1000);
  settings.setSqlLogType(SqlLogType.RAW);
  settings.setSuppressOptimisticLockException(true);
  settings.excludeNull(true);
}).set(c -> {
  c.value(e.employeeName, "aaa");
}).execute();

除外するカラムを指定することもできます。

Employee employee = createEmployee();

Result<Employee> result = queryDsl.update(e, settings ->
    settings.exclude(e.hiredate, e.salary)
).single(employee).execute();

注釈

WHERE句なしで更新を実行するには、 allowEmptyWhere の設定を有効にしてください。

エンティティによるレコードの更新

単一のエンティティを更新するには次のようにします。

void doSomething() {
    Employee employee = queryDsl.from(e).where(c -> c.eq(e.employeeId, 5)).fetchOne();
    employee.setEmployeeName("aaa");
    employee.setSalary(new Salary("2000"));
    
    Result<Employee> result = queryDsl.update(e).single(employee).execute();
}

このクエリは次のように生成されます。

update EMPLOYEE set EMPLOYEE_NAME = ?, SALARY = ?, VERSION = ? + 1
where EMPLOYEE_ID = ? and VERSION = ?

バッチ更新もサポートされています。

void doSomething() {
    Employee employee = createEmployee();
    Employee employee2 = createEmployee();
    List<Employee> employees = Arrays.asList(employee, employee2);
    
    BatchResult<Employee> result = queryDsl.update(e).batch(employees).execute();
}

execute メソッドによって投げられる例外は次のとおりです。

  • OptimisticLockException: エンティティにバージョンプロパティがあり、更新件数が 0 の場合

エンティティによるレコードの更新と更新されたレコードの取得

returning メソッドを呼び出すことで、エンティティを更新すると同時に更新したエンティティを取得できます。

Department result = queryDsl.update(d).single(department).returning().fetchOne();

このSQLを生成します。

update DEPARTMENT set DEPARTMENT_NO = ?, DEPARTMENT_NAME = ?, LOCATION = ?, VERSION = ? + 1
where DEPARTMENT_ID = ? and VERSION = ?
returning DEPARTMENT_ID, DEPARTMENT_NO, DEPARTMENT_NAME, LOCATION, VERSION

returning メソッドで返すプロパティを指定することもできます。

結果を Optional として受け取るには、fetchOne の代わりに fetchOptional メソッドを使用します。

注釈

この機能は、H2 Database、PostgreSQL、SQL Server、およびSQLiteのダイアレクトのみがサポートしています。

Where 式によるレコードの更新

条件に基づいてレコードを更新するには次のようにします。

int count = queryDsl.update(e)
    .set(c -> c.value(e.departmentId, 3))
    .where(c -> {
        c.isNotNull(e.managerId);
        c.ge(e.salary, new Salary("2000"));
    })
    .execute();

このクエリは次のように生成されます。

update EMPLOYEE t0_ set t0_.DEPARTMENT_ID = ?
where t0_.MANAGER_ID is not null and t0_.SALARY >= ?

プロパティ式

すべてのプロパティ式メソッドは org.seasar.doma.jdbc.criteria.expression.Expressions クラスにあり、静的なインポートで使用できます。

算術式

算術式には以下のメソッドがあります。

  • add - (+)

  • sub - (-)

  • mul - (*)

  • div - (/)

  • mod - (%)

add メソッドを使用する例です。

int count = queryDsl.update(e)
    .set(c -> c.value(e.version, add(e.version, 10)))
    .where(c -> c.eq(e.employeeId, 1))
    .execute();

このクエリは次のように生成されます。

update EMPLOYEE t0_
set t0_.VERSION = (t0_.VERSION + ?)
where t0_.EMPLOYEE_ID = ?

文字列関数

次の文字列関数が提供されます。

  • concat

  • lower

  • upper

  • trim

  • ltrim

  • rtrim

concat を使用した例です。

int count = queryDsl.update(e)
    .set(c -> c.value(e.employeeName, concat("[", concat(e.employeeName, "]"))))
    .where(c -> c.eq(e.employeeId, 1))
    .execute();

このクエリは次のように生成されます。

update EMPLOYEE t0_
set t0_.EMPLOYEE_NAME = concat(?, concat(t0_.EMPLOYEE_NAME, ?))
where t0_.EMPLOYEE_ID = ?

リテラル式

literal メソッドはすべての基本データ型をサポートしています。

literal を使用する例です。

Employee employee = queryDsl.from(e)
    .where(c -> c.eq(e.employeeId, literal(1)))
    .fetchOne();

このクエリは次のように生成されます。

select t0_.EMPLOYEE_ID, t0_.EMPLOYEE_NO, t0_.EMPLOYEE_NAME, t0_.MANAGER_ID, t0_.HIREDATE,
t0_.SALARY, t0_.DEPARTMENT_ID, t0_.ADDRESS_ID, t0_.VERSION
from EMPLOYEE t0_
where t0_.EMPLOYEE_ID = 1

注釈

リテラル式はバインド変数として扱われるのではなく、SQLに直接埋め込まれることに注意してください。

Case 式

以下のメソッドがcase式でサポートされています。

  • when

when を使用する例は次のとおりです。

List<String> list = queryDsl
    .from(e)
    .select(
        when(c -> {
            c.eq(e.employeeName, literal("SMITH"), lower(e.employeeName));
            c.eq(e.employeeName, literal("KING"), lower(e.employeeName));
        }, literal("_")))
    .fetch();

このクエリは次のように生成されます。

select case
        when t0_.EMPLOYEE_NAME = 'SMITH' then lower(t0_.EMPLOYEE_NAME)
        when t0_.EMPLOYEE_NAME = 'KING' then lower(t0_.EMPLOYEE_NAME)
        else '_' end
from EMPLOYEE t0_

サブクエリ select 式

select メソッドはサブクエリのselect式をサポートします。

使用例です。

Employee_ e = new Employee_();
Employee_ e2 = new Employee_();
Department_ d = new Department_();

SelectExpression<Salary> subSelect = select(c ->
    c.from(e2)
     .innerJoin(d, on -> on.eq(e2.departmentId, d.departmentId))
     .where(cc -> cc.eq(e.departmentId, d.departmentId))
     .groupBy(d.departmentId)
     .select(max(e2.salary))
);

int count = queryDsl.update(e)
    .set(c -> c.value(e.salary, subSelect))
    .where(c -> c.eq(e.employeeId, 1))
    .execute();

このクエリは次のように生成されます。

update EMPLOYEE t0_
set t0_.SALARY = (
    select max(t1_.SALARY)
    from EMPLOYEE t1_
    inner join DEPARTMENT t2_ on (t1_.DEPARTMENT_ID = t2_.DEPARTMENT_ID)
    where t0_.DEPARTMENT_ID = t2_.DEPARTMENT_ID
    group by t2_.DEPARTMENT_ID
)
where t0_.EMPLOYEE_ID = ?

ユーザー定義式

Expressions.userDefined を利用することで、ユーザー定義の式を定義できます。

カスタムの replace 関数を定義する例です。

UserDefinedExpression<String> replace(PropertyMetamodel<String> expression, PropertyMetamodel<String> from, PropertyMetamodel<String> to) {
    return Expressions.userDefined(expression, "replace", from, to, c -> {
        c.appendSql("replace(");
        c.appendExpression(expression);
        c.appendSql(", ");
        c.appendExpression(from);
        c.appendSql(", ");
        c.appendExpression(to);
        c.appendSql(")");
    });
}

クエリでカスタムの replace 関数を使用します。

List<String> list = queryDsl
    .from(d)
    .select(replace(d.location, Expressions.literal("NEW"), Expressions.literal("new")))
    .fetch();

このクエリは次のように生成されます。

select replace(t0_.LOCATION, 'NEW', 'new') from DEPARTMENT t0_

スコープ

スコープを使用すると、よく使用されるクエリ条件を指定できます。

スコープを定義するには、 @Scope で注釈されたメソッドを持つクラスを作成します。

public class DepartmentScope {
    @Scope
    public Consumer<WhereDeclaration> onlyTokyo(Department_ d) {
        return c -> c.eq(d.location, "Tokyo");
    }
}

スコープを有効にするには、@Metamodelscopes 要素にスコープクラスを指定します。

@Entity(metamodel = @Metamodel(scopes = { DepartmentScope.class }))
public class Department { 
  // ...
}

Department_ には onlyTokyo メソッドが含まれています。このメソッドは以下のように使用できます。

List<Department> list = queryDsl.from(d).where(d.onlyTokyo()).fetch();

このクエリは次のように生成されます。

select t0_.DEPARTMENT_ID, t0_.DEPARTMENT_NO, t0_.DEPARTMENT_NAME, t0_.LOCATION, t0_.VERSION from DEPARTMENT t0_
where t0_.LOCATION = ?

他のクエリ条件をスコープと組み合わせるには、andThen メソッドを使用します。

List<Department> list = queryDsl
    .from(d)
    .where(d.onlyTokyo().andThen(c -> c.gt(d.departmentNo, 50)))
    .fetch();

クラス内で複数のスコープを定義する:

public class DepartmentScope {
    @Scope
    public Consumer<WhereDeclaration> onlyTokyo(Department_ d) {
        return c -> c.eq(d.location, "Tokyo");
    }

    @Scope
    public Consumer<WhereDeclaration> locationStartsWith(Department_ d, String prefix) {
        return c -> c.like(d.location, prefix, LikeOption.prefix());
    }

    @Scope
    public Consumer<OrderByNameDeclaration> sortByNo(Department_ d) {
        return c -> c.asc(d.departmentNo);
    }
}

ちょっとした便利機能

DAO での実行

DAOインタフェースのデフォルトメソッド内でDSLを実行すると便利です。 config オブジェクトを取得するには、デフォルトメソッド内で Config.get(this) を呼び出します。

@Dao
public interface EmployeeDao {

  default Optional<Employee> selectById(Integer id) {
    QueryDsl queryDsl = new QueryDsl(Config.get(this));

    Employee_ e = new Employee_();
    return queryDsl.from(e).where(c -> c.eq(e.employeeId, id)).fetchOptional();
  }
}

また、 QueryDsl.of(this)new QueryDsl(Config.get(this)) のショートカットとして使用することもできます。

@Dao
public interface EmployeeDao {

  default Optional<Employee> selectById(Integer id) {
    Employee_ e = new Employee_();
    return QueryDsl.of(this).from(e).where(c -> c.eq(e.employeeId, id)).fetchOptional();
  }
}

非常に大きなバッチでメモリ使用量を抑える

Query DSL のエンティティバッチ操作では、Config.getQueryImplementors() が提供する自動バッチクエリ実装が使用されます。デフォルトでは、これらの実装はバッチに含まれるすべての PreparedSql をデータベースへの送信前にあらかじめ構築します。数十万件規模の非常に大きなエンティティリストでは、batchSize を設定していてもヒープを使い切る可能性があります。batchSizeexecuteBatch() 1 回あたりに JDBC ドライバへ送る件数を決めるだけで、メモリ上に同時に存在する PreparedSql の数を制御するものではないからです。

Query DSL のバッチ操作でピーク時のメモリ使用量を抑えるには、ConfigQuery implementors をオーバーライドして、チャンク化されたクエリ実装をオプトインで利用できます:

public class MyConfig implements Config {
    private final QueryImplementors queryImplementors = new QueryImplementors() {
        @Override
        public <ENTITY> AutoBatchInsertQuery<ENTITY> createAutoBatchInsertQuery(
                Method method, EntityType<ENTITY> entityType) {
            return new ChunkedAutoBatchInsertQuery<>(entityType);
        }

        @Override
        public <ENTITY> AutoBatchUpdateQuery<ENTITY> createAutoBatchUpdateQuery(
                Method method, EntityType<ENTITY> entityType) {
            return new ChunkedAutoBatchUpdateQuery<>(entityType);
        }

        @Override
        public <ENTITY> AutoBatchDeleteQuery<ENTITY> createAutoBatchDeleteQuery(
                Method method, EntityType<ENTITY> entityType) {
            return new ChunkedAutoBatchDeleteQuery<>(entityType);
        }
    };

    @Override
    public QueryImplementors getQueryImplementors() {
        return queryImplementors;
    }
    // ... other Config methods ...
}

このオーバーライドを適用すると、queryDsl.insert(e).batch(entities)queryDsl.update(e).batch(entities)queryDsl.delete(e).batch(entities) などの Query DSL のエンティティバッチ操作では、コマンドが代表となる PreparedStatement を作成できるように、最初の SQL 文だけがあらかじめ準備されます。残りの SQL 文は実行中にエンティティ 1 件ずつ構築され、バインド・JDBC バッチへの追加を行ったのち、GC 対象になります。JDBC バッチのフラッシュ境界は引き続き batchSize に従いますが、メモリ上の SQL リスト自体は O(1) に保たれます。

これは完全なオプトインです。QueryImplementors を差し替えない限り、挙動は変わりません。

テーブル名の上書き

メタモデルのコンストラクタは修飾されたテーブル名を受け付けることができます。これにより、デフォルトのテーブル名を上書きできます。

この機能は、同じ構造を共有する2つのテーブルを扱う場合に便利です。

Department_ da = new Department_("DEPARTMENT_ARCHIVE");
Department_ d = new Department_();

int count = queryDsl
    .insert(da)
    .select(c -> c.from(d))
    .execute();

このクエリは次のように生成されます。

insert into DEPARTMENT_ARCHIVE (DEPARTMENT_ID, DEPARTMENT_NO, DEPARTMENT_NAME,
LOCATION, VERSION) select t0_.DEPARTMENT_ID, t0_.DEPARTMENT_NO, t0_.DEPARTMENT_NAME,
t0_.LOCATION, t0_.VERSION from DEPARTMENT t0_

ユーザー定義演算子

ユーザー定義の演算子は、 UserDefinedCriteriaContext インスタンスをコンストラクタを介して受け入れるクラスのメソッドとして実装することができます。

record MyExtension(UserDefinedCriteriaContext context) {
  public void regexp(PropertyMetamodel<String> propertyMetamodel, String regexp) {
    context.add(
        (b) -> {
          b.appendExpression(propertyMetamodel);
          b.appendSql(" ~ ");
          b.appendParameter(propertyMetamodel, regexp);
        });
  }
}

上記のクラスは、クエリの WHERE、JOIN、または HAVING 句で次のように使用できます。

var d = new Department_();
var list =
    queryDsl.from(d)
        .where(c -> c.extension(MyExtension::new, (ext) -> {
            ext.regexp(d.departmentName, "A");
        }))
        .orderBy(c -> c.asc(d.departmentId))
        .select()
        .fetch();

上記のクエリは次の SQL に変換されます。

select t0_.DEPARTMENT_ID, t0_.DEPARTMENT_NO, t0_.DEPARTMENT_NAME, t0_.LOCATION, t0_.VERSION from DEPARTMENT t0_
where t0_.DEPARTMENT_NAME ~ ?
order by t0_.DEPARTMENT_ID asc

デバッグ

DSL によって生成される SQL ステートメントを調べるには、asSql メソッドを使用します。

void doSomething() {
    Department_ d = new Department_();
    
    Listable<Department> stmt = queryDsl.from(d).where(c -> c.eq(d.departmentName, "SALES"));
    
    Sql<?> sql = stmt.asSql();
    System.out.printf("Raw SQL      : %s\n", sql.getRawSql());
    System.out.printf("Formatted SQL: %s\n", sql.getFormattedSql());
}

上記のコードは以下を出力します。

Raw SQL      : select t0_.DEPARTMENT_ID, t0_.DEPARTMENT_NO, t0_.DEPARTMENT_NAME, t0_.LOCATION, t0_.VERSION from DEPARTMENT t0_ where t0_.DEPARTMENT_NAME = ?
Formatted SQL: select t0_.DEPARTMENT_ID, t0_.DEPARTMENT_NO, t0_.DEPARTMENT_NAME, t0_.LOCATION, t0_.VERSION from DEPARTMENT t0_ where t0_.DEPARTMENT_NAME = 'SALES'

asSql メソッドはSQLステートメントをデータベースに対して実行しません。代わりに、SQLステートメントを構築し、Sql オブジェクトとして返します。

peek メソッドを使用して Sql オブジェクトを取得することもできます。

List<String> locations = queryDsl
    .from(d)
    .peek(System.out::println)
    .where(c -> c.eq(d.departmentName, "SALES"))
    .peek(System.out::println)
    .orderBy(c -> c.asc(d.location))
    .peek(sql -> System.out.println(sql.getFormattedSql()))
    .select(d.location)
    .peek(sql -> System.out.println(sql.getFormattedSql()))
    .fetch();

上記のコードは、クエリの様々な段階で SQL ステートメントを出力します。

select t0_.DEPARTMENT_ID, t0_.DEPARTMENT_NO, t0_.DEPARTMENT_NAME, t0_.LOCATION, t0_.VERSION from DEPARTMENT t0_
select t0_.DEPARTMENT_ID, t0_.DEPARTMENT_NO, t0_.DEPARTMENT_NAME, t0_.LOCATION, t0_.VERSION from DEPARTMENT t0_ where t0_.DEPARTMENT_NAME = ?
select t0_.DEPARTMENT_ID, t0_.DEPARTMENT_NO, t0_.DEPARTMENT_NAME, t0_.LOCATION, t0_.VERSION from DEPARTMENT t0_ where t0_.DEPARTMENT_NAME = 'SALES' order by t0_.LOCATION asc
select t0_.LOCATION from DEPARTMENT t0_ where t0_.DEPARTMENT_NAME = 'SALES' order by t0_.LOCATION asc

サンプルプロジェクト

追加のガイダンスについては、以下のサンプルプロジェクトを参照してください。