JVM系言語のSQLマッパーについてPostgreSQLの型への対応状況を比較検証
KotlinでWebアプリケーションを作るにあたり、SQLを直接記述できるタイプのO/Rマッパー(本稿ではSQLマッパーと呼びます)を探し求めました。
SQLマッパーに求める機能
SQLマッパーに求める機能はBindとMapです。この記事ではBindとMapを次のように定義します。
- Bind
- Map
次のような機能は(あれば嬉しいかもしれませんが)特に求めていません。
最初はなんとなくsql2oを使っていましたが、プロパティの型によっては思い通りにBind/Mapされないことがあり、各種のSQLマッパーがどのような型に対応しているのか真面目に検証してみようと思いました。
検証対象とバージョン
上記の求める機能を持つSQLマッパーとして、次の5つのライブラリを検証しました。
- Apache Commons DbUtils: 1.7
- Spring JDBC Template: 5.1.9.RELEASE
- MyBatis: 3.5.2
- JDBI (with jdbi3-postgres): 3.10.1
- sql2o (with sql2o-postgres): 1.6.0
対象とするデータベースはPostgreSQLです。
- PostgreSQL: 11.5
- PostgreSQL JDBC Driver: 42.2.8
基本的な情報
基本的な情報は次の通りです。最新については2019-09-29時点の情報です。
| 名前 | Apache Commons DbUtils | Spring JDBC Template | MyBatis | JDBI | sql2o |
|---|---|---|---|---|---|
| リンク | ドキュメント リポジトリ |
ドキュメント リポジトリ |
ドキュメント リポジトリ |
ドキュメント リポジトリ |
ドキュメント リポジトリ |
| 開発者 | Apache Software Foundation | Pivotal | MyBatisチーム | JDBI Project Members | Lars Aaberg氏 |
| ライセンス | Apache License 2.0 | Apache License 2.0 | Apache License 2.0 | Apache License 2.0 | MIT License |
| 最新バージョン | 1.7 | 5.1.10 | 3.5.2 | 3.10.1 | 1.6.0 |
| 最新版公開日 | 2017-07-02 | 2019-09-28 | 2019-07-15 | 2019-09-07 | 2018-10-20 |
| 最終コミット日 | 2019-08-16 | 2019-09-27 | 2019-09-28 | 2019-09-29 | 2019-05-16 |
| SQLを書く場所 | 引数 | 引数 | 別ファイル or アノテーション | 引数 or アノテーション | 引数 |
検証方法
検証対象とする型のカラムを含むテーブルsql_mapper_testを作成し、そこにテストデータを1行INSERTしておきます。
create table sql_mapper_test ( -- simple c_boolean boolean, c_integer integer, c_decimal decimal, c_double double precision, c_varchar varchar(100), c_text text, c_bytea bytea, c_uuid uuid, -- datetime c_date date, c_time time, c_timetz timetz, c_timestamp timestamp, c_timestamptz timestamptz, -- complex c_inet_ipv4 inet, c_inet_ipv6 inet, c_url text, c_integer_array integer[], c_varchar_array varchar(10)[] );
BindとMapについて、それぞれ次のように対応可否を検証します。
- Bind
SELECT COUNT(*) FROM sql_mapper_test WHERE カラム名 = :valueのようなSQLでテストデータと同じ値をBindして、1が返ってくれば対応していると判断。
- Map
SELECT カラム名 FROM sql_mapper_testというSQLで、テストデータと同じ値が返ってくれば対応していると判断。
https://github.com/orangain/compare-sql-mappers
検証結果
結果は次のスプレッドシートに記載しています。 それぞれのセルは、1列目のカラムを2列目の型でBind/Mapした結果を表します。
- Success(緑): 正しくBind/Mapできた
- Wrong Value(黄): 期待とは異なる値がBind/Mapされた(配列が文字列としてBindされてしまう、期待とは異なる型にMapされてしまう、型は同じだけど値が異なるなど)
- 〜Exception(赤): 発生した例外の名前
https://docs.google.com/spreadsheets/d/1Tf3uB5xFIZnP1ieSu9JQdKaGDY_XfJyGJYleAXI3328/edit?usp=sharing
解説
まず最初に、timetz⇔OffsetTime、url⇔URLの変換は試した全ライブラリでできませんでした。
Commons DbUtilsとSpring JDBC Templateは同じような結果となりました。
基本的にはJDBCドライバー任せで、Bind時はJDBCドライバーが対応していればBindできます。
Map時はJDBCドライバーが返す値をそのままキャストして返すので、期待している型とは異なる型が返ってくる場合があります。
例えば、日時に関する型はjava.sql.Dateなどで返ってくるので、LocalDateなどのDate and Time APIの型で受け取ることはできません。
また、integer型のカラムをBigDecimalで受け取りたいと思っても、返ってくるのはIntegerです。
Spring JDBC Templateの追加の特徴として、List<T>をBindした時に、個数分のカンマ区切りのプレースホルダーに展開してくれるので、 IN (:list) のような記述が可能です。
MyBatisはMap時に期待する型に変換してくれるので、インターフェイスの戻り値の型に応じてDate and Time APIの型で受け取ることもできます。
PostgreSQLの配列型に関しても、 Integer[] や String[] などの配列型であれば受け取れました。
ただし、Integer[]をパラメーターとしてBindすることはできなかったので、注意が必要です。
MyBatisの場合、(本稿の狙いとはズレますが)動的なSQL組み立て機能が強みなので、<foreach>を使えばINなどのパラメーターを展開できます。
JDBIはPostgresプラグインを有効にするとほぼ全ての型をBind/Mapでき、一番良い結果となりました。
他の全ライブラリが対応しているjava.sql.Dateとjava.sql.TimeをMapできないのは意外でしたが、LocalDateやLocalTimeを使えば問題ありません。
普通のプレースホルダーは :name のような形式ですが、<ids>という形式のプレースホルダーを使ってbindList()でList<T>をBindすると、個数分のカンマ区切りのプレースホルダーに展開してくれるので、IN (<list>) のような記述が可能です。
Sql2oはMap時に期待する型に変換してくれますが、Date and Time APIには対応していません。
細かいですが、integerをBigDecimalで受け取る際は、一度doubleに変換されて、桁数がずれてしまいました。
List<T>や配列をBindした時に、個数分のカンマ区切りのプレースホルダーに展開してくれるので、 IN (:list) のような記述が可能です。
大まかにまとめると次のようになります。
| Apache Commons DbUtils | Spring JDBC Template | MyBatis | JDBI | sql2o | |
|---|---|---|---|---|---|
| 基本的な型への対応 | ○ | ○ | ○ | ○ | ○ |
| Date and Time APIへの対応 | △Mapのみ | △Mapのみ | ○ | ○ | △Mapのみ |
| ArrayやListへの対応 | × | × | △数値の配列はダメ | ○ | × |
| Bind時のコレクションの展開 | × | △配列はダメ | △ <foreach> で対応 |
△配列はダメ*3 | ○ |
結論
比較したSQLマッパーのPostgreSQLの型への対応度合いは、次の順で良い結果となりました。
JDBIは、ドキュメントに対応している型が明確に記述されていたのも好感が持てました。試しに使ってみようと思います。
なお、各種ライブラリの検証結果で対応していない型についても、Bind時にSQLの書き方を工夫したり、コンバーターを自作したりすれば対応できるので、ライブラリの評価はこれだけで決まるものではありません。 むしろ、対応している型を把握した上でライブラリを使うことが大切だと思うので、その助けになれば幸いです。 また、この結果はJDBCドライバーの実装に大きく依存するので、他のDBやJDBCドライバーを使う場合は、この結果をそのまま使えないことに注意してください。
なるべく注意を払いましたが、すべてのライブラリにおいてドキュメントやソースコードを完全に読んだわけではないので、検証方法がおかしいところなどありましたら、ぜひお知らせください。