2024年3月24日日曜日

ragtimeを利用してmigration用コマンドを実装


背景

ragtimeとはclojureでdb(データベース)のmigrationなどを管理するライブラリの1つです。
htmlを略式で書けるhiccupなどを開発されたweavejesterさんが開発しています。
dbにおけるmigrationとは、データ構造の構築や巻き戻しの管理を意味します。

ragtimeは公式の説明が充実しているため実装内容の概要把握はできたものの、書き方を間違えた際に発生するエラーを見ても原因が分かりづらかったり、注意していないと呼び出し方を間違える記述がいくつかあったので、備忘録を兼ねて実装の全体像と注意点を記事に残します。

実装対象

dbを使える状態のcljプロジェクト
以前から取り組んでいるログ取りサーバーのプロジェクトを使いました。
https://github.com/asukiaaa/clj-server-practice

ragtimeを依存対象として設定

deps.ednのdepsにragtimeを記述して呼び出し可能にします。
clojarsや公式の説明は0.8系ですが、githubのreadmeには記事を書いている時点では0.9.4が紹介されているので、新しい方が不具合が取れて良いと思うので0.9.4を設定しました。
この記事の内容は0.8系でも0.9系でも動きはしました。

deps.edn
dev.weavejester/ragtime {:mvn/version "0.9.4"}

migrationファイルを準備

ragtimeのmigrationファイルはednにupとdownのsqlを書く方式と、up.sqlとdown.sqlの2つを作る方式で記述できます。
自分はupとdownを1つのファイルにまとめて、ファイルの数を少なくしつつ処理を比較するのが好きなので、edn方式を使いました。

下記のmigrationを作成しました。
今回はファイル名の始まりを作成日時にしています。
順序を整えるため、日時や番号をファイルの先頭に記載するのが良いです。

resource/migrations/202303231026-create-user-table.edn
{:up
["CREATE TABLE user(
id SERIAL NOT NULL PRIMARY KEY UNIQUE,
name CHAR(255) NOT NULL,
email CHAR(255) NOT NULL UNIQUE,
hash CHAR(255),
salt CHAR(255),
password_reset JSON,
auth JSON,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP);"]
:down
["DROP TABLE user;"]}

upとdownは「文字列」ではなく「文字列の配列」なことに注意です。
間違えて配列で包んでいない文字列を渡して下記のエラーが発生して時間を取られました。
clj -M:run db migrate
Applying 202303231026-create-user-table
Execution error (ClassCastException) at clojure.java.jdbc/prepare-statement (jdbc.clj:645).
class java.lang.Character cannot be cast to class java.lang.String (java.lang.Character and java.lang.String are in module java.base of loader 'bootstrap')

Full report at:
/tmp/clojure-7877890294147323206.edn

ragtimeに渡す値を準備

ragtime実行時に渡すdbの接続情報とmigrationファイルの一覧を準備します。

src/asuki/back/config.clj
(ns asuki.back.config
(:require [ragtime.jdbc :as rjdbc]))

(def db-spec (or (System/getenv "DATABASE_URL")
(System/getenv "JAWSDB_MARIA_URL")
{:dbtype "mysql"
:host "mariadb"
:port 3306
:dbname "server_practice"
:user "maria-user"
:password "maria-pass"}))

(def ragtime
{:datastore (rjdbc/sql-database db-spec)
:migrations (rjdbc/load-resources "migrations")})

注意点として、ここで呼んでいるjdbcはragtime.jdbcであり、clojure.java.jdbcでありません。間違ってclojure.java.jdbcを使うとsql-databaseやload-resources関数は無いという下記のような不具合で処理が止まります。
Syntax error compiling at (asuki/back/config.clj:22:15).
No such var: jdbc/sql-database

ragtimeの説明にはdbのurlを渡す場合はconnection-uriで渡すように説明がありますが、それを行うと自分の環境では下記のエラーが発生して処理できませんでした。
clj -M:run db migrate
Execution error (SQLException) at java.sql.DriverManager/getConnection (DriverManager.java:708).
No suitable driver found for mysql://mariadb:3306/server_practice?user=maria-user&password=maria-pass

Full report at:
/tmp/clojure-5694223121477406490.edn

そのため、db-specと同等にdbの設定がurl(uri?)の場合はmapに内包せずそのまま渡しています。
; OK
(def ragtime
{:datastore (rjdbc/sql-database db-spec)
:migrations (rjdbc/load-resources "migrations")})
; bad
(def ragtime
{:datastore (rjdbc/sql-database (if (string? db-spec)
{:connection-uri db-spec}
db-spec))
:migrations (rjdbc/load-resources "migrations")})

ragtime呼び出し用のコマンド作成

replを起動してragtime.replを呼び出すのは、開発中は良いのですが本番用にjarにまとめるとできなくなるので、main関数に文字列を渡して機能を切り替え可能にしました。

deps.edn
{:aliases
{:run {:main-opts ["-m" "asuki.back.core"]}
:uberjar {:replace-deps {com.github.seancorfield/depstar {:mvn/version "2.0.216"}}
:exec-fn hf.depstar/uberjar
:exec-args {:aot true
:jar "back.jar"
:main-class "asuki.back.core"
:sync-pom true}}}}

src/asuki/back/core.clj
(ns asuki.back.core
(:gen-class)
(:require [ragtime.repl :as ragr]
[asuki.back.config :as config]))

(defn -main [& args]
(condp = (first args)
"server-with-migration"
(fn []
(ragr/migrate config/ragtime)
(start-server config/port))
"server" (start-server config/port)
"db" (condp = (second args)
"migrate" (ragr/migrate config/ragtime)
"rollback" (ragr/rollback config/ragtime))))

これにより、serverの実行やmigrationを実行時の文字列で切り替え可能です。
clj -M:run db migrate
clj -M:run server

上記の設定はmain関数をuberjarで固めるjarファイルの起点にしているので、jarファイルで実行時も同様に切り替え可能です。
java -jar back.jar server-with-migration

おわり

配列にすべきところを文字列にしたり、ragtime.jdbcとすべきところをclojure.java.jdbcとしたりしてエラーが発生して時間を取られましたが、ragtimeを利用したmigrationの仕組みを期待通りに作れました。

参考

公式の始め方解説です。
Getting Started - ragtime

今回の記事で実施した変更です。
https://github.com/asukiaaa/clj-server-practice/commit/19127b7ebcecee90d8cc9f7a5cc23f60d663394d

渡す文字列によって挙動を変えるコマンドの実装の参考にしたファイルです。
https://github.com/paulbutcher/clj-migratus/blob/main/src/clj_migratus.clj

0 件のコメント :