ClojureScriptでRDBを使ったアプリを作成する(その1)

アプリ作成

Clojure + ClojureScript + SQLiteの組み合わせで、タスク管理アプリを作成します。 まずはプロジェクトを作成します。。clj-templateを使って楽をしましょう。

bash
lein new clj-template tasks

project.cljを編集します。clojureのバージョンを1.4にしたり、必要なパッケージを追加したりしています。

diff
diff --git a/project.clj b/project.clj
index 799322c..99e588b 100644
--- a/project.clj
+++ b/project.clj
@@ -1,11 +1,15 @@
(defproject tasks "0.1.0-SNAPSHOT"
:description "FIXME: write this!"
:url "http://exampl.com/FIXME"
- :dependencies [[org.clojure/clojure "1.3.0"]
+ :dependencies [[org.clojure/clojure "1.4.0"]
[noir-cljs "0.3.0"]
[jayq "0.1.0-alpha1"]
[fetch "0.1.0-alpha2"]
[crate "0.1.0-alpha3"]
- [noir "1.3.0-beta2"]]
+ [noir "1.3.0-beta2"]
+ [enfocus "1.0.0-beta1"]
+ [org.clojure/java.jdbc "0.2.3"]
+ [org.xerial/sqlite-jdbc "3.7.2"]]
+ :plugins [[lein-cljsbuild "0.2.8"]]
:cljsbuild {:builds [{}]}
:main ^{:skip-aot true} tasks.server)

DB操作

DBを操作するためのコードを書きます。

diff
diff --git a/src/tasks/models/db.clj b/src/tasks/models/db.clj
new file mode 100644
index 0000000..796730b
--- /dev/null
+++ b/src/tasks/models/db.clj
@@ -0,0 +1,32 @@
+(ns tasks.models.db
+ (:require [clojure.java.jdbc :as sql]))
+
+(def db
+ {:classname "org.sqlite.JDBC"
+ :subprotocol "sqlite"
+ :subname "db/database.db"
+ })
+
+(defn init-db []
+ (try
+ (sql/with-connection db
+ (sql/create-table
+ :tasks
+ [:id "integer primary key"]
+ [:title "varchar(250)"]
+ [:status "varchar(10)"]
+ ))
+ (catch Exception ex
+ (.getMessage (.getNextException ex)))))
+
+(defn add-task [task]
+ (sql/with-connection db
+ (sql/transaction
+ (sql/insert-record :tasks task))))
+
+(defn get-all-task []
+ (sql/with-connection db
+ (sql/transaction
+ (sql/with-query-results result
+ ["SELECT * FROM tasks"]
+ (into [] result)))))

データベースとテーブルを作成して、レコードの追加を取得を行ってみます。 database.dbを配置するためのdbディレクトリを、プロジェクトのルートディレクトリにあらかじめ作成しておく必要がある事に注意してください。

bash
$ lein repl
nREPL server started on port 35663
REPL-y 0.1.0-beta10
Clojure 1.3.0
Exit: Control+D or (exit) or (quit)
Commands: (user/help)
Docs: (doc function-name-here)
(find-doc "part-of-name-here")
Source: (source function-name-here)
(user/sourcery function-name-here)
Javadoc: (javadoc java-object-or-class-here)
Examples from clojuredocs.org: [clojuredocs or cdoc]
(user/clojuredocs name-here)
(user/clojuredocs "ns-here" "name-here")
tasks.server=> (use 'tasks.models.db)
nil
tasks.server=> (init-db)
(0)
tasks.server=> (add-task {:title "buy milk"})
{:last_insert_rowid() 1}
tasks.server=> (get-all-task)
[{:status nil, :title "buy milk", :id 1}]
tasks.server=> (add-task {:title "send a mail"})
{:last_insert_rowid() 2}
tasks.server=> (get-all-task)
[{:status nil, :title "buy milk", :id 1} {:status nil, :title "send a mail", :id 2}]
tasks.server=>

DBとのやりとりをマップで行うのはとても楽だと思います。 これで、replからDBにタスクを登録できるようになりました。 次はビューを作成して、ブラウザからタスクを登録してみます。

ビュー作成

\ diff diff --git a/src/tasks/views/main.clj b/src/tasks/views/main.clj index 6e69e7c..a6a751b 100644 --- a/src/tasks/views/main.clj +++ b/src/tasks/views/main.clj @@ -1,8 +1,17 @@ (ns tasks.views.main (:require [tasks.views.common :as common]) (:use [noir.core :only [defpage]] - [hiccup.core :only [html]])) + tasks.models.db + noir.fetch.remotes + hiccup.form))

(defpage "/" [] - (common/layout - [:div#content])) + (common/layout + [:h2 "Add Task"] + [:div {:id "add-task"} + (text-field {:class "title"} "title") + [:button {:class "submit"} "Submit"]])) + +(defremote add-task-to-db [task] + (add-task task) + (println (get-all-task))) \

ここで登場したdefremoteは、[[https://github.com/ibdknox/fetch][ibdknox/fetch]] というライブラリで定義されています。 fetchは、クライアント/サーバ間のやりとりを簡単にするためのライブラリです。 add-task-to-dbは、クライアントからタスクを受け取ると、それをDBに追加します。 最後に、クライアント側のコードを書く。クライアント側はClojureScriptを使います。 ここで使っている [[https://github.com/ckirkendall/enfocus][ckirkendall/enfocus]] は、DOMとテンプレートを扱うためのライブラリです。 Clojure用の[[https://github.com/cgrand/enlive][cgrand/enlive]] にインスパイアされたものらしいです。

\ diff diff --git a/src/tasks/client/main.cljs b/src/tasks/client/main.cljs index dddf7d5..f48672d 100644 --- a/src/tasks/client/main.cljs +++ b/src/tasks/client/main.cljs @@ -1,9 +1,14 @@ (ns tasks.client.main (:require [noir.cljs.client.watcher :as watcher] [clojure.browser.repl :as repl] - [crate.core :as crate]) + [crate.core :as crate] + [enfocus.core :as ef] + [fetch.remotes :as remotes]) (:use [jayq.core :only [$ append]]) - (:use-macros [crate.macros :only [defpartial]])) + (:use-macros [crate.macros :only [defpartial]]) + (:require-macros [enfocus.macros :as em] + [fetch.macros :as fm])) +

;;************************************************ ;; Dev stuff @@ -23,3 +28,19 @@

(append $content (up-and-running))

+(defn get-task-title [] + (em/from (em/select ["#title"]) (em/get-prop :value))) + +(defn get-task-data [] + {:title (get-task-title) + :status "unfinidhed"}) + +(defn push-task [] + (fm/remote (add-task-to-db (get-task-data)))) + + +(em/defaction setup [] + [".submit"] (em/listen :click push-task)) + +(set! (.-onload js/window) setup) +
\

get-task-titleは、フォームに入力されたテキストを取得する関数です。 get-task-dataで呼び出しています。get-task-dataは入力された値からタスクを生成します。 push-taskで、クライアントからサーバへタスク登録のリクエストを送ります。 fm/remoteで、サーバ側の関数を実行している。実装がどうなっているのか、一度見てみたい所ですね。 setupは、ボタンをクリックした時のイベントを定義します。

これで、ブラウザからDBにタスクを登録できるようになりました。

ブラウザから操作する

lein runを実行し、ブラウザのhttp://localhost:8090 にアクセスします。 フォームに適当な文字列を入力し、SbmitをクリックするとDBに登録されます。 add-task-to-dbで、タスクを登録した後にDBの全てのタスクをコンソールに出力しているので、それを見れば登録されていることが確認できますね。

<code>\</code> bash $ lein run 09:31:56 :: Using source dir: src/ WARNING: filter already refers to: #'clojure.core/filter in namespace: enfocus.macros, being replaced by: #'enfocus.macros/filter WARNING: delay already refers to: #'clojure.core/delay in namespace: enfocus.macros, being replaced by: #'enfocus.macros/delay 09:31:57 :: Files updated :simple 2012-11-14 09:31:57.065:INFO::Logging to STDERR via org.mortbay.log.StdErrLog09:31:57 :: Simple compile Starting server... Server started on port [8090]. You can view the site at http://localhost:8090
<h1></h1>
2012-11-14 09:31:57.066:INFO::jetty-6.1.25 2012-11-14 09:31:57.093:INFO::Started SocketConnector@0.0.0.0:8090 09:32:00 :: Done [{:status nil, :title buy milk, :id 1} {:status nil, :title send a mail, :id 2} {:status unfinidhed, :title aaa, :id 3}] [{:status nil, :title buy milk, :id 1} {:status nil, :title send a mail, :id 2} {:status unfinidhed, :title aaa, :id 3} {:status unfinidhed, :title bbb, :id 4}]
\

ClojureScript所感

サーバサイドとクライアントでほぼ同じ構文を使えるのはとても楽です。 fetchを使えば、クライアントからサーバ側の関数をほとんど違和感なく呼び出すこともできます。 何より、書いていて楽しいのが良いですね。 ただ、あまり良くないのかな、と思う所として、デバッグがやりにくい点があります。 いいやり方があるのかもしれないけど、どこでエラーが発生しているのかいまいち把握しづらいと感じます。 また、構文が似ているだけあって、ライブラリの非互換性が残念です。 うっかり、ClojureScriptでClojureのライブラリを呼び出そうとしてしまいそうです。

次回は、DBからデータを取得し、画面に表示したりしてみようと思います。