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からデータを取得し、画面に表示したりしてみようと思います。

Org-mode 7.9.1の新機能(Org Agenda編)

org-mode 7.9.1の新機能、Agenda編です。

変数:org-agenda-sticky

Agendaを付箋のように扱うためのオプションです。
従来のAgendaは、qキーを押すとアジェンダバッファが削除されていました。 org-agenda-stickyを有効にすることで、アジェンダバッファが削除されなくなります。 e2wmを使う時、特定のウィンドウにアジェンダを表示しっぱなしにするために使えそうです。

org-agenda-custom-command-contexts

アジェンダコマンドのコンテキストを設定します。
org-capture-template-contextsと同じように設定すればOKです。
下記の例だと、”p”コマンドはtxtファイルの中でしか使用できません。
(setq org-agenda-custom-commands-contexts
‘((“p” (in-file . “.txt”))))

bulkアクションの変更

kから始まるコマンドは削除され、”バルクアクション”と統合されました。

アジェンダで複数のエントリをマークした後、B sを実行することでタスクをリスケジュールできます。

ポイントが日付の上にある時にkを実行することで、その日付で上書きされたキャプチャが実行されます。

NとPを押すことでアイテムを上/下に移動できるようになりました

コマンド:org-agenda-bulkmark-all

全てのアイテムをマークします。 一括して削除やアーカイブしたい場合に便利ですね。

変数:org-agenda-persistent-marks

有効にすることで、バルクアクションを実行した後もマークが有効なままとなります。

バルクアクションでTODOステータスを間違えて変更した場合等に使えそうです。

org-agenda-skip-timestamp-id-deadline-is-shown

todo-unblocked と nottodo-unblocked

org-agenda-skip-ifという、

アジェンダをカテゴリでフィルタできる

Org-mode 7.9.1の新機能(Org Capture編)

2012-09-03にOrg-modeのバージョン7.9.1がリリースされました。 今回は、キャプチャに追加された新機能を紹介します。

コマンド:org-capture-string

プロンプトで入力した文字列がテンプレートの注釈として挿入されます。

org-captureでは

  1. ‘M-x org-capture’を実行
  2. テンプレートを選択
  3. 入力

ですが、org-capture-stringだと

  1. ‘M-x org-capture-string’を実行
  2. ミニバッファから文字列を入力
  3. テンプレートを選択
  4. 2で入力した文字列が挿入された状態で入力

となります。バッチモードで使う際に便利そうです。

変数:org-capture-templates-contexts

キャプチャのテンプレートが使えるコンテキストを設定できます。 下記のように設定すると、org-modeでキャプチャした時だけ”t”のテンプレートを使うことができるようになります。

scm
(setq org-capture-templates-contexts
'(("t" ((in-mode . "org-mode")))))

コンテキストはデフォルトで4種類用意されています。自分で関数を書けば、かなり柔軟にテンプレートを操作できそうです。 ホスト名、ネットワーク、時間帯等いろいろ条件を設定すれば便利になるでしょう。

  • in-file: マッチするファイルでのみ使用できる
  • in-mode: マッチするモードでのみ使用できる
  • not-in-file: マッチするファイルでは使用できない
  • not-in-mode: マッチするファイルでは使用できない
  • [function]: カスタム関数

テンプレート:%l

リテラルリンクを挿入するためのテンプレートです。 %aはリンクと文言がセットだけど、%lだとリンクしか挿入されません。

変数:org-capture-bookmark

nil以外をセットすると、キャプチャした際に自動的にブックマークに追加します。

テンプレート?%

^{PROMPT}と連携させるようですが、使い方がよくわかりませんでした。

空行挿入の制御

キャプチャした時に、エントリの前か後に空行を挿入するための設定です。

  • :empty-lines-before i

    エントリの前にi行の空行を挿入する

  • :empty-lines-after i

    エントリの後にi行の空行を挿入する

フック:org-capture-prepare-finalize-hook

キャプチャの最終プロセスが始まる前に実行されるフックです。

Linux Native KVM Toolsのメモリ割り当て

LKNTが仮想マシンにメモリをどのように割り当てているのか調べました. 流れとしては,ゲスト作成時にメモリを確保,KVMに確保したメモリを伝える,ゲストが終了する際に確保していたメモリを開放という手順です. 今回はユーザ側のコードですが,いずれはKVMの内部でメモリがどのように扱われているのかも調べてみたいです.

ユーザインタフェース


ユーザがゲストに割り当てるメモリのサイズを指定するにはコマンドライン引数を用います.

メモリ確保


指定した値は,グローバル変数のram_sizeに格納されます.

build-run.c/int kvm_cmd_run

コマンドライン引数で指定しなかった場合どうなるのでしょうか? build-run.cの中でram_sizeに値が格納されているかどうかを確認し,格 納されていない場合はget_ram_size関数で割り当てるメモリサイズを算出 し,ram_sizeに格納します.

build-run.c

get_ram_sizeには使用可能なCPUコア数を渡しています.そして,コア数からメモリサイズを算出します. コア数が2なら,64x(2+3)=320MBとなるようです. ゲストに割り当てられるメモリサイズはマシン全体のメモリサイズにRAM_SIZE_RATIOを掛けたサイズとなります. 算出したメモリサイズが割り当てられる限界値を越えている場合,限界値に修正してram_sizeを返却します.

build.c/host_ram_size

get_ram_sizeの中で呼び出されているhost_ram_size関数はホストマシンのメモリサイズを調べる関数です. sysconf(_SC_PHYS_PAGES)で物理メモリのページ数を取得,sysconf(_SC_PAGE_SIZE)でページサイズをバイト単位で取得します. ページ数xページサイズ=メモリサイズ(バイト)となります. 最後に単位をメガバイトに変更して返却します. メガバイトに変換する時,右シフトを使っています.

build.c

1メガバイト = 1048576バイトで,2進数にすると1048576 = 0b100000000000000000000となります. これを20ビット右シフトすると1になり,バイトからメガバイトへ変換ができるわけです.

build-run.c/int kvm_cmd_run

ゲストに割り当てるメモリのサイズが最小サイズより大きいか,もしくはホストのメモリサイズより大きくなっていないか確認します. 問題なければ,ram_sizeをメガバイトからバイトに戻しています.

KVMへ確保したメモリを登録する

さて,kvmの初期化関数に移動しましょう.

builtin-run.c/kvm_cmd_run

kvm_initは,さらにアーキテクチャ固有のinit関数を呼び出しています.

kvm.c/kvm__init

x86/kvm.c

kvm->ram_sizeにコマンドライン引数から取得した/自動生成したメモリサイズを格納します. ここで,32bitのギャップについてチェックしています.

KVM_32BIT_GAP_SIZEは (768<<20) = 805306368 = 768MB. KVM_32BIT_GAP_STARTは ((1ULL << 32) – KVM_32BIT_GAP_SIZE) = (4294967296 – 768MB) = (4096MB -768MB) = 3328MB.

指定したram_sizeが3328MBより小さい場合,そのサイズをmmapで確保します. ram_sizeが3328MB以上の場合,ram_size+768MBをmmapでマッピングします. メモリのマップに成功した場合,mprotectを使ってkvm->ram_start + KVM_32BIT_GAP_START〜KVM_32BIT_GAP_SIZEのメモリ範囲のアクセス保護を設定しています. PROT_NOTEは,メモリに全くアクセスできない設定となります. メモリのマップに失敗した場合はdieです.

madviceはメモリの利用に関するアドバイスを設定する関数です. kvm->ram_startからkvm->ram_sizeの範囲について,MADV_MERGEABLEに設定しています.MADV_MERGEABLEは,ユーザがマージしてもよいという設定です.

メモリの設定

build-run.cでしばらく先にあるkvm__init_ramでメモリ設定を行います.

builtin-run.c

x86/kvm.c

ここでも32ビットギャップを調べて,結果によって挙動を変えています. メモリサイズが3328MBより小さい場合,ゲストに設定する仮想メモリのサイズにkvm->ram_sizeを,ホストメモリの開始位置にkvm->ram_startを格納し,kvm__register_memを呼び出してVMのメモリの設定を行います.

3328MB以上の場合,仮想メモリのサイズに3328MBを設定してVMのメモリ設定を行い, その後,残りのサイズを再びVMに設定しています.

/kvm.c

ioctlのKVM_SET_USER_MEMORY_REGIONを使い,VMにメモリの変更を許可します.

KVM_SET_USER_MEMORY_REGIONはKVMのAPIです. mem_slotsがマザーボードのメモリスロットに対応するようです. 3328MB以上のメモリを扱う場合は,複数スロットに分割するわけですね. これで,VMがメモリを使えるようになります.

メモリの開放

VMを終了するときにメモリを開放する必要があります.kvm__deleteの中で開放されています.

builtin-run.c

kvm.c

munmapを使い,指定したアドレス範囲のマップを消去するだけのようです.

Emacsのデータストアを見てみる

本記事はEmacs Advent Calendar jp: 2011 : ATNDの23日目です. emacs lispで,keyとvalueを扱う方法について調べていたので,そのま とめです. emacs lispでkey-valueを扱う方法としては,alist(連想リスト), plist(属性リスト),ハッシュテーブルがあるようです. alistとplistはS式を使ったもので,シンプルです.ただ,リストを順番 に見ていくため,計算量は最悪の場合O(n)となり,効率が悪そうです. ハッシュテーブルの場合,計算量はO(1)なので,大量のデータを扱う場 合はplistやalistより速いのですが,データをS式で扱うのではなくハッ シュテーブル型になります. どれを使うかは,用途によるでしょう,plistはユーザが設定を書く時に よく使われますね.

alist(連想リスト)

alist(association list)はコンスセルを用いたリスト.

keyを与えるとkeyとvalueのコンスセルが返ってきます.valueを取り出 すためにcdrを使うのが少し手間な気がします.

plist(属性リスト)

plist(property list)は属性を保持するためのリスト.

個人的に好きなデータ構造です.リストなので扱いやすいためです.

hash table

名前の通り,ハッシュテーブル.

ハッシュテーブルは,今回調べていて初めて存在を知りました.Emacsで 大量のkeyとvalueを扱う場合は素直にハッシュテーブルを使うのがよさ そうですね.

おわりに

EmacsLispでkeyとvalueを扱う構造について紹介しました. Emacs で IPC や RPC (Emacs Advent Calendar jp: 2011) – 技術 日記@kiwanamiのように,emacs lispから外部の言語やDBにアクセスす る試みも行われています.RDBやKVSをemacsから使う日もそう遠くはなさ そうです.