読者です 読者をやめる 読者になる 読者になる

(-> % read write unlearn)

All opinions expressed are solely my own and do not express the views or opinions of my employer.

clojure yesql (1)

clojure

ClojureRDBSQLをごにょごにょするライブラリを探すとkormaの記事が多いです。しかし、java生まれseasar育ちとしてはORMよりもsqlテンプレート方式のほうが使いやすいです。SQL好きだし。ClojureSQLテンプレートなら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

yesqlMySQLコネクタの依存を追加します。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のキーワードみたいになってるparam1param2が動的に埋め込まれるバインド変数です。

再度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の結果セットはletresultシンボルにバインドされ、次の行で標準出力に吐いてます。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

表示されている内容は以下の通りです。

  1. 横線。
  2. 関数の完全な名前hi-yesql.core/select-foo
  3. 関数の引数の説明([{:keys (param2 param1)} {:keys [connection]}])
  4. SQLにコメントで記載した説明文return arguments simply via dual table.*4
  5. nilclojure.repl/doc自体の返り値で標準出力じゃないので、無視してください。

こんな感じで、実際の開発ではREPLを使っているはずなので、適宜参照しながら進められます。

yesqlの基本はこれだけです。
ここで紹介した内容は注釈も含めてREADMEに書かれています。

今回は、1ファイルに1SQLテンプレートを書くdefqueryを使いました。 1ファイルに複数SQLテンプレートを書けるdefqueries関数も存在します。*5


2015年08月09日追記

ソースコードgithubにpushしました。

https://github.com/hatappo/hi-yesql/tree/cfb0b76a49e5e7590633e9b005d3db074af652c6

*1:本番ではパスワードを設定しましょう。

*2:yesqlの4系だとキーワード形式のパラメータにパラメータ名で紐付けるということができません。4系ではキーワード形式でパラメータに名前を付けても、通常の?バインド変数のようにあくまで順序で対応付けます。ちなみにSQLテンプレート内にはキーワード形式ではなく?でバインド変数を書くこともできます。その場合クエリする関数側では :? というキーワードにベクタとしてパラメータを渡します。これを使うメリットは特にないと思いますが。

*3:defqueryの段階で、この接続情報などのオプション情報を渡して紐付けてしまうこともできます。また、その場合も生成された関数を使う段階でさらに上書きで渡すこともできます。

*4:この英語はたぶん間違ってます。。

*5:defqueriesでは、-- name: というコメントをファイル内の各SQLに付けることで生成される関数名を制御できます。