# DAOインタフェース

uroboroSQLはDAO(Data Access Object)を用いた単一テーブルへのCRUDに対応しています。

下記のテーブルとそれに対応するエンティティクラスを例として説明します。

-- MySQLの場合
create table employee (
  emp_no number(6) not null auto_increment
  , first_name varchar(20) not null
  , last_name varchar(20) not null
  , birth_date date not null
  , gender char(1) not null
  , email varchar(100) null
  , lock_version number(10) not null
  , constraint employee_PKC primary key (emp_no)
)

-- Postgresqlの場合
create table employee (
  emp_no serial not null
  , first_name varchar(20) not null
  , last_name varchar(20) not null
  , birth_date date not null
  , gender char(1) not null
  , email varchar(100) null
  , lock_version number(10) not null
  , constraint employee_PKC primary key (emp_no)
) ;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Employee {
  private long empNo;
  private String firstName;
  private String lastName;
  private LocalDate birthDate;
  private Gender gender;
  private Optional<String> email = Optional.empty();
  private long lockVersion = 0;

  // 中略 getter/setter
}
1
2
3
4
5
6
7
8
9
10
11

# エンティティクラスの検索

# キーを指定した1件取得(SqlAgent#find)

メソッド名 戻り値の型
SqlAgent#find(Class<E>, Object...) Optional<E>

主キーを指定してエンティティを取得します。PKカラムの数と引数に指定するキーの数は合わせる必要があります。

// emp_no = 1 のレコードをエンティティとして取得
Optional<Employee> employee = agent.find(Employee.class, 1);
1
2

# 条件指定検索(SqlAgent#query) 0.11.0+

メソッド名 戻り値の型
SqlAgent#query(Class<E>) SqlEntityQuery<E>

エンティティクラスを利用した検索を行うためのオブジェクト(SqlEntityQuery)を取得します。
SqlEntityQueryに対して抽出条件の指定を行い、抽出条件に該当するエンティティを取得します。


# 抽出条件の指定(SqlEntityQuery#equal /#notEqual /#greaterThan /#lessThan /#greaterEqual /#lessEqual /#in /#notIn /#like /#startsWith /#endsWith /#contains /#notLike /#notStartsWith /#notEndsWith /#notContains /#between /#notBetween /#betweenColumns /#notBetweenColumns /#isNull /#isNotNull /#where)

抽出条件指定メソッド記述例 生成されるwhere句の条件式 補足説明
equal("col", "value") col = 'value'
notEqual("col", "value") col != 'value'
greaterThan("col", 1) col > 1
lessThan("col", 1) col < 1
greaterEqual("col", 1) col >= 1
lessEqual("col", 1) col <= 1
in("col", "val1", "val2") col in ('val1', 'val2')
in("col", List.of("val1", "val2")) col in ('val1', 'val2')
notIn("col", "val1", "val2") col not in ('val1', 'val2')
notIn("col", List.of("val1", "val2")) col not in ('val1', 'val2')
like("col", "%val%") like '%val%' valはエスケープされない
startsWith("col", "val") like 'val%' valはエスケープされる
endsWith("col", "val") like '%val' valはエスケープされる
contains("col", "val") like '%val%' valはエスケープされる
notLike("col", "%val%") not like '%val%' valはエスケープされない
notStartsWith("col", "val") not like 'val%' valはエスケープされる
notEndsWith("col", "val") not like '%val' valはエスケープされる
notContains("col", "val") not like '%val%' valはエスケープされる
between("col", 1, 2) col between 1 and 2
notBetween("col", 1, 2) 0.23.0+ col not between 1 and 2
betweenColumns(2, "col1", "col2") 0.23.0+ 2 between col1 and col2
notBetweenColumns(2, "col1", "col2") 0.23.0+ 2 not between col1 and col2
isNull("col") col is null
isNotNull("col") col is not null
where("col = 1 or col = 2") (col = 1 or col = 2) もし複数回where()が呼び出された場合は条件を AND で結合する
where("col = /*col1*/", "col1", 1) (col = 1/*col1*/) パラメータの指定(1件)付き
where("col = /*col1*/ or col = /*col2*/", Map.of("col1", 1, "col2", 2)) (col = 1/*col1*/ or col = 2/*col2*/) パラメータの指定(複数件)付き
// emp_no = 1 のレコードをList<Employee>で取得
agent.query(Employee.class).equal("emp_no", 1).collect();

// emp_no = 10 又は 20 のレコードをList<Employee>で取得
agent.query(Employee.class).in("emp_no", 10, 20).collect();

// first_name like '%Bob%' のレコードをList<Employee>で取得
agent.query(Employee.class).contains("first_name", "Bob").collect();

// where句を直接記述(first_name = 'Bob' and last_name = 'Smith')した結果をList<Employee>で取得
agent.query(Employee.class).where("first_name =''/*firstName*/", "firstName", "Bob").where("last_name = ''/*lastName*/", "lastName", "Smith").collect();
1
2
3
4
5
6
7
8
9
10
11

注意

同じカラムに対して where 以外の抽出条件指定メソッドを複数指定した場合、最後に指定した抽出条件が有効になります。

agent.query(Employee.class)
    .greaterThan("emp_no", 20)
    .lessEqual("emp_no", 10)
    .collect();
1
2
3
4

上記の場合、 lessEqual メソッドの指定が有効になり、以下のSQLが発行されます。
greaterThan メソッドの指定は無視されます)

select
  emp_no      as emp_no
, first_name  as first_name
, last_name   as last_name
...
from employee
where
  emp_no <= 10
1
2
3
4
5
6
7
8

警告

SqlEntityQueryに対して抽出条件を指定する場合paramメソッドは使用しないでください。 SqlEntityQuery#param()には@Deprecatedが付与されており、将来削除される予定です。

# ソート順(SqlEntityQuery#asc /#desc)や取得データの件数(#limit)、開始位置(#offset)、悲観ロック(#forUpdate /#forUpdateNoWait /#forUpdateWait)の指定 0.11.0+

SqlEntityQueryでは抽出条件に加えて検索結果のソート順や取得件数の制限、開始位置の指定、明示的なロック指定が行えます。

条件指定メソッド記述例 生成されるSQL 補足説明
asc("col1", "col2") order by col1 asc, col2 asc NULLSが有効な場合はNULLS LASTが出力される
asc("col1", Nulls.FIRST) order by col1 asc NULLS FIRST 複数回asc()が呼び出された場合は呼び出し順に並べる
desc("col1", "col2") order by col1 desc, col2 desc NULLSが有効な場合はNULLS LASTが出力される
desc("col1", Nulls.FIRST) order by col1 desc NULLS FIRST 複数回asc()が呼び出された場合は呼び出し順に並べる
limit(10) LIMIT 10 接続しているDBでlimit句が使用できない場合はUroborosqlRuntimeExceptionがスローされる
offset(10) OFFSET 10 接続しているDBでoffset句が使用できない場合はUroborosqlRuntimeExceptionがスローされる
forUpdate()0.14.0+ FOR UPDATE 接続しているDBでFOR UPDATE句が使用できない場合はUroborosqlRuntimeExceptionがスローされる
forUpdateNoWait()0.14.0+ FOR UPDATE NOWAIT 接続しているDBでFOR UPDATE NOWAIT句が使用できない場合はUroborosqlRuntimeExceptionがスローされる
forUpdateWait()0.14.0+ FOR UPDATE WAIT 10 接続しているDBでFOR UPDATE WAIT句が使用できない場合はUroborosqlRuntimeExceptionがスローされる
forUpdateWait(30)0.14.0+ FOR UPDATE WAIT 30 接続しているDBでFOR UPDATE WAIT句が使用できない場合はUroborosqlRuntimeExceptionがスローされる
// birth_dateの降順、first_nameの昇順でソートした結果を List<Employee>で取得
agent.query(Employee.class).desc("birth_date").asc("first_name").collect();

// emp_no の昇順でソートした結果の3行目から5件取得
agent.query(Employee.class).asc("emp_no").offset(3).limit(5).collect();

// 明示的な行ロックを行う
agent.query(Employee.class).forUpdate().collect();
1
2
3
4
5
6
7
8

# オプティマイザーヒントの指定(SqlEntityQuery#hint) 0.18.0+

SqlEntityQuery#hint()を使用することで、SQLに対してオプティマイザーヒントを指定することができます。

SqlAgent agent = ...
agent.query(User.class).hint("ORDERED").lessThan("age", 30).collect();
1
2

出力されるSQL(Oracleの場合)

select /*+ ORDERED */ id, name, age, ... from user where age < 30
1

注意

オプティマイザーヒントの指定は、利用するDBがオプティマイザーヒントをサポートしている場合に有効になります。
また、指定可能なヒント句は利用するDBに依存します。

# 検索結果の取得(SqlEntityQuery#collect /#first /#one /#select /#stream)

SqlEntityQueryから抽出条件に該当するエンティティを取得します。

メソッド 説明
collect() 検索結果をエンティティのリストとして取得する
first() 検索結果の先頭行を取得する
one() 検索結果の先頭行を取得する。検索結果が2件以上の場合DataNonUniqueExceptionをスローする
Stream<C> select(String col, Class<C> type) 0.18.0+ 検索結果の指定したカラムの値をjava.util.stream.Streamとして取得する。
stream() 検索結果をjava.util.stream.Streamとして取得する
// List<Employee>で取得
List<Employee> employees = agent.query(Employee.class).collect();

// 検索結果の先頭行を取得
Optional<Employee> employee = agent.query(Employee.class).first();

// 検索結果(カラム値)の取得
String employeeName = agent.query(Employee.class)
    .equal("employeeId", 1)
    .select("employeeName", String.class).findFirst().orElseThrow();
1
2
3
4
5
6
7
8
9
10

# 取得するカラム/除外するカラムの指定(#includeColumns / #excludeColumns) 0.23.0+

検索結果の取得 を呼び出す前に検索結果に含めるカラムを指定することで 利用しないカラムに対する不要なアクセスを減らすことができます。

メソッド 説明
includeColumns(String... cols) 検索結果に含めるカラム名を指定する(複数指定可)
excludeColumns(String... cols) 検索結果から除外するカラム名を指定する(複数指定可)
// List<Employee>を取得 (取得したEmployeeインスタンスにはemployeeIdのみ設定されている)
List<Employee> employees = agent.query(Employee.class)
                              .includeColumns("employeeId")
                              .collect();

// List<Employee>を取得 (取得したEmployeeインスタンスにはemployeeName以外が設定されている)
List<Employee> employees = agent.query(Employee.class)
                              .excludeColumns("employeeName")
                              .collect();
1
2
3
4
5
6
7
8
9

# 集約関数(SqlEntityQuery#count /#sum /#sum /#min /#max /#exists /#notExists) 0.12.0+

SqlEntityQueryではエンティティを取得する他に結果の集計を行うこともできます。

メソッド 説明
count() 検索結果の件数を取得する
count(String col) 検索結果のうち、引数で指定したカラムがNULLでない行の件数を取得する
sum(String col) 検索結果のうち、引数で指定したカラムの合計値を取得する
min(String col) 検索結果のうち、引数で指定したカラムの最小値を取得する
max(String col) 検索結果のうち、引数で指定したカラムの最大値を取得する
exists(Runnable runnable) 検索結果が1件以上ある場合に引数で渡した関数を実行する
notExists(Runnable runnable) 検索結果が0件の場合に引数で渡した関数を実行する
// 検索結果の件数を取得
long count = agent.query(Employee.class).count();

// 検索結果が1件以上の場合にログを出力する
agent.query(Employee.class).greaterThan("emp_no", 10).exists(() -> {
  log.info("Employee(emp_no > 10) exists.");
});
1
2
3
4
5
6
7

TIP

集約関数を使用すると、検索結果からEntityオブジェクトを生成しないためメモリ効率が良くなります。 以下2つの処理結果は同じですが、メモリの使い方が違います。

// collect()を使用すると、検索結果がエンティティに変換されるためメモリを使用する
long count = agent.query(Employee.class).collect().size();

// count()を使用すると件数のみ取得できる(エンティティは生成されない)
long count = agent.query(Employee.class).count();
1
2
3
4
5

# エンティティの挿入

# 1件の挿入(SqlAgent#insert/#insertAndReturn)

メソッド名 戻り値の型
<E> SqlAgent#insert(E) int
<E> SqlAgent#insertAndReturn(E) 0.15.0+ E

エンティティクラスのインスタンスを使って1レコードの挿入を行います。

  • @Idアノテーションの指定があるフィールド
  • 対するカラムが自動採番となっているフィールド

上記の型がprimitive型の場合、もしくはフィールドの値がnullの場合、カラムの値は挿入時に自動採番されます。
また、挿入により採番された値がエンティティの該当フィールドにも設定されます。
フィールドに値を指定した場合は自動採番カラムであっても指定した値が挿入されます。

デフォルト値の指定があるカラムに対するフィールドが null の場合、カラムの値にデフォルト値が設定されます。

NULL可であるカラムに対するフィールドの値が null の場合、そのカラムに値は設定されず、結果として NULL になるか、またはデフォルト値が設定されます。
NULL可であるカラムに対するフィールドの型が Optional型の場合、Optional.empty() が設定されていればそのカラムには NULL が設定されます。Optional.empty() 以外の値が設定されていれば、Optionalが内包する値が設定されます。

AndReturnが付くメソッドでは、挿入したエンティティオブジェクトを戻り値として取得できるため、 エンティティの挿入に続けて処理を行う場合に便利です。

Employee employee = new Employee();
employee.setFirstName("Susan");
employee.setLastName("Davis");
employee.setBirthDate(LocalDate.of(1969, 2, 10));
employee.setEmail(Optional.of("susan.davis@sample.com")); // email カラムには susan.davis@sample.com が設定される
employee.setGender(Gender.FEMALE); // MALE("M"), FEMALE("F"), OTHER("O")

// 1件の挿入
agent.insert(employee);
System.out.println(employee.getEmpNo()); // 自動採番された値が出力される
1
2
3
4
5
6
7
8
9
10

# 複数件の挿入(SqlAgent#inserts /#insertsAndReturn) 0.10.0+

メソッド名 戻り値の型
SqlAgent#inserts(Stream<E>) int
SqlAgent#inserts(Stream<E>, InsertsType) int
SqlAgent#inserts(Stream<E>, InsertsCondition<? super E>) int
SqlAgent#inserts(Stream<E>, InsertsCondition<? super E>, InsertsType) int
SqlAgent#insertsAndReturn(Stream<E>) 0.15.0+ Stream<E>
SqlAgent#insertsAndReturn(Stream<E>, InsertsType) 0.15.0+ Stream<E>
SqlAgent#insertsAndReturn(Stream<E>, InsertsCondition<? super E>) 0.15.0+ Stream<E>
SqlAgent#insertsAndReturn(Stream<E>, InsertsCondition<? super E>, InsertsType) 0.15.0+ Stream<E>
SqlAgent#inserts(Class<E>, Stream<E>) int
SqlAgent#inserts(Class<E>, Stream<E>, InsertsType) int
SqlAgent#inserts(Class<E>, Stream<E>, InsertsCondition<? super E>) int
SqlAgent#inserts(Class<E>, Stream<E>, InsertsCondition<? super E>, InsertsType) int
SqlAgent#insertsAndReturn(Class<E>, Stream<E>) 0.15.0+ Stream<E>
SqlAgent#insertsAndReturn(Class<E>, Stream<E>, InsertsType) 0.15.0+ Stream<E>
SqlAgent#insertsAndReturn(Class<E>, Stream<E>, InsertsCondition<? super E>) 0.15.0+ Stream<E>
SqlAgent#insertsAndReturn(Class<E>, Stream<E>, InsertsCondition<? super E>, InsertsType) 0.15.0+ Stream<E>

java.util.stream.Stream経由で渡される複数のエンティティインスタンスを挿入します。

  • @Idアノテーションの指定があるフィールド
  • 対するカラムが自動採番となっているフィールド

の型がprimitive型の場合、もしくはフィールドの値がnullの場合、カラムの値は挿入時に自動採番されます。
また、挿入により採番された値がエンティティの該当フィールドにも設定されます。
フィールドに値を指定した場合は自動採番カラムであっても指定した値が挿入されます。

注意

複数件の挿入で生成されるSQLでは、行毎のフィールドの値の有無を変更することができません。
最初に挿入するエンティティで@Idの指定があるフィールドや自動採番カラムに対するフィールドに値を設定する場合は、 2件目以降のエンティティにも必ず値を設定するようにしてください。
また、最初に挿入するエンティティで@Idの指定があるフィールドや自動採番カラムに対するフィールドの値にnullを設定する場合は、 2件目以降のエンティティで値を設定していても無視されて自動採番されます。

AndReturnが付くメソッドでは、挿入したエンティティオブジェクトのjava.util.stream.Streamを戻り値として取得できるため、 エンティティの挿入に続けて処理を行う場合に便利です。

注意

AndReturnの戻り値となるStream<E>を生成する際、挿入したエンティティを全件メモリ上に保持します。 大量データの挿入を行うとOOMEが発生する場合があるので、insertsAndReturnを使用する場合は挿入する データの件数に気をつけてください。件数が多い場合は一度insertsで挿入した後に、再度検索するといった方法を検討してください。

// 1件の挿入
Department dept = new Department();
dept.setDeptName("sales");
agent.insert(dept);

// 複数件の挿入(EmployeeとDeptEmpの挿入)
agent.inserts(agent.insertsAndReturn(agent.query(Employee.class).stream())
  .map(e -> {
    DepEmp deptEmp = new DeptEmp();
    deptEmp.setEmpNo(e.getEmpNo());
    deptEmp.setDepNo(dept.getDepNo());
    return deptEmp;
  })
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 挿入方法(InsertsType)の指定

InsertsTypeを指定することで実行される挿入用のSQLを変更することが出来ます。

InsertsType 説明
BATCH java.sql.PreparedStatement#executeBatch()を使用したバッチSQL実行
BULK insert into ... values ( ... ), ( ... )という風にvaluesに複数行の値を出力し一度に複数レコードを挿入する。
DBがこの記法をサポートしている場合に指定可能。DBが未サポートの場合、指定してもBATCHとして実行される。
Stream<Employee> employees = agent.query(Employee.class)
  .stream()
  .map(e -> e.setEmpNo(e.getEmpNo() + 1000));
  
// 複数件の挿入(バッチ実行)
agent.inserts(employees, InsertsType.BATCH);
}
1
2
3
4
5
6
7

TIP

InsertsTypeは、初期値設定が可能です。

BATCH と BULK の選択について

一般的に少ないレコード数を挿入する場合は BULK を指定する方が早くなります。(DBの種類やJDBCドライバーの実装、割り当てられたメモリにより差異はあります)
これは、 BULK の動作が以下のように2段階の動作であるのに対し、

  1. SQLをpreparedStatementに変換
  2. SQL発行

BATCH の動作が以下のような動作になっているためです。

  1. SQLをpreparedStatementに変換
  2. 挿入条件で指定した条件になるまで蓄積したデータをDBに送信
  3. SQL発行
  4. 挿入するデータが無くなるまで2~3の繰り返し

しかしBULK で発行するSQLは挿入する件数に比例して肥大化し、それに伴い 1. の preparedStatement への変換に時間がかかるようになります。
それに比べて BATCH では挿入する件数が増えても BULK に比べて処理時間の増加が緩やかなので、挿入する件数が増えると BATCH のほうが高速になります。
BULK を指定する場合は実際に挿入にかかる時間を計測し、BATCH より早いことを確認してください。

# 挿入条件(InsertsCondition)の指定

挿入用SQLの実行条件を指定します。
InsertsCondition<E>#test(SqlContext ctx, int count, E entity)の戻り値がtrueの場合に挿入用SQLを実行します。
InsertsConditionはFunctionalInterfaceのためlambda式が利用できます。

Stream<Employee> employees = agent.query(Employee.class)
  .stream()
  .map(e -> e.setEmpNo(e.getEmpNo() + 1000));
  
// 複数件の挿入(10件毎に挿入)
agent.inserts(employees, (ctx, count, entity) -> count == 10);
1
2
3
4
5
6

# エンティティの更新

# 1件の更新(SqlAgent#update /#updateAndReturn)

メソッド名 戻り値の型
<E> SqlAgent#update(E) int
<E> SqlAgent#updateAndReturn(E) 0.15.0+ E

エンティティクラスのインスタンスを使って1レコードの更新を行います。

レコード更新時、@Versionアノテーションの指定があるフィールドに対するカラムはカウントアップされます。
また、更新された値がエンティティの該当フィールドにも設定されます。

NULL可であるカラムに対するフィールドの値が null の場合、そのカラムは 更新されません
NULL可であるカラムに対するフィールドの型が Optional型の場合、Optional.empty() が設定されていればそのカラムは NULL で更新されます。 Optional.empty() 以外の値が設定されていれば、Optionalが内包する値で更新されます。

補足

エンティティクラスのインスタンスを使った1レコードの更新では、@Idを指定したフィールドに対するカラムや自動採番カラムは更新できません。
@Idを指定したフィールドに対するカラムや自動採番カラムを更新する場合は、後述する条件指定による複数件の更新を使用してください。

AndReturnが付くメソッドでは、更新したエンティティオブジェクトを戻り値として取得できるため、 エンティティの更新に続けて処理を行う場合に便利です。

agent.find(Employee.class, 1).ifPresent(employee -> {
  employee.setLastName("Wilson");
  employee.setEmail(Optional.empty()); // email を null に更新
  System.out.println(employee.getLockVersion()); // 1

  // エンティティの更新
  agent.update(employee);
  System.out.println(employee.getLockVersion()); // 2
});
1
2
3
4
5
6
7
8
9

# 条件指定による複数件の更新(SqlAgent#update) 0.15.0+

メソッド名 戻り値の型
SqlAgent#update(Class<? extends E>) SqlEntityUpdate<E>

更新対象のレコードを抽出する条件を指定して更新を行います。
抽出条件の指定方法は 抽出条件の指定 を参照してください。
また、set()メソッドで更新対象のフィールドと値を指定することができます。

// first_name に 'Bob' を含むエンティティの性別を更新
agent.update(Employee.class)
  .contains("firstName", "Bob")
  .set("gender", Gender.MALE)
  .count();
1
2
3
4
5

# 複数件の更新(SqlAgent#updates /#updatesAndReturn) 0.15.0+

メソッド名 戻り値の型
SqlAgent#updates(Stream<E>) int
SqlAgent#updates(Stream<E>, UpdatesCondition<? super E>) int
SqlAgent#updatesAndReturn(Stream<E>) Stream<E>
SqlAgent#updatesAndReturn(Stream<E>, UpdatesCondition<? super E>) Stream<E>
SqlAgent#updates(Class<E>, Stream<E>) int
SqlAgent#updates(Class<E>, Stream<E>, UpdatesCondition<? super E>) int
SqlAgent#updatesAndReturn(Class<E>, Stream<E>) Stream<E>
SqlAgent#updatesAndReturn(Class<E>, Stream<E>, UpdatesCondition<? super E>) Stream<E>

java.util.stream.Stream経由で渡される複数のエンティティインスタンスを使って更新します。

TIP

insertsと違い必ずバッチSQL実行になります。

レコード更新時、@Versionアノテーションの指定があるフィールドに対するカラムはカウントアップされます。
また、更新された値がエンティティの該当フィールドにも設定されます。

AndReturnが付くメソッドでは、更新したエンティティオブジェクトのjava.util.stream.Streamを戻り値として取得できるため、 エンティティの更新に続けて処理を行う場合に便利です。

WARNING

AndReturnの戻り値となるStream<E>を生成する際、更新したエンティティを全件メモリ上に保持します。 大量データの更新を行うとOOMEが発生する場合があるので、updatesAndReturnを使用する場合は更新する データの件数に気をつけてください。件数が多い場合は一度updatesで更新した後に、再度検索するといった方法を検討してください。

// 複数件の更新
agent.updates(agent.query(Employee.class)
  .stream()
  .map(e -> {
    e.setFirstName(e.getFirstName() + "_new");
    return e;
  })
);
1
2
3
4
5
6
7
8

# 更新条件(UpdatesCondition)の指定

更新用SQLの実行条件を指定します。
UpdatesCondition<E>#test(SqlContext ctx, int count, E entity)の戻り値がtrueの場合に更新用SQLを実行します。
UpdatesConditionはFunctionalInterfaceのためlambda式が利用できます。

Stream<Employee> employees = agent.query(Employee.class)
  .stream()
  .map(e -> {
    e.setFirstName(e.getFirstName() + "_new");
    return e;
  });
  
// 複数件の更新(10件毎に挿入)
agent.updates(employees, (ctx, count, entity) -> count == 10);
1
2
3
4
5
6
7
8
9

# エンティティのマージ 0.22.0+

# 1件のマージ(SqlAgent#merge /#mergeAndReturn)

メソッド名 戻り値の型
<E> SqlAgent#merge(E) int
<E> SqlAgent#mergeAndReturn(E) E
<E> SqlAgent#mergeWithLocking(E) int
<E> SqlAgent#mergeWithLockingAndReturn(E) E

エンティティクラスのインスタンスを使ってPKによるレコードの検索を行い、レコードがある場合は更新を行います。
レコードがない場合、もしくは引数で指定したインスタンスのPKに該当するフィールドに値の指定が無い場合は挿入を行います。
(これは通常、UPSERTMERGE と呼ばれる動作です)

AndReturn が付くメソッドでは、更新、または挿入したエンティティオブジェクトを戻り値として取得できるため、 エンティティの更新や挿入に続けて処理を行う場合に便利です。

WithLocking が付くメソッドでは、PKによるレコードの検索時、レコードの悲観ロックも合わせて行います。

WARNING

接続しているDBが SELECT FOR UPDATE もしくは SELECT FOR UPDATE NOWAIT をサポートしていない場合、WithLocking が付くメソッドを呼び出すと UroborosqlRuntimeException がスローされます。

# mergeメソッドを使用しない場合

agent.find(Employee.class, 1).ifPresentOrElse(employee -> {
  employee.setLastName("Wilson");

  // エンティティの更新
  agent.update(employee);
}, () -> {
  Employee employee = new Employee();
  employee.setFirstName("Susan");
  employee.setLastName("Wilson");
  employee.setBirthDate(LocalDate.of(1969, 2, 10));
  employee.setGender(Gender.FEMALE); // MALE("M"), FEMALE("F"), OTHER("O")

  // エンティティの挿入
  agent.insert(employee);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# mergeメソッドを利用する場合(更新)

Employee employee = ...;  // find or create instance.
// employee.setId(1); // id(PK) is 1
employee.setLastName("Wilson");
agent.merge(employee);
1
2
3
4

# mergeメソッドを利用する場合(挿入)

Employee employee = new Employee();
employee.setFirstName("Susan");
employee.setLastName("Wilson");
employee.setBirthDate(LocalDate.of(1969, 2, 10));
employee.setGender(Gender.FEMALE); // MALE("M"), FEMALE("F"), OTHER("O")
agent.merge(employee);
1
2
3
4
5
6

# エンティティの削除

# 1件の削除(SqlAgent#delete /#deleteAndReturn)

メソッド名 戻り値の型
<E> SqlAgent#delete(E) int
<E> SqlAgent#deleteAndReturn(E) 0.15.0+ E

エンティティクラスのインスタンスを使って1レコードの削除を行います。

AndReturnが付くメソッドでは、削除したエンティティオブジェクトを戻り値として取得できるため、 エンティティの削除に続けて処理を行う場合に便利です。

agent.find(Employee.class, 1).ifPresent(employee -> {
  // エンティティの削除
  agent.delete(employee);
});
1
2
3
4

# PKを指定した複数件の削除(SqlAgent#delete) 0.11.0+

メソッド名 戻り値の型
SqlAgent#delete(Class<? extends E>, Object...) int
// PK(emp_no) = 1 or 2 のエンティティの削除
agent.delete(Employee.class, 1, 2);
1
2

# 条件指定による複数件の削除(SqlAgent#delete) 0.11.0+

メソッド名 戻り値の型
SqlAgent#delete(Class<? extends E>) SqlEntityDelete<E>

削除対象のレコードを抽出する条件を指定して削除を行います。
抽出条件の指定方法は 抽出条件の指定 を参照してください。

// first_name = 'Bob' に該当するエンティティの削除
agent.delete(Employee.class).contains("firstName", "Bob").count();
1
2

# 全ての行を削除(SqlAgent#truncate0.17.0+

メソッド名 戻り値の型
SqlAgent#truncate(Class<? extends E>) SqlAgent

エンティティクラスとマッピングされているテーブルの全てのレコードをTRUNCATE文により削除します。 一般的に大量レコードの削除は、TRUNCATE文による削除のほうが性能上有利ですが、DBMSによってはロールバックできませんので、注意してください。

TIP

PostgreSQLは、TRUNCATE文のロールバック可能です。

SqlAgent#truncateは、SqlAgentを戻り値として返すため、SqlAgent#truncateに続けて、SqlAgent#insertsをつなげることにより、 テーブルの洗い替えを実装することが可能です。

// 全てのレコードを削除
agent.truncate(Employee.class);

// テーブルの洗い替え
agent.truncate(Employee.class)
     .inserts(employees.stream());
1
2
3
4
5
6

DAOインタフェースで生成されるSQL

DAOインタフェースを使ってテーブルにアクセスする場合、生成されるSQLは以下のようにスキーマ名で修飾したテーブル名が出力されます。

agent.find(Employee.class, 1);
1

出力されるSQL

SELECT
  emp_no        AS emp_no
, first_name    AS first_name
, last_name     AS last_name
, birth_date    AS birth_date
, gender        AS gender
, email         AS email
, lock_version  AS lock_version
FROM PUBLIC.employee  -- 接続しているスキーマが PUBLIC の場合
WHERE emp_no = /*empNo*/1
1
2
3
4
5
6
7
8
9
10

postgresqlで複数のスキーマにまたがるテーブル群にアクセスする為にsearch_pathオプションでスキーマを複数指定するようなケースでは、修飾されたスキーマ名が期待するスキーマと一致せずテーブルにアクセスできない場合があります。
このようなケースでは、システムプロパティ uroborosql.use.qualified.table.name を指定することで出力されるSQLからスキーマ名を除くことができます。0.25.1+

// uroborosql.use.qualified.table.name システムプロパティに false を指定
System.setProperty("uroborosql.use.qualified.table.name", "false");
SqlConfig config = UroboroSQL.builder("jdbc:h2:mem:uroborosql", "sa", "").build();
try (SqlAgent agent = config.agent()) {
  agent.find(Employee.class, 1);
}
1
2
3
4
5
6

出力されるSQL

SELECT
  emp_no        AS emp_no
, first_name    AS first_name
, last_name     AS last_name
, birth_date    AS birth_date
, gender        AS gender
, email         AS email
, lock_version  AS lock_version
FROM employee  -- テーブル名のみの出力となる
WHERE emp_no = /*empNo*/1
1
2
3
4
5
6
7
8
9
10

uroborosql で指定できるシステムプロパティについては システムプロパティ を参照してください

# Entityアノテーション

DAOインタフェースで利用するエンティティクラスではテーブルとのマッピングやカラムの属性を指定するためにアノテーションを利用することができます。

# @Table

エンティティクラスに紐づけるテーブル名を指定します。
テーブル名と名前が一致しないエンティティクラスにマッピングしたい場合に利用します。

属性名 必須 説明 初期値
name String - マッピングするテーブル名。指定しない場合はクラス名をスネークケースにしたテーブルとマッピングする なし
schema String - マッピングするテーブルの所属するスキーマ名 なし
import jp.co.future.uroborosql.mapping.annotations.Table;

// name指定なし (departmentテーブルにマッピング)
@Table
public class Department {
  // 以下略
}

// name指定あり
@Table(name = "employee")
public class CustomEmployee {
  // 以下略
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# @Column

フィールドに紐づけるカラム名を指定します。
カラム名と名前が一致しないフィールドにマッピングしたい場合に利用します。

属性名 必須 説明 初期値
name String マッピングするカラム名 なし
import jp.co.future.uroborosql.mapping.annotations.Table;
import jp.co.future.uroborosql.mapping.annotations.Column;

@Table(name = "employee")
public class Employee {
  @Column(name = "emp_no")
  private long employeeNo;

  private String firstName;

  // 以下略
}
1
2
3
4
5
6
7
8
9
10
11
12

# @Domain

独自に作成した型(ドメインクラス)やEnumのフィールドにカラムをマッピングする場合に指定します。

属性名 必須 説明 初期値
valueType Class<?> ドメインクラスを生成するのに必要な値の型 なし
factoryMethod String - ドメインクラスを生成・取得するメソッド名。指定しない場合はコンストラクタが呼び出される。 ""
toJdbcMethod String - JDBCが受け付けられる値に変換するメソッド名 "getValue"
nullable boolean - null可かどうかの指定 false

import jp.co.future.uroborosql.mapping.annotations.Table;
import jp.co.future.uroborosql.mapping.annotations.Domain;

@Domain(valueType = String.class, factoryMethod = "of", toJdbcMethod = "getName", nullable = true)
public static class NameDomain {
  private String name;

  private NameDomain(String name) {
    this.name = name;
  }

  public static NameDomain of(String name) {
    return new NameDomain(name);
  }

  public String getName() {
    return name;
  }
}

@Table
public class Employee {
  private long empNo;
  private NameDomain firstName;

  // 以下略
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# @Transient

フィールドとカラムのマッピング対象から除外します。

TIP

例えば、エンタープライズシステムの設計でしばしば利用される最終登録日時や最終更新日時など、 INSERT/UPDATEの対象から除外したいケースで利用します。

属性名 必須 説明 初期値
insert boolean - agent#insert()実行時にフィールドを無視するかどうか。trueの場合は無視する。 true
update boolean - agent#update()実行時にフィールドを無視するかどうか。trueの場合は無視する。 true

import jp.co.future.uroborosql.mapping.annotations.Table;
import jp.co.future.uroborosql.mapping.annotations.Transient;

@Table
public class Employee {

  // 途中略

  @Transient
  private String memo; // 常に無視

  @Transient(insert = false, update = true)
  private LocalDate creationDate; // insert時は対象、update時は無視

  @Transient(insert = true, update = false)
  private LocalDate updateDate;  // insert時は無視、update時は対象

  // 以下略
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# @Version

このアノテーションが付与されたフィールドは楽観ロック用のバージョン情報を保持するフィールドになります。
デフォルト(LockVersionOptimisticLockSupplier)ではUPDATE時にはSET句で+1され、WHERE句の検索条件に追加されてSQLを実行し更新件数が0の場合にはOptimisticLockExceptionをスローします。

WARNING

@Versionを付与するフィールドにマッピングされるDBカラムの型は数値型でなければなりません。

属性名 必須 説明 初期値
supplier0.17.0+ OptimisticLockSupplier - バージョン情報カラム LockVersionOptimisticLockSupplier

# サプライヤの種類

サプライヤ型 概要 説明
LockVersionOptimisticLockSupplier ロックバージョン UPDATEのSET句で+1がセットされます。
CyclicLockVersionOptimisticLockSupplier 循環式ロックバージョン UPDATEのSET句でバージョン情報カラム名 % 数値カラムの最大値 + 1がセットされます。
TimestampOptimisticLockSupplier タイムスタンプ UPDATEのSET句でタイムスタンプ(System.currentTimeMillis())がセットされます。
FieldIncrementOptimisticLockSupplier フィールド値インクリメント UPDATEのSET句で2WaySQLのバインド変数を利用して、バージョン情報カラム名+1がセットされます。

import jp.co.future.uroborosql.mapping.annotations.Table;
import jp.co.future.uroborosql.mapping.annotations.Version;
import jp.co.future.uroborosql.mapping.TimestampOptimisticLockSupplier;

@Table
public class Employee {
  private long empNo;
  private String firstName;
  private String lastName;

  // 途中略

  @Version(supplier = TimestampOptimisticLockSupplier.class)
  private long lockVersion = 0;

  // 以下略
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# @Id /@GeneratedValue /@SequenceGenerator 0.12.0+

これらのアノテーションが付与されたフィールドは自動採番フィールドになります。
@Id@GeneratedValueは必ずセットでフィールドに付与する必要があります。
@GeneratedValueのstrategy属性がGenerationType.SEQUENCEの場合に@SequenceGeneratorを付与してシーケンスの生成方法を指定する必要があります。
1つのエンティティに属する複数のフィールドを自動採番フィールドとして指定することも可能です。

アノテーション 説明
@Id エンティティの自動採番フィールドを識別するアノテーション
@GeneratedValue 自動採番フィールドの値の生成戦略を指定するアノテーション
@SequenceGenerator ID生成に使用するSEQUENCEの情報を設定するアノテーション
アノテーション 属性名 必須 説明 初期値
@Id なし - - - -
@GeneratedValue strategy GenerationType - ID生成戦略の型。GenerationType.IDENTITY, GenerationType.SEQUENCEのいずれかを指定 GenerationType.IDENTITY
@SequenceGenerator sequence String シーケンス名 なし
@SequenceGenerator catalog String - シーケンスが所属するカタログ名 ""
@SequenceGenerator schema String - シーケンスが所属するスキーマ名 ""
import jp.co.future.uroborosql.mapping.annotations.Table;
import jp.co.future.uroborosql.mapping.annotations.Id;
import jp.co.future.uroborosql.mapping.annotations.GeneratedValue;
import jp.co.future.uroborosql.mapping.annotations.SequenceGenerator;

@Table
public class Employee {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private long empId;

  @Id
  @GeneratedValue(strategy = GenerationType.SEQUENCE)
  @SequenceGenerator(sequence = 'employee_emp_detail_id_seq')
  private long empDetailId;

  private String firstName;

  // 以下略
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20