# 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)
) ;
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
}
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);
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();
2
3
4
5
6
7
8
9
10
11
注意
同じカラムに対して where
以外の抽出条件指定メソッドを複数指定した場合、最後に指定した抽出条件が有効になります。
agent.query(Employee.class)
.greaterThan("emp_no", 20)
.lessEqual("emp_no", 10)
.collect();
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
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();
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();
2
出力されるSQL(Oracleの場合)
select /*+ ORDERED */ id, name, age, ... from user where age < 30
注意
オプティマイザーヒントの指定は、利用する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();
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();
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.");
});
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();
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()); // 自動採番された値が出力される
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;
})
);
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);
}
2
3
4
5
6
7
TIP
InsertsType
は、初期値設定が可能です。
BATCH と BULK の選択について
一般的に少ないレコード数を挿入する場合は BULK
を指定する方が早くなります。(DBの種類やJDBCドライバーの実装、割り当てられたメモリにより差異はあります)
これは、 BULK
の動作が以下のように2段階の動作であるのに対し、
- SQLをpreparedStatementに変換
- SQL発行
BATCH
の動作が以下のような動作になっているためです。
- SQLをpreparedStatementに変換
- 挿入条件で指定した条件になるまで蓄積したデータをDBに送信
- SQL発行
- 挿入するデータが無くなるまで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);
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
});
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();
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;
})
);
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);
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に該当するフィールドに値の指定が無い場合は挿入を行います。
(これは通常、UPSERT
や MERGE
と呼ばれる動作です)
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);
});
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);
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);
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);
});
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);
2
# 条件指定による複数件の削除(SqlAgent#delete
) 0.11.0+
メソッド名 | 戻り値の型 |
---|---|
SqlAgent#delete(Class<? extends E>) | SqlEntityDelete<E> |
削除対象のレコードを抽出する条件を指定して削除を行います。
抽出条件の指定方法は 抽出条件の指定 を参照してください。
// first_name = 'Bob' に該当するエンティティの削除
agent.delete(Employee.class).contains("firstName", "Bob").count();
2
# 全ての行を削除(SqlAgent#truncate
) 0.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());
2
3
4
5
6
DAOインタフェースで生成されるSQL
DAOインタフェースを使ってテーブルにアクセスする場合、生成されるSQLは以下のようにスキーマ名で修飾したテーブル名が出力されます。
agent.find(Employee.class, 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
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);
}
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
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 {
// 以下略
}
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;
// 以下略
}
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;
// 以下略
}
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時は対象
// 以下略
}
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;
// 以下略
}
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;
// 以下略
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20