筆者自身もコミッターとして関わるJavaのDB永続化ライブラリ「uroboroSQL」の紹介です。
はじめに
エンタープライズシステム開発では、まだまだJavaで作られていることが多く、システム特性上、やはりRDBが利用されるケースが多いですよね。
Amazon AuroraやCloud Spannerといったプロダクトに注目が集まるのも、時代の変化とともにDBも並列分散型でスケールアウトはしたいけれども、トランザクションもSQLも使いたいというCAP定理を覆す特徴を持っていることが要因だと思います。
昨年の12/24にクリスマス・イブにCockroachDBに負荷をかけてみるという記事を投稿したのですが、このCockroachDBもそんな理想を追い求めるプロダクトで、RDBでNoSQLのメリットを教授したいニーズはもはやエンジニアが切望する夢なんですね
JavaとRDBの歴史
2000年代前半にJavaで作られたシステムは、JDBCのAPIをそのまま利用することも多かったのですが、その後、Hibernate、iBatis(現在のMyBatis)、SeaserプロジェクトのS2Daoなどをはじめとして、ORマッパー(ORM)が開発され、利用されるようになりました。
その後、2006年にJPA(Java Persistence API)の1.0がJava標準の永続化フレームワークとして策定され、2009年にJPA2.0、2013年にJPA2.1と、JavaSEでも利用はできるのですが、JavaEEのEJBと共に進化を続けているという状況ですね。
なお、最新のライブラリ比較については、2017年度 Java 永続化フレームワークについての考察(1)の記事が非常に参考になります(残念ながらこれから紹介するuroboroSQLは入ってません)。
uroboroSQLとは
uroboroSQLは、JavaにおけるDB永続化ライブラリの一つであり、基本的にはJavaからSQLを生成することよりも、SQLに足りないところをJavaで補うアプローチを採用しています。
もちろん、1レコードのINSERT/UPDATE/DELETEでSQLをいちいち書くのも辛いので、ORMとしてのAPIも提供しています。
特徴的な機能
開発時に便利なREPL機能搭載
2Way-SQLでの開発時にビルド不要で即試すことが可能です。
カバレッジレポート
SQL文の条件分岐を集計してカバレッジレポートを行うことが可能です。
その他の特徴
項目 | uroboroSQLの対応 |
---|---|
ライセンス | MIT |
体制 | OSS |
latest | v0.5 (2017/12) |
SQL外部化 | ○ |
DSL | × |
Java | 8<= |
Stream Lamdba対応 | ○ |
エンティティ自動生成 | ○ |
区分値対応 | ○(列挙体、定数クラスいずれも可) |
ストアドプロシージャ呼出 | ○ |
ResultSetのカスタマイズ | ○ |
Oracle | ○ |
DB2 | - |
MySQL | ○ |
PostgreSQL | ○ |
MariaDB | - |
MS-SQL | ○ |
H2 | ○ |
Derby | ○ |
Sybase | - |
SQLite | ○ |
依存 | commons-lang3,slf4,ognl,jline |
※2017/12/23時点最新バージョンとなるv0.5.0時点
uroboroSQLのコードサンプル
さて、ライブラリを理解するには、利用時にどんな実装になるのか見るのが手っ取り早いですよね。
というわけで、よく利用する実装をサンプルとして、まとめました。
ちなみに、執筆時点では、公式ドキュメントよりも豊富かも
接続
SqlConfig config = UroboroSQL.builder("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", "sa", "sa").build();
トランザクション
try (SqlAgent agent = config.agent()) {
agent.required(() -> {
// insert/update/delete
});
}
2way-SQL
SELECT /* _SQL_ID_ */
DEPT.DEPT_NO AS DEPT_NO
, DEPT.DEPT_NAME AS DEPT_NAME
FROM
DEPARTMENT DEPT
WHERE
1 = 1
/*IF SF.isNotEmpty(dept_no)*/
AND DEPT.DEPT_NO = /*dept_no*/1
/*END*/
/*IF SF.isNotEmpty(dept_name)*/
AND DEPT.DEPT_NAME LIKE '%' || /*dept_name*/'' || '%'
/*END*/
INSERT /* _SQL_ID_ */
INTO
DEPARTMENT
(
DEPT_NO
, DEPT_NAME
) VALUES (
/*dept_no*/1
, /*dept_name*/'sample'
)
UPDATE /* _SQL_ID_ */
DEPARTMENT DEPT
SET
DEPT.DEPT_NAME = /*dept_name*/'sample'
DEPT.LOCK_VERSION = DEPT.LOCK_VERSION + 1
WHERE
DEPT.DEPT_NO = /*dept_no*/1
AND DEPT.LOCK_VERSION = /*lock_version*/0
DELETE /* _SQL_ID_ */
FROM
DEPARTMENT DEPT
WHERE
DEPT.DEPT_NO = /*dept_no*/1
S2Dao等と同じ文法で、SQL内でコメント標記で分岐を記述することができます。
SELECT(リスト取得)
try (SqlAgent agent = config.agent()) {
List<Map<String, Object>> deptList =
agent.query("department/select_department")
.param("dept_name", "retail")
.collect();
}
SELECT(Stream取得、Map型)
try (SqlAgent agent = config.agent()) {
Stream<Map<String, Object>> depts =
agent.query("department/select_department")
.param("dept_name", "retail")
.stream();
}
SELECT(Stream取得、モデル型)
try (SqlAgent agent = config.agent()) {
Stream<Department> depts =
agent.query("department/select_department")
.param("dept_name", "retail")
.stream(Department.class);
}
SELECT(1件取得、Map型、取得できない場合例外)
try (SqlAgent agent = config.agent()) {
Map<String, Object> dept =
agent.query("department/select_department")
.param("dept_no", 1001)
.first();
}
SELECT(1件取得、モデル型、取得できない場合例外)
try (SqlAgent agent = config.agent()) {
Department dept =
agent.query("department/select_department")
.param("dept_no", 1001)
.first(Department.class);
}
SELECT(1件取得、Map型、Optional)
try (SqlAgent agent = config.agent()) {
Map<String, Object> dept =
agent.query("department/select_department")
.param("dept_no", 1001)
.findFirst()
.orElse(null);
}
SELECT(1件取得、モデル型、Optional)
try (SqlAgent agent = config.agent()) {
Department dept =
agent.query("department/select_department")
.param("dept_no", 1001)
.findFirst(Department.class)
.orElse(null);
}
INSERT/UPDATE/DELETE
try (SqlAgent agent = config.agent()) {
agent.required(() -> {
// insert
agent.update("department/insert_department")
.param("dept_no", 1001)
.param("dept_name", "sales")
.count();
// update
agent.update("department/update_department")
.param("dept_no", 1001)
.param("dept_name", "HR")
.count();
// delete
agent.update("department/delete_department")
.param("dept_no", 1001)
.count();
});
}
INSERT/UPDATE/DELETE(バッチ実行)
List<Map<String, Object>> inputList = new ArrayList<>();
// 中略
try (SqlAgent agent = config.agent()) {
agent.required(() -> {
agent.batch("department/insert_department")
.paramStream(inputList.stream())
.count();
});
}
DAOインタフェース
下記のようなモデルクラスがある前提とします。
@Table(name = "DEPARTMENT")
public class Department {
private int deptNo;
private String deptName;
@Version
private int lockVersion = 0;
// 中略 getter/setter
}
@Versionが付与されたフィールドは楽観ロック用のバージョン情報としてuroboroSQLが認識し、UPDATE時にはSET句で+1され、WHERE句の検索条件に追加されてSQLを発行し、更新件数が0の場合に
OptimisticLockException
を発生させます。
SELECT(主キー検索)
try (SqlAgent agent = config.agent()) {
Department dept =
agent.find(Department.class, 1001).orElse(null);
}
v0.5.0時点では、DAOインタフェースで利用できるのは単一テーブル主キー検索のみですが、直近のバージョンアップで単一テーブルでWHERE句相当の検索条件を指定できるようにする予定です。
INSERT
try (SqlAgent agent = config.agent()) {
Department hrDept = new Department();
hrDept.setDeptNo(1002);
hrDept.setDeptName("HR");
agent.insert(hrDept);
}
UPDATE
try (SqlAgent agent = config.agent()) {
agent.required(() -> {
Department dept =
agent.find(Department.class, 1001).orElseThrow(Exception::new);
dept.setDeptName("Human Resources");
agent.update(dept);
});
}
DELETE
try (SqlAgent agent = config.agent()) {
agent.required(() -> {
Department dept =
agent.find(Department.class, 1001).orElseThrow(Exception::new);
agent.delete(dept);
});
}
参考:uroboroSQLドキュメント、ツール、サンプル
- uroboroSQL日本語ドキュメント
- uroboroSQLの紹介 (OSC2017 Nagoya) #oscnagoya
- uroboroSQL ソースジェネレータ
- uroboroSQL サンプルCLIアプリケーション
- uroboroSQL サンプルWebアプリケーション(with Spring Boot)