ClojureでRDBとSQLをごにょごにょするライブラリを探すとkormaの記事が多いです。しかし、java生まれseasar育ちとしてはORMよりもsqlテンプレート方式のほうが使いやすいです。SQL好きだし。ClojureでSQLテンプレートならyesqlです。yesqlは、Luminusの2でデフォルトのpersistence層ライブラリになりました。結構前ですが。
というわけで、yesqlの日本語の記事はあまり見かけないので書いてみます。
yesqlのstableは4系ですが、最新の5系になって結構機能追加されているので、今回は5系を使います。githubのREADMEだと0.5.0-beta2
てなってますが、嘘です。書いている時点での最新はclojarsにも上がっている0.5.0-rc3
です。
leiningen
入ってる前提です。無かったらこんな感じで入れておいてください。
mkdir -p ~/bin cd ~/bin wget https://raw.githubusercontent.com/technomancy/leiningen/stable/bin/lein chmod a+x ./lein
~/bin
にはパスを通しておいてください。
yesql
素のプロジェクトからスタートします。
mkdir -p ~/work cd ~/work lein new hi-yesql cd ./hi-ysql tree
yesql
とMySQLのコネクタの依存を追加します。open ./project.clj
とかやってファイルを開いてください。中身を以下のように書き換えます。
(defproject hi-yesql "0.1.0-SNAPSHOT" :dependencies [[org.clojure/clojure "1.7.0"] [mysql/mysql-connector-java "5.1.36"] [yesql "0.5.0-rc3"]])
邪魔くさいので:description
、:url
、:licence
は消しました。
ついでにmainネームスペースを指定しときます。さらに以下のように書き換えます。
(defproject hi-yesql "0.1.0-SNAPSHOT" :main hi-yesql.core :dependencies [[org.clojure/clojure "1.7.0"] [mysql/mysql-connector-java "5.1.36"] [yesql "0.5.0-rc3"]])
main指定したネームスペースにmainメソッドを書きます。
open ./src/hi_yesql/core.clj
。このネームスペースはleiningenが勝手に作って勝手に適当なコードを書いといてくれてます。全部消して以下のように書きます。
(ns hi-yesql.core (:require [yesql.core :as yesql])) (yesql/defquery select "sql/select.sql") (defn -main "Javaで言う public static void main(String[] args)" [& args] (println "-- start.") (println "-- finish."))
defquery
はマクロです。defquery
は、第一引数の名前の関数(DAOメソッドみたいなの)を生成します。defquery
は、第二引数をSQLテンプレートファイルのパスとして認識します。
コマンドラインからlein run
で実行してみます。
Exception in thread "main" java.io.FileNotFoundException: sql/select.sql, compiling:( .....
怒られます。
第二引数のパス(クラスパス上から見てsql/select.sql
の場所)にSQLテンプレートのファイルがないので怒られました。
SQLテンプレートファイルを作ります。コマンドラインから以下のような感じで。
mkdir -p ./resources/sql touch ./resources/sql/select.sql open ./resources/sql/select.sql
中身はこんなです。
-- return arguments simply via dual table. SELECT :param1 AS ret1, :param2 AS ret2 FROM DUAL;
clojureのキーワードみたいになってるparam1
とparam2
が動的に埋め込まれるバインド変数です。
再度lein run
してみます。エラーは起きませんが、特に何も起きません(ログは出力されますが)。select
というSQLを発行する関数を生成しただけだからです。
生成される関数を利用するコードを追記します。
(ns hi-yesql.core (:require [yesql.core :as yes])) (def db-spec {:classname "com.mysql.jdbc.Driver" :subprotocol "mysql" :subname "//localhost:3306/test" :user "root" :password ""}) (yes/defquery select-foo "sql/select.sql") (defn -main "Javaで言う public static void main(String[] args)" [& args] (println "-- start.") (let [result (select-foo {:param1 "foo" :param2 123} {:connection db-spec})] (println (first result))) (println "-- finish.") )
db-spec
は接続するDBの情報です。ローカルのMySQLにrootのノンパスで接続してます*1。最近のMySQLだとtest
スキーマはデフォルトで作られないかもしれないです。そしたら存在するDBスキーマ名に変えてください。今回は実際に存在するテーブルは使用してないので、存在するDBスキーマ名ならなんでもいいです。
defquery
によって生成された関数select-foo
は第一引数にテンプレートのパラメータを取ります。*2第二引数はオプションの情報を取ります。オプションの情報にはDBの接続情報を:connection
キーワードで渡しています。*3
select-foo
の結果セットはlet
でresult
シンボルにバインドされ、次の行で標準出力に吐いてます。firstで最初の要素をとっているのは、結果セットが複数を想定してベクタ(たぶん)で返ってくるからです。
lein run
してみます。以下のように出力されればうまくいってます。
-- start. {:ret1 foo, :ret2 123} -- finish.
SQLの実行結果がマップで返ってきました。
さて、defquery
はどんな関数を生成したのでしょうか。今度はREPLを起動してdefquery
が生成したselect-foo
の定義を見てみます。
lein repl
でREPLを起動します。
(clojure.repl/doc select-foo)
と打って関数ドキュメントを表示します。select-foo
については、既にこのスコープに関数が存在するので、sel
とかまで打ってTabを打つと、名前が補完されると思います。
hi-yesql.core=> (clojure.repl/doc select-foo) ------------------------- hi-yesql.core/select-foo ([{:keys (param2 param1)} {:keys [connection]}]) return arguments simply via dual table. nil
表示されている内容は以下の通りです。
- 横線。
- 関数の完全な名前
hi-yesql.core/select-foo
。 - 関数の引数の説明
([{:keys (param2 param1)} {:keys [connection]}])
。 - SQLにコメントで記載した説明文
return arguments simply via dual table.
*4。 nil
。clojure.repl/doc
自体の返り値で標準出力じゃないので、無視してください。
こんな感じで、実際の開発ではREPLを使っているはずなので、適宜参照しながら進められます。
yesqlの基本はこれだけです。
ここで紹介した内容は注釈も含めてREADMEに書かれています。
今回は、1ファイルに1SQLテンプレートを書くdefquery
を使いました。
1ファイルに複数SQLテンプレートを書けるdefqueries
関数も存在します。*5
2015年08月09日追記
https://github.com/hatappo/hi-yesql/tree/cfb0b76a49e5e7590633e9b005d3db074af652c6
*1:本番ではパスワードを設定しましょう。
*2:yesqlの4系だとキーワード形式のパラメータにパラメータ名で紐付けるということができません。4系ではキーワード形式でパラメータに名前を付けても、通常の?バインド変数のようにあくまで順序で対応付けます。ちなみにSQLテンプレート内にはキーワード形式ではなく?でバインド変数を書くこともできます。その場合クエリする関数側では :? というキーワードにベクタとしてパラメータを渡します。これを使うメリットは特にないと思いますが。
*3:defqueryの段階で、この接続情報などのオプション情報を渡して紐付けてしまうこともできます。また、その場合も生成された関数を使う段階でさらに上書きで渡すこともできます。
*4:この英語はたぶん間違ってます。。
*5:defqueriesでは、-- name: というコメントをファイル内の各SQLに付けることで生成される関数名を制御できます。