Edited at

Clojure Luminus のデータベース・ライブラリ

More than 3 years have passed since last update.

ClojureのLuminusのドキュメントのデータベースの節( http://www.luminusweb.net/docs/database.md )の勝手な和訳です。2016年02月24日時点の内容に基いています。

勝手に和訳するのは駄目でしょうか。。 MITライセンスなので大丈夫。(コメントでayato_pさんに教えてもらいました。ありがとうございます。)

きっかけは、久しぶりにlein new luminusしてみたら、DBのライブラリがYesqlからHugSQLというのに替わっていてドキュメントを読んだからです。

末尾の訳注に先に目を通したほうが分かりやすいかもしれないです(あるいは、余計に分かりにくいかもしれないです)。



Database Access


Contents


  1. Configuring the Database

  2. Configuring Migrations

  3. Setting up the database connection

  4. Working with HugSQL

  5. SQL Korma


Configuring the Database

Luminusはデフォルトでは、データベースのマイグレーションにはMigratusを、データベースの操作にはHugSQLを、それぞれ使います。例えば、+postgresのようなデータベースprofileを使えば、マイグレーションとデフォルトのDBコネクションは自動でセットアップされるでしょう。


Configuring Migrations

まず初めに、自分のデータベースの接続設定をprofiles.cljに書かなければいけません。デフォルトでは、developmenttestingという2つのprofileがそれぞれに生成されます。

{:profiles/dev  {:env {:database-url "jdbc:postgresql://localhost/my_app_dev?user=db_user&password=db_password"}}

:profiles/test {:env {:database-url "jdbc:postgresql://localhost/myapp_test?user=db_user&password=db_password"}}}

そうしたら、データベース・スキーマをマイグレーションやロールバックするためのSQLスクリプトを書きます。このSQLスクリプトは、idを数値として捉えてその順に適用されます。そして、resources/migrationsフォルダ配下に置かれることが期待されます。Luminusテンプレートは、userというテーブルに対するサンプルのマイグレーションファイルを生成してくれます。

resources/migrations/20150720004935-add-users-table.down.sql

resources/migrations/20150720004935-add-users-table.up.sql

以上の準備を経ることで、以下のようにマイグレーションが実行可能になります。

lein run migrate

適用されたマイグレーションは次のようにしてロールバックできます。

lein run migrate

migratusプラグインを使って、次のようにさらに追加のマイグレーションファイルを生成することができます。

lein migratus create add-guestbook-table

詳細はデータベース・マイグレーションを参照してください。


Setting up the database connection

DBのコネクションの設定は、アプリケーションの<app>.db.coreネームスペースの中にあります。デフォルトでは、DBのコネクションはDATABASE_URL環境変数として渡されることが期待されます。

コネクション・プールを行うために、conmanライブラリが使われてます。

DBのコネクションは、conman/connect!関数がDB設定のmapとコネクションを格納するatomを渡して呼び出されることによって初期化されます。connect!関数は、HikariCPライブラリを使ってプールされたJDBCコネクションを作成します。disconnect!関数が呼び出されると、作成されたコネクションは閉じられます。

(ns myapp.db.core

(:require
...
[config.core :refer [env]]
[conman.core :as conman]
[mount.core :refer [defstate]]))

(def pool-spec
{:adapter :postgresql
:init-size 1
:min-idle 1
:max-idle 4
:max-active 32})

(defn connect! []
(conman/connect!
(assoc
pool-spec
:jdbc-url (env :database-url))))

(defn disconnect! [conn]
(conman/disconnect! conn))

(defstate ^:dynamic *db*
:start (connect!)
:stop (disconnect! *db*))

コネクションはdefstateマクロを使って規定されます。*db*componentが:startの状態に変わるときにconnect!関数が呼び出され、*db*componentが:stopの状態に変わるときにdisconnect!関数が呼び出されます。

このコネクションは、conman.core/with-transactionのスコープの中でクエリ関数が呼び出されるときにトランザクションが使用可能なコネクションに自動的に切り替えたいので動的(dynamic)である必要があります。

*db*componentのライフサイクルは、mountライブラリによって管理されます。これについてはcomponentのライフサイクル管理の節で議論されています。

<app>.handler/init<app>.handler/destroy関数は、(mount/start)(mount/stop)を呼び出すことによってdefstateを用いて定義されたあらゆるcomponentの初期化と停止をそれぞれ行います。この仕組みによって、DBコネクションはサーバが起動するときに利用可能になりサーバが停止するときに綺麗に始末されます。

複数のデータベースを相手にする場合、データベースのコネクションを各々追跡する必要があるので、それぞれに別々のatomが必要となります。


Working with HugSQL

HugSQLはクエリに定義するのに素のSQLを使用します。各クエリに対して関数名と doc string を割り当てるためにSQLのコメントを利用します。

規約として、クエリは全てresources/sql/queries.sqlファイルに書きます。しかしながら、アプリケーションが育って大きくなってくれば当然クエリを複数のファイルに分割したくなるでしょう。

クエリファイルのフォーマットは、以下を見るとよく分かるでしょう。

-- :name create-user! :! :n

-- :doc creates a new user record
INSERT INTO users
(id, first_name, last_name, email, pass)
VALUES (:id, :first_name, :last_name, :email, :pass)

生成された関数の名前は:nameコメントによって規定されます。この関数名の後には、「コマンド」と「(問い合わせ)結果」という2つのフラグが続きます。

次のコマンドフラグが利用可能です。



  • :? - result-setをともなうクエリ(デフォルト値)


  • :! - いかなるステートメントにも対応


  • :<! - INSERT ... RETURNINGをサポート


  • :i! - insertとjdbcの.getGeneratedKeysをサポート

(問い合わせ)結果フラグです



  • :1 - 単一行のデータをhash-mapとして返す


  • :* - 複数行のデータをhash-mapのvectorとして返す


  • :n - 影響を与えた(つまり 挿入or更新or削除 された)行数を返す


  • :raw - 結果を加工せずにそのまま返します(デフォルト値)

クエリ自体は、コロンを接頭辞として付加したパラメータ名によって印された動的なパラメータと素のSQLを用いて書きます。

クエリ関数は、conman/bind-connectionマクロを呼び出すことによって生成します。このconman/bind-connectionマクロは、上述されたようなクエリのファイルを1つ以上と、コネクションのvarを受け取ります。

(conman/bind-connection conn "sql/queries.sql")

bind-connectionが実行されると、さっき定義したクエリはmyapp.db.core/create-user!関数にマッピングされます。bind-connectionによって生成された関数は、デフォルトではconnatom内のコネクションを使用するようになります。ただし、関数の実行時に明示的に別のコネクションを渡せばこの限りではありません。

(create-user!

{:id "user1"
:first_name "Bob"
:last_name "Bobberton"
:email "bob.bobberton@mail.com"
:pass "verysecret"})

生成されたこの関数はパラメータなしでも実行できます。

(get-users)

この関数に明示的にコネクションを渡すこともできます。トランザクションの中での実行のようなケースに対応するためです。

; デフォルトとは異なるコネクションを定義

(def some-other-conn
"jdbc:postgresql://localhost/myapp_test?user=test&password=test")

(create-user!
{:id "user1"
:first_name "Bob"
:last_name "Bobberton"
:email "bob.bobberton@mail.com"
:pass "verysecret"}
some-other-conn) ; <- 明示的にデフォルトとはコネクションを渡している

conmanライブラリは、トランザクションを使ってステートメントを実行するためのwith-transactionマクロも提供しています。このwith-transactionマクロはそのボディの内部においては、当該コネクションを「トランザクション付きのコネクション」に再束縛します。bind-connectionの実行によって生成されたSQLクエリ関数は全て、デフォルトではwith-transactionマクロ内では「トランザクション付きのコネクション」を使用します。

(with-transaction [t-conn conn]

(jdbc/db-set-rollback-only! t-conn)
(create-user!
{:id "foo"
:first_name "Sam"
:last_name "Smith"
:email "sam.smith@example.com"})
(get-user {:id "foo"}))

詳細は公式ドキュメントを参照してください。


SQL Korma


KormaはClojureのドメイン固有言語です。あなたの好きなRDBMSでの作業で発生する苦痛を取り除いてくれます。柔軟性に重点を置いて設計され、速度に重点を置いて構築されました。Kormaは、後味の悪いデータに対してシンプルで直感的なインターフェイスを提供します。

Korma is a domain specific language for Clojure that takes the pain out of working with your favorite RDBMS. Built for speed and designed for flexibility, Korma provides a simple and intuitive interface to your data that won't leave a bad taste in your mouth.


http://sqlkorma.com/

既存のプロジェクトにKormaのサポートを追加するのはけっこう簡単です。まず必要なのはproject.cljにKormaの依存を書き足すことです。

[korma "0.4.0"]

次に、Kormaを使い出すためにkorma.dbへの参照を加えなければいけません。

(ns myapp.db.core

(:require
[korma.core :refer :all]
[korma.db :refer [create-db default-connection]]))

Kormaは、create-dbを使ってコネクションを生成することを必要とします。そしてデフォルトのコネクションはdefault-connection関数によってセットすることができます。

(connect! [] (create-db db-spec))

(defstate ^:dynamic *db*
:start (connect!))

(default-connection *db*)

この関数は、c3p0ライブラリを使って、あなたのDB設定に対するコネクション・プールを作成します。注意点として、全てのクエリのデフォルトとして最後に作成されたコネクション・プールがセットされます。

Kormaは、SQLのテーブルを表現するのにエンティティを用います。エンティティは、クエリの構築における中核要素にあたります。エンティティは、defentityマクロを使うことによって作成されます。

(defentity users)

usersエンティティを使って、さっき登場したクエリをuserを作成するためのクエリに書き直すことができます。以下のようになります。

(defn create-user [user]

(insert users
(values user)))

userを取得するクエリはこんなふうに書き直せるでしょう。

(defn get-user [id]

(first (select users
(where {:id id})
(limit 1))))

Kormaとその機能に関するより詳しいドキュメントなら、公式ドキュメントのページを参照してください。



訳注



  • profileという単語が2通り使われている。


    1. Luminusのテンプレート作成の際に指定する追加のコンポーネントの指定。

      例えばlein new luminus guestbook +h2ってやるときの+h2がprofile。

    2. 「development」とか「testing」とか「production」とかの環境で分ける設定や情報のこと。

      この意味ではprofiles.cljとかがそう。



  • 文中でcomponentって出てくる場合は、単に「構成要素」を意味してるんじゃなくて、componentライブラリのことを指しているのがほとんどじゃないかと思う。componentライブラリについてそもそもよく知らないので、なるべく「component」ってそのまま残している。