Edited at

エンタープライズJavaで使えるORM「uroboroSQL」まとめ

More than 1 year has passed since last update.

筆者自身もコミッターとして関わるJavaのDB永続化ライブラリ「uroboroSQL」の紹介です。

uroboroSQL

https://github.com/future-architect/uroborosql


はじめに

エンタープライズシステム開発では、まだまだJavaで作られていることが多く、システム特性上、やはりRDBが利用されるケースが多いですよね。

Amazon AuroraCloud Spannerといったプロダクトに注目が集まるのも、時代の変化とともにDBも並列分散型でスケールアウトはしたいけれども、トランザクションもSQLも使いたいというCAP定理を覆す特徴を持っていることが要因だと思います。

昨年の12/24にクリスマス・イブにCockroachDBに負荷をかけてみるという記事を投稿したのですが、このCockroachDBもそんな理想を追い求めるプロダクトで、RDBでNoSQLのメリットを教授したいニーズはもはやエンジニアが切望する夢なんですね:sparkles:


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は入ってません:cry:)。


uroboroSQLとは

uroboroSQLは、JavaにおけるDB永続化ライブラリの一つであり、基本的にはJavaからSQLを生成することよりも、SQLに足りないところをJavaで補うアプローチを採用しています。

もちろん、1レコードのINSERT/UPDATE/DELETEでSQLをいちいち書くのも辛いので、ORMとしてのAPIも提供しています。


特徴的な機能


開発時に便利なREPL機能搭載

2Way-SQLでの開発時にビルド不要で即試すことが可能です。

asciicast


カバレッジレポート

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のコードサンプル

さて、ライブラリを理解するには、利用時にどんな実装になるのか見るのが手っ取り早いですよね。

というわけで、よく利用する実装をサンプルとして、まとめました。

ちなみに、執筆時点では、公式ドキュメントよりも豊富かも:sweat_smile:


接続

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


department/select_department.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*/



department/insert_department.sql

INSERT /* _SQL_ID_ */

INTO
DEPARTMENT
(
DEPT_NO
, DEPT_NAME
) VALUES (
/*dept_no*/1
, /*dept_name*/'sample'
)


department/update_department.sql

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


department/delete_department.sql

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ドキュメント、ツール、サンプル