ClojureのデータベースアクセスライブラリClojureQLを使う

この記事は Clojure Advent Calendar 2011 の7日目の記事です。

ClojureでDBにアクセスするには

  • Javaのライブラリ(JPA/Hibernate/MyBatisなど)を使う
  • clojure.contrib.sqlを使う
  • その他clojure用ライブラリを使う

Javaのライブラリを使うとあんまりClojureっぽくないですし、contribのはネイティヴすぎてちょっと使いにくい。。 ということで今回はClojureQLを使ってみます。

ClojureQLはDSLちっくにSQLを組み立てて、実行結果はマップで返ってくる。ClojureでJavaのO/Rマッパーを使っても、結局マップっぽく使いがちなので、元々マップで返ってくるのは良いと個人的に思う。

project.clj

mysqlで試してみます。

(defproject clojureql-sample "1.0.0-SNAPSHOT"
  :description "FIXME: write description"
  :dependencies [[org.clojure/clojure "1.2.1"]
                 [clojureql "1.0.0"]
                 [mysql/mysql-connector-java "5.1.13"]
                 ])

データベースオブジェクトを作成する

(def db {:classname "com.mysql.jdbc.Driver", 
             :subprotocol "mysql",
             :user "root", 
             :subname "//localhost:3306/example"})

テーブルオブジェクトを作成する

(table db :employee)
;; => SELECT employee.* FROM employee 

実はこの返り値

(class (table db :employee))
;; => clojureql.core.RTable 

clojure.core.RTableクラスという参照クラスで、値を取得するにはdereferenceする必要がありあす。

@(table db :employee)
;; => ({:age 30, :department_id 1, :address_id 1, :name "SMITH", :version 0, :id 1} {:age 28, :department_id 1, :address_id 2, :name "ALLEN", :version 0, :id 2} {:age 38, :department_id 1, :address_id 3, :name "WARD", :version 0, :id 3} {:age 40, :department_id 2, :address_id 4, :name "JONES", :version 0, :id 4}) 

この段階で初めてSQLが発行されます。

以降の説明では

(def employee (table db :employee))
(def department (table db :department))
(def address (table db :address))

を使います。

ClojureQLではこのテーブルオブジェクトを関係代数っぽい演算(メソッド)を使用して操作します。

選択する

(select employee (where (= :id 1)))
;; => SELECT employee.* FROM employee WHERE (id = 1)

where句がつきます。

@(select employee (where (= :id 1)))
;; => ({:age 30, :department_id 1, :address_id 1, :name "SMITH", :version 0, :id 1})

射影する

(project employee [:id :name])
;; => SELECT employee.id,employee.name FROM employee 

指定したフィールドだけ取得します

@(project employee [:id :name])
;; => ({:name "SMITH", :id 1} {:name "ALLEN", :id 2} {:name "WARD", :id 3} {:name "JONES", :id 4}) 

制限する

(take employee 2)
;; => SELECT employee.* FROM employee LIMIT 2  

LIMIT句がつきます。(Oracleは非対応かな)

@(take employee 2)
;; => ({:age 30, :department_id 1, :address_id 1, :name "SMITH", :version 0, :id 1} {:age 28, :department_id 1, :address_id 2, :name "ALLEN", :version 0, :id 2}) 

スキップする

(drop employee 2)
;; => SELECT employee.* FROM employee OFFSET 2 

ソートする

(sort employee [:age :id])
;; => SELECT employee.* FROM employee ORDER BY age asc,id asc       

order byがつきます。

@(sort employee [:age :id])
;; => ({:age 28, :department_id 1, :address_id 2, :name "ALLEN", :version 0, :id 2} {:age 30, :department_id 1, :address_id 1, :name "SMITH", :version 0, :id 1} {:age 38, :department_id 1, :address_id 3, :name "WARD", :version 0, :id 3} {:age 40, :department_id 2, :address_id 4, :name "JONES", :version 0, :id 4})  

昇順、降順まで指定したいときは

(sort employee [:age#desc :id#asc])
;; => SELECT employee.* FROM employee ORDER BY employee.age desc,employee.id asc

これらの関数をチェインすることでSQLを構築できます。DSLチックになってきました。

(-> employee (project [:id :name :age]) (take 3) (drop 2) (sort [:id]))
;; => SELECT employee.id,employee.name,employee.age FROM employee ORDER BY id asc LIMIT 1 OFFSET 2 

結合する

(join employee address (where (= :employee.address_id :address.id)))
;; => SELECT employee.*,address.* FROM employee JOIN address ON (employee.address_id = address.id) 

第二引数にJOINの条件を書きます。

@(join employee address (where (= :employee.address_id :address.id)))
;; => ({:age 30, :department_id 1, :address_id 1, :name "SMITH", :version 0, :city "ALBANY", :street "A STREET", :id 1} {:age 28, :department_id 1, :address_id 2, :name "ALLEN", :version 0, :city "ALBANY", :street "B STREET", :id 2} {:age 38, :department_id 1, :address_id 3, :name "WARD", :version 0, :city "TRENTON", :street "C STREET", :id 3} {:age 40, :department_id 2, :address_id 4, :name "JONES", :version 0, :city "AUSTIIN", :street "D STREET", :id 4})

2つ以上JOINする場合はチェインします。

(-> employee
    (join address (where (= :employee.address_id
                            :address.id)))
    (join department (where (= :employee.department_id
                               :department.id)))
    (project [:id :name :age
              :address.city :address.street
              [:department.name :as :department]
              :department.location]))
;; => SELECT employee.id,employee.name,employee.age,address.city,address.street,department.name AS department,department.location FROM employee JOIN address ON (employee.address_id = address.id) JOIN department ON (employee.department_id = department.id) 

結合するテーブルでカラム名が被るとエラーになるので、[:xxx :as :yyy]で名前を変えている点に注意。

@(-> employee (join address (where (= :employee.address_id :address.id))) (join department (where (= :employee.department_id :department.id))) (project [:id :name :age :address.city :address.street [:department.name :as :department] :department.location]))
;; => ({:age 30, :name "SMITH", :location "NEW YORK", :city "ALBANY", :street "A STREET", :department "ACCOUNTING", :id 1} {:age 28, :name "ALLEN", :location "NEW YORK", :city "ALBANY", :street "B STREET", :department "ACCOUNTING", :id 2} {:age 38, :name "WARD", :location "NEW YORK", :city "TRENTON", :street "C STREET", :department "ACCOUNTING", :id 3} {:age 40, :name "JONES", :location "DALLAS", :city "AUSTIIN", :street "D STREET", :department "RESEARCH", :id 4}) 

次回はINSERT/UPDATE/DELETE文について説明します。