LoginSignup
18
9

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-12-23

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

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

18
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
9