DB を使う#
設定する#
application.conf にデータソースを書きます。
db {
blog_example {
main = true
driver = "org.mariadb.jdbc.Driver"
url = "jdbc:mariadb://127.0.0.1:3306/blog_example?useUnicode=true&characterEncoding=UTF-8"
url = ${?DB_URL}
username = "blog"
username = ${?DB_USER}
password = ""
password = ${?DB_PASSWORD}
maximumPoolSize = 10
connection_pool_type = "hikari"
}
}
codegen {
package = "db"
}
秘密はファイルに書かないでください。 ${?ENV_NAME} の形にしておくと、
環境変数があればそちらが勝ちます。無ければ前の行の値が残ります。
ドライバは jimble-db が MySQL / MariaDB と PostgreSQL の両方を持っているので、
ふつうは足す必要はありません。
DB 製品を選ぶ#
product に製品を書くと、SQL ビルダーがその製品用の SQL を出します。
db {
blog_example {
main = true
product = "postgresql"
driver = "org.postgresql.Driver"
url = "jdbc:postgresql://127.0.0.1:5432/blog_example"
username = "blog"
password = "blog"
password = ${?DB_PASSWORD}
}
}
落とし穴
**${?ENV} の行だけを書かないでください。**環境変数が無いときにキーごと消えます
(上の書き方は「まず既定値、あれば環境変数で上書き」の2行組です)。
パスワードが消えたまま PostgreSQL に繋ぐと、返ってくるのは
The server requested SCRAM-based authentication, but no password was provided. で、
設定の話に見えません。
データベースがまだ無いとき#
create_database_sql を書いておくと、繋がらなかったときに1度だけ流します。
db {
blog_example {
...
create_database_sql = "CREATE DATABASE blog_example"
}
}
書かなければ何もしないので、手で作ってあるなら要りません。 本番では権限を持たない利用者で繋ぐほうが普通なので、開発と CI のためのものです。
| 書ける値 | 製品 |
|---|---|
mysql(既定)/ mariadb |
MySQL 8.x / MariaDB |
postgresql / postgres / pgsql |
PostgreSQL 16 以降 |
省略すると mysql です。知らない値を書くと起動時に落ちます。
アプリのコードは1行も変わりません。
// この1行が、product に応じて
// MySQL SELECT `post`.`id` AS `post__id` ... LIMIT ? OFFSET ?
// PostgreSQL SELECT "post"."id" AS "post__id" ... LIMIT ? OFFSET ?
// になります
SQL.select().from(Post.instance()).where(Post.id.eq(1L));
データソースごとに違う製品を書けます(メインは MySQL、集計用は PostgreSQL、など)。
その製品で書けないもの#
書けないものは、SQL を組み立てたところで DialectException になります。
実行してから製品の構文エラーで落ちる、ということはありません。
| ビルダー | PostgreSQL |
|---|---|
Dsl.match(...)(全文検索) |
書けない。to_tsvector は語彙の分割もスコアも別物なので、黙って置き換えません |
Dsl.dateFormat(col, "%Y-%m-%d") |
書けない。to_char は書式の言語が違うので、置き換えると例外にならずに違う文字列が返ります。Java 側で整えてください |
Dsl.jsonExtract(col, "$.a[0]") |
配列・ワイルドカード・引用符つきキーは書けません。$.a.b の形だけ |
逆に、名前だけ違うものは黙って揃えます(RAND / RANDOM、IFNULL / COALESCE、
TRUNCATE / TRUNC など)。Dsl.concat(...) は PostgreSQL では || になります
(PostgreSQL の concat() は NULL を空文字として飲み込むので、
名前だけ置き換えると MySQL と結果が変わります)。
どうしてもその製品の書き方を使いたいときは Dsl.freeSql(...) で逃げられます。
気をつけること#
- **空間関数の座標の順が違います。**MySQL 8 の SRID 4326 は緯度・経度、 PostGIS は経度・緯度です。jimble は吸収しないので、両方で使うなら自分で合わせてください
- マイグレーションの SQL(
conf/migration/...)は自分で書いたものがそのまま流れます。 両方の製品で動かすなら、001_xxx.mysql.sql/001_xxx.postgresql.sqlと ファイル名の接尾辞で分けます(接尾辞なしはどちらでも当たります)。 詳しくは 製品ごとに SQL を分ける
起動時に繋ぐ#
**設定を書いただけでは繋がりません。**アプリの main で明示的に呼びます。
public static void main (String[] args) {
Migration.install(); // 起動時マイグレーション(要るなら。DBUtil.load より前)
// 繋がらなければ false。ここで止めます
if (!DBUtil.load(Conf.conf().config(), App.class)) {
throw new IllegalStateException("DB を読み込めませんでした(このすぐ上のログに原因が出ています)");
}
JimbleServer.start(new App());
}
DBUtil.load の2つ目はクラスパスの起点です(マイグレーション SQL をここから探します)。
自分のアプリのクラスを渡してください。
落とし穴
戻り値を捨てないでください。DBUtil.load は繋がらなくても例外を投げません
(原因をログに出して false を返します)。捨てるとサーバーは起動してしまい、
最初にリクエストが来たところで落ちます。
DBUtil.getMainDB() が「DB が読み込めていません」と言って落ちるので迷子にはなりませんが、
起動した時点で気づけるほうが早いです。
終わるときは DBUtil.stop() です(Web はグレースフルシャットダウンが呼びます。
バッチやテストでは自分で呼びます)。
データソースが複数あるとき#
db {
main_db { main = true, ... }
log_db { ... }
}
DBUtil.getDB("log_db") で別のデータソースが取れます。
main = true のものは DBUtil.getMainDB() です(引数なしの getDB() はありません)。
トップレベルに並べたものは、1本ごとに独立した扱いを受けます。
conf/migration/<データソース名>/のマイグレーションが当たります- codegen が
db/<データソース名>/を作ります migration/db_lock/db_value/db_cacheもそちらに作られます
設定を書き写さずにもう1本ぶら下げたいだけなら、subs を使います。
db {
main_db {
main = true
...
subs {
archive_db { }
}
}
}
db.newSubDB("archive_db") で取ります。
落とし穴
接続もトランザクションも共有しません。subs は親とは別のプールです。
同じデータベースを指していても別の接続なので、親のトランザクションの中で
newSubDB に書いても一緒には戻りません。片方だけ残ります。
親から設定を引き継ぐ、それ以上のことはしていないと思ってください。
落とし穴
subs にはマイグレーションも codegen も当たりません。
データソースごとの処理は、トップレベルの db { } しか回らないためです。
だから subs に置けるのは「アプリが自分で作る作業用テーブル」までです。
表と型が要るもの(別 DB の監査ログなど)は、トップレベルにもう1本書いてください。
また subs が別のデータベースを指すなら、そのデータベースが先に在ること。
subs は親のデータソースを作る途中で繋ぎにいくので、あとに書いた
トップレベルの create_database_sql は間に合いません
(failed create subs datasource で起動が止まります)。
2本立てと subs を両方書いたものが examples/approval-data にあります。
テーブル定義を生成する#
./gradlew codegen
DB のスキーマから、テーブルごとのクラスを生成します。
Post.id // Column
Post.title
Post.instance() // Table
io.jimble.db プラグインを入れると、migrate → codegen → compileJava が繋がります。
DDL を書いて起動すれば、コードのほうが追いつきます。
plugins {
id("io.jimble.db")
}
生成されたクラスはリポジトリに入れてください。 生成物をコミットしないと、DB が無い環境でビルドできなくなります。
引く#
Data row = db.select(
SQL.select()
.from(Post.instance())
.where(Post.id.eq(1L))
);
// SELECT の結果はテーブル名でネストする(要件 F-D-02)
String title = row.getData("post").getString("title");
// Column で引けば、途中の文字列が出てこない
String same = row.getString(Post.title);
SELECT の結果はテーブル名でネストします。
post と comment を join したとき、両方に id があっても衝突しません。
Column で引けば、文字列のキーがコードに出てきません。
row.extractTableData(Post.instance()) で、1テーブルぶんだけ取り出せます。
エラーの見方#
try (DB db = BlogExample.db()) {
List<Data> rows = db.selectList(SQL.select().from(Post.instance()));
/*
* DB のエラーは例外ではなく戻り値で返る(要件 F-D-11)。
* select 系は null、更新系は -1。
*/
if (rows == null) {
Log.error("引けませんでした: " + db.getError());
return;
}
}
DB のエラーは例外ではなく戻り値で返ります。
select 系は null、insert は -1、update / delete は -1 です。
理由は db.getError() に入っています。
マイグレーション#
conf/migration/<スキーマ名>/001_xxx.sql に置きます。
# --- !Ups
CREATE TABLE `note` (
`id` BIGINT UNSIGNED AUTO_INCREMENT COMMENT 'ID' PRIMARY KEY,
`title` VARCHAR(200) NOT NULL COMMENT 'タイトル',
`created_at` DATETIME NOT NULL COMMENT '作成日時'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='メモ';
# --- !Downs
DROP TABLE `note`;
./gradlew migrate
適用済みのものは記録されるので、二度は走りません。 複数のサーバーが同時に起動しても、ロックを取るので1つしか流れません。
いつ何が走るか、失敗したときにどうなるか、生成される型の対応は マイグレーションとコード生成 にまとめてあります。