以下の公式ドキュメントを和訳しました。
Clojure Luminus - Your first application
この和訳は以下のバージョンのテンプレートを前提としています。
https://clojars.org/luminus/lein-template/versions/2.9.8.76
テンプレートのバージョンに関してはayato_pさんの以下の記事が参考になります。
陳腐化しない Clojure の情報を書くために気をつけるべきこと/あるいは情報が陳腐化していることに気付くために知っておきたいこと
LuminusはClojureのさまざまなライブラリの集合体です。
このチュートリアルで個別のライブラリの使い方もある程度理解することができるでしょう。
ゲストブックアプリ
このチュートリアルではLuminusを使ったシンプルなゲストブックアプリを構築する方法を説明する。
ゲストブックアプリはメッセージを投稿することと他のユーザーが投稿したメッセージのリストを表示することができる。
このアプリケーションはHTMLテンプレーティング、データベースアクセス、プロジェクト設計の基本を実演することになる。
もし、あなたがまだ好みのClojure用のエディタを持っていないとしたら、LightTableを使用してこのチュートリアルを行うことをおすすめする。
JDKのインストール
ClojureはJVM上で動作するので、JDKがインストールされている必要がある。
もし、あなたの環境にJDKがまだインストールされていない場合、OpenJDKをおすすめする。
それはここからダウンロードできる。
ちなみにLuminusはデフォルト設定でJDK 8を必要とすることを記しておく。
Leiningenのインストール
Luminusを動かすためにはLeiningenをインストールする必要がある。
Leiningenは以下のような簡単な手順でインストールできる。
- スクリプトをダウンロードする
- スクリプトに実行権限を付与する(eg: chmod +x lein)
- パスの通ったディレクトリにそれを配置する(eg: ~/bin)
- 「lein self-install」を実行し、インストールが完了するのを待つ
$ wget https://raw.github.com/technomancy/leiningen/stable/bin/lein
$ chmod +x lein
$ mv lein ~/bin
$ lein
新しいアプリケーションの作成
Leiningenのインストールができたら、アプリケーションの初期化のためにターミナルで以下のコマンドを実行する。
$ lein new luminus guestbook +h2
$ cd guestbook
上記のコマンドはH2 embedded databaseを使用する新しいテンプレートプロジェクトを作成する。
Luminusアプリケーションの詳解
新しく作成したアプリケーションは以下のような構造をしている。
guestbook
|____.gitignore
|____Procfile
|____project.clj
|____profiles.clj
|____README.md
|____src
| |____guestbook
| |____core.clj
| |____handler.clj
| |____layout.clj
| |____middleware.clj
| |____routes
| | |____home.clj
| |____db
| |____core.clj
| |____migrations.clj
|____env
| |____dev
| | |____clj
| | |____guestbook
| | |____config.clj
| | |____dev_middleware.clj.clj
| |____prod
| |____clj
| |____guestbook
| |____config.clj
|____test
| |____guestbook
| |____test
| |____handler.clj
|
|____resources
|____templates
| |____about.html
| |____base.html
| |____home.html
| |____error.html
|____public
| |____css
| | |____screen.css
| |____img
| |____js
|____docs
| |____docs.md
|____migrations
| |____20150718103127-add-users-table.down.sql
| |____20150718103127-add-users-table.up.sql
|____sql
|____queries.sql
アプリケーションの最上位のフォルダに入っているファイルに関して見ていこう。
-
Procfile
- Herokuへのデプロイを支援するために使われる -
README.md
- 伝統的にアプリケーションのドキュメントが書かれるファイル -
project.clj
- Leiningenがプロジェクトの設定とライブラリの依存関係を管理するために使われる -
profiles.clj
- コードのリポジトリに入れたくないローカル設定を行うために使われる -
.gitignore
- ビルドによって生成されたファイル等のGitのバージョン管理から除外するファイルのリスト
ソースディレクトリ
すべてのソースコードはsrcフォルダ以下に置かれる。
今回作成するアプリケーションはguestbookという名前なので、プロジェクトの最上位の名前空間はこのguestbookになる。
では、作成されたすべての名前空間に関して見ていこう。
guestbook
-
core.clj
- サーバーの起動と終了に関するロジックを含む、このアプリケーションのエントリポイントである。 -
handler.clj
- アプリケーションの基本的なルーティングを定義する。これはアプリケーションのエントリポイントである。 -
layout.clj
- ページのコンテンツを描画するためのレイアウトヘルパーを記述するための名前空間である。 -
middleware.clj
- アプリケーションのカスタムミドルウェアを含む名前空間である。
guestbook.db
db
名前空間はアプリケーションのモデルを定義し、永続化層を処理するために使用される。
-
core.clj
- データベースとのやり取りのための関数を保持するために使用される -
migrations.clj
- データベースマイグレーションを実行するために使用される
guestbook.routes
routes
名前空間はhomeとaboutページのルーティング定義とコントローラが配置される場所である。
あなたが認証のような複数のルーティング定義や特定のワークフローを追加する場合は、ここにそのための名前空間を作成する必要がある。
-
home.clj
- アプリケーションのhomeページとaboutページを定義する名前空間
Envディレクトリ
これは環境依存コードのソースを含むフォルダである。dev設定は開発中に使用され、prod設定は本番環境用にコンパイルされるときに使用される。
config.clj
- 通常は開発用設定を含む
dev_middleware.clj
- 本番ではコンパイルされるべきでない開発時に使用するミドルウェアを含む
テストディレクトリ
ここはアプリケーションのテストを配置する場所であり、いくつかのサンプルテストがすでに定義されている。
リソースディレクトリ
これはアプリケーションのすべての静的なリソースを配置する場所である。
resourcesディレクトリ下のpublicディレクトリに配置したファイルはアプリケーションサーバーによってクライアントにそのまま配信される。
いくつかのCSSリソースがすでに作成されている。
HTMLテンプレート
templates
ディレクトリはページ表示のためのSelmerテンプレートのために準備されている。
about.html
- aboutページ
base.html
- サイトのベースレイアウト
home.html
- homeページ
SQLクエリ
SQLクエリはresources/sql
フォルダの中にある。
queries.sql
- SQLクエリとそれらに結びつく関数を定義する。
マイグレーションディレクトリ
LuminusはMigratusでマイグレーション管理を行っている。
マイグレーションはupとdownのSQLファイルで管理される。
これらのファイルは慣例的に日付を使ってバージョン管理され、順番に適用される。
20150718103127-add-users-table.up.sql - テーブル作成のためのマイグレーションファイル
20150718103127-add-users-table.down.sql - テーブル削除のためのマイグレーションファイル
プロジェクトファイル
上で記述したように、すべての依存関係はproject.clj
ファイルを更新することによって管理される。私たちの作成したアプリケーションのプロジェクトファイルはルートフォルダに存在し、以下のような内容となっている。
(defproject guestbook "0.1.0-SNAPSHOT"
:description "FIXME: write description"
:url "http://example.com/FIXME"
:dependencies [[org.clojure/clojure "1.7.0"]
[selmer "0.8.7"]
[com.taoensso/timbre "4.0.2"]
[com.taoensso/tower "3.0.2"]
[markdown-clj "0.9.67"]
[environ "1.0.0"]
[compojure "1.4.0"]
[ring-webjars "0.1.1"]
[ring/ring-defaults "0.1.5"]
[ring-ttl-session "0.1.1"]
[ring "1.4.0"
:exclusions [ring/ring-jetty-adapter]]
[metosin/ring-middleware-format "0.6.0"]
[metosin/ring-http-response "0.6.3"]
[bouncer "0.3.3"]
[prone "0.8.2"]
[org.clojure/tools.nrepl "0.2.10"]
[org.webjars/bootstrap "3.3.5"]
[org.webjars/jquery "2.1.4"]
[migratus "0.8.2"]
[conman "0.1.1"]
[to-jdbc-uri "0.2.0"]
[com.h2database/h2 "1.4.187"]
[org.immutant/web "2.0.2"]]
:min-lein-version "2.0.0"
:uberjar-name "guestbook.jar"
:jvm-opts ["-server"]
:main guestbook.core
:migratus {:store :database}
:plugins [[lein-environ "1.0.0"]
[lein-ancient "0.6.5"]
[migratus-lein "0.1.5"]]
:profiles
{:uberjar {:omit-source true
:env {:production true}
:aot :all}
:dev [:project/dev :profiles/dev]
:test [:project/test :profiles/test]
:project/dev {:dependencies [[ring/ring-mock "0.2.0"]
[ring/ring-devel "1.4.0"]
[pjstadig/humane-test-output "0.7.0"]
[mvxcvi/puget "0.8.1"]]
:repl-options {:init-ns guestbook.core}
:injections [(require 'pjstadig.humane-test-output)
(pjstadig.humane-test-output/activate!)]
;;when :nrepl-port is set the application starts the nREPL server on load
:env {:dev true
:port 3000
:nrepl-port 7000}}
:project/test {:env {:test true
:port 3001
:nrepl-port 7001}}
:profiles/dev {}
:profiles/test {}})
project.clj
はアプリケーションについてそれぞれ異なった観点から記述した、キーと値のペアを含んだ単なるClojureのリストであることが分かるだろう。
たいていの場合、新しいライブラリをプロジェクトに追加することが多い。これらのライブラリは特に:dependencies
のベクトルを使う。新しいライブラリをプロジェクト内で使うには、私たちはただ依存関係をここに追加するだけでいい。
:plugins
ベクトル内の要素は環境変数の読み込み(via. Environ plugin)などの機能性の追加を提供するために使われる。
:profiles
は、開発あるいは本番環境のビルドに向けてプロジェクトを初期化するために使用する。
これは環境ごとに異なるmap形式の設定となっている。
このプロジェクトは:dev
と:test
の複合プロファイルとして設定していることを記しておく。
これらのプロファイルは:project/dev
と:project/test
からの変数だけでなく、profiles.clj
中の:project/dev
と:project/test
からの変数も含んでいる。
後者には共有のコードリポジトリにチェックインしないローカル環境変数を含むべきである。
project.clj
ビルドファイルの構築に関するより詳細な事項に関しては公式のLeiningen
のドキュメントを参照してください。
データベースの作成
まず、アプリケーションのモデルを作るためにmigrations
フォルダ中に配置されている<date>-add-users-table.up.sql
を開こう。
ファイルは以下のような記述になっている。
CREATE TABLE users
(id VARCHAR(20) PRIMARY KEY,
first_name VARCHAR(30),
last_name VARCHAR(30),
email VARCHAR(30),
admin BOOLEAN,
last_login TIME,
is_active BOOLEAN,
pass VARCHAR(100));
以下のようにusers
テーブルを私たちのアプリケーションにより適切な形に書き換えよう。
CREATE TABLE guestbook
(id INTEGER PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(30),
message VARCHAR(200),
timestamp TIMESTAMP);
guestbook
テーブルはコメントした人の名前、メッセージの内容、タイムスタンプなどのメッセージを記述するすべてのフィールドを保存する。
ファイルを保存してプロジェクトのルートディレクトリで以下のコマンドを実行しよう。
$ lein run migrate
もし、すべてがうまく行ったなら、これでデータベースが初期化されるはずだ。
データベースへのアクセス
次にsrc/guestbook/db/core.clj
ファイルを見てみよう。
ここにはデータベースへの接続設定がすでにあることが分かる。
(ns guestbook.db.core
(:require
[clojure.java.jdbc :as jdbc]
[yesql.core :refer [defqueries]]
[taoensso.timbre :as timbre]
[environ.core :refer [env]])
(:import java.sql.BatchUpdateException))
(def conn
{:classname "org.h2.Driver"
:connection-uri (:database-url env)
:make-pool? true
:naming {:keys clojure.string/lower-case
:fields clojure.string/upper-case}})
(defqueries "sql/queries.sql" {:connection conn})
データベースの接続設定は実行時の:database-url
環境変数から読み込まれる。
この変数は開発環境の場合はprofiles.clj
から取り込まれ、本番環境の場合は以下のように環境変数に設定されている必要がある。
$ export DATABASE_URL="jdbc:h2:./guestbook.db"
H2 embedded databaseを使用するため、URLの中でプロジェクトを起動したディレクトリからの相対パスで指定したファイルにデータは保存される。
データベースのクエリにマッピングされる関数はdefqueries
が呼び出された時に生成される。
そのdefqueries
はsql/queries.sql
ファイルを参照していることが分かるだろう。
このファイルはresources
の中にあるので、ファイルを開いて中身を見てみよう。
-- name: create-user!
-- creates a new user record
INSERT INTO users
(id, first_name, last_name, email, pass)
VALUES (:id, :first_name, :last_name, :email, :pass)
-- name: update-user!
-- update an existing user record
UPDATE users
SET first_name = :first_name, last_name = :last_name, email = :email
WHERE id = :id
-- name: get-user
-- retrieve a user given the id.
SELECT * FROM users
WHERE id = :id
各関数は-- name:[関数名]
という形式のコメントを使用することで定義されていることが分かるだろう。
関数名を定義した次の行のコメントは関数のドキュメントになっており、さらに次の行からSQLを記述するような形式になっている。
SQLで使用するパラメータは:[パラメータ名]
という表記になっている。
実際にクエリの内容を以下のように書き換えてみよう。
-- name:save-message!
-- creates a new message
INSERT INTO guestbook
(name, message, timestamp)
VALUES (:name, :message, :timestamp)
-- name:get-messages
-- selects all available messages
SELECT * from guestbook
最後にこの名前空間はrun
というヘルパー関数を提供してくれる。この関数はconn
というアトムからデータベースへの接続を探してクエリ関数を実行する。
アプリケーションを起動する
以下のようにして開発モードでアプリケーションを起動することができる。
>lein run
00:49:54.865 [main] DEBUG org.jboss.logging - Logging Provider: org.jboss.logging.Slf4jLoggerProvider
15-Jul-19 00:49:55 Nyx INFO [guestbook.handler] - nREPL server started on port 7000
15-Jul-19 00:49:55 Nyx INFO [guestbook.handler] -
-=[guestbook started successfullyusing the development profile]=-
00:49:55.772 INFO [org.projectodd.wunderboss.web.Web] (main) Registered web context /
15-Jul-19 00:49:55 Nyx INFO [guestbook.core] - server started on port: 3000
一度サーバーを起動したら、ブラウザでhttp://localhost:3000
を開くことができ、アプリケーションが動作していることが分かるはずだ。起動時に以下のようなパラメータを指定するか、PORT
という環境変数を設定することでサーバーを別のポートで起動させることができる。
lein run 8000
表示されたページが私たちにデータベースを初期化するためのマイグレーションを実行することを即してることに注意してほしい。しかし、私たちはすでにマイグレーションを以前行っているため、もう一度行う必要はない。
ページの作成と入力フォームの取り扱い
ルーティング定義はguestbook.routes.home
名前空間に定義されている。そのファイルを開き、データベースから取り出したメッセージを表示するためのロジックを追加しよう。
まずはじめにdb
、Bouncer、ring.util.responseの名前空間への参照を一緒に追加する必要がある。
(ns guestbook.routes.home
(:require
...
[guestbook.db.core :as db]
[bouncer.core :as b]
[bouncer.validators :as v]
[ring.util.response :refer [redirect]]))
次にフォームのパラメータをバリデートするための関数を作る。
(defn validate-message [params]
(first
(b/validate
params
:name v/required
:message [v/required [v/min-count 10]])))
この関数は:name
と:message
のキーに対応する値が私たちが定めたルール従っているかチェックするためにBouncerのvalidate
関数を使用している。
具体的には、nameは必須で、messageは少なくとも10文字である必要があるようにしている。
Bouncerはmessageの場合のようにバリデータに複数のルールを渡すためにベクタ構文を使用している。
min-count
を使用した場合のようにバリデータが追加のパラメータをとる場合、ベクタ構文が同様に使用される。その値は、暗黙的に第一引数としてバリデータに渡されます。
こうして、以下のようにメッセージをバリデートし、保存するための関数を追加した。
(defn save-message! [{:keys [params]}]
(if-let [errors (validate-message params)]
(-> (redirect "/")
(assoc :flash (assoc params :errors errors)))
(do
(db/save-message!
(assoc params :timestamp (java.util.Date.)))
(redirect "/"))))
この関数はフォームのパラメータを含むリクエストからの:params
キーを取得する。
validate-message
関数がエラーを返した場合、/
にリダイレクトし、エラーと一緒に提供されたパラメータを入れたレスポンスを:flash
キーに関連付ける。
エラーを返さない場合、データベースにメッセージを保存し、リダイレクトする。
さらにhome-page
コントローラを以下のように変更する。
(defn home-page [{:keys [flash]}]
(layout/render
"home.html"
(merge {:messages (db/get-messages)}
(select-keys flash [:name :message :errors]))))
この関数はバリデートエラーなどの:flash
セッションからのすべてのパラメータと一緒に現在保存されているメッセージをテンプレートに渡してhomeページの描画する。
ルーティングの設定としてはhome-page
とsave-message!
の両方にリクエストを渡す必要がある。
(defroutes home-routes
(GET "/" request (home-page request))
(POST "/" request (save-message! request))
(GET "/about" [] (about-page)))
compojure.core
からPOST
を参照できるようにすることを忘れないようにしよう。
(ns guestbook.routes.home
(:require ...
[compojure.core :refer [defroutes GET POST]]
...))
コントローラが出来上がったので、resources/templates
ディレクトリ内にあるhome.html
テンプレートのファイルを開こう。
そのままの状態だと、content
ブロック中のcontent
変数の内容をただ表示するだけになっている。
{% extends "base.html" %}
{% block content %}
<div class="jumbotron">
<h1>Welcome to guestbook</h1>
<p>Time to start building your site!</p>
<p><a class="btn btn-primary btn-lg" href="http://luminusweb.net">Learn more »</a></p>
</div>
<div class="row">
<div class="span12">
{{docs|markdown}}
</div>
</div>
{% endblock %}
メッセージを繰り返し処理するようにし、リスト中のそれぞれの値を表示するようにcontent
ブロックを変更しよう。
{% extends "base.html" %}
{% block content %}
<div class="jumbotron">
<h1>Welcome to guestbook</h1>
<p>Time to start building your site!</p>
<p><a class="btn btn-primary btn-lg" href="http://luminusweb.net">Learn more »</a></p>
</div>
<div class="row">
<div class="span12">
<ul class="messages">
{% for item in messages %}
<li>
<time>{{item.timestamp|date:"yyyy-MM-dd HH:mm"}}</time>
<p>{{item.message}}</p>
<p> - {{item.name}}</p>
</li>
{% endfor %}
</ul>
</div>
</div>
{% endblock %}
上記のようにメッセージを繰り返し処理するためにforイテレータを使用した。それぞれのメッセージはmessage
、name
、 timestamp
のキーを使用したマップになっているため、キーの名前でアクセスすることができる。タイムスタンプを人間が読むことのできる形式に変換する日付フィルターが使用できることも記しておこう。
最後にユーザーがメッセージの送信するためのフォームを作成する。ユーザーが送信したnameとmessageの値をテンプレートにはめ込み、それらに関連するすべてのエラーを描画する。
また、CSRF対策のためにフォーム内でcsrf-field
タグを使用している。
<div class="row">
<div class="span12">
<form method="POST" action="/">
{% csrf-field %}
<p>
Name:
<input class="form-control"
type="text"
name="name"
value="{{name}}" />
</p>
{% if errors.name %}
<div class="alert alert-danger">{{errors.name|join}}</div>
{% endif %}
<p>
Message:
<textarea class="form-control"
rows="4"
cols="50"
name="message">{{message}}</textarea>
</p>
{% if errors.message %}
<div class="alert alert-danger">{{errors.message|join}}</div>
{% endif %}
<input type="submit" class="btn btn-primary" value="comment" />
</form>
</div>
</div>
最終的なhome.html
テンプレートは以下のようになるだろう。
{% extends "base.html" %}
{% block content %}
<div class="row">
<div class="span12">
<ul class="messages">
{% for item in messages %}
<li>
<time>{{item.timestamp|date:"yyyy-MM-dd HH:mm"}}</time>
<p>{{item.message}}</p>
<p> - {{item.name}}</p>
</li>
{% endfor %}
</ul>
</div>
</div>
<div class="row">
<div class="span12">
<form method="POST" action="/">
{% csrf-field %}
<p>
Name:
<input class="form-control"
type="text"
name="name"
value="{{name}}" />
</p>
{% if errors.name %}
<div class="alert alert-danger">{{errors.name|join}}</div>
{% endif %}
<p>
Message:
<textarea class="form-control"
rows="4"
cols="50"
name="message">{{message}}</textarea>
</p>
{% if errors.message %}
<div class="alert alert-danger">{{errors.message|join}}</div>
{% endif %}
<input type="submit" class="btn btn-primary" value="comment" />
</form>
</div>
</div>
{% endblock %}
入力フォームをよりかっこよく整形するためにresources/public/css
フォルダに配置されたscreen.css
ファイルを更新することができるようになった。
body {
height: 100%;
padding-top: 70px;
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
line-height: 1.4em;
background: #eaeaea;
width: 550px;
margin: 0 auto;
}
.messages {
background: white;
width: 520px;
}
ul {
list-style: none;
}
ul.messages li {
position: relative;
font-size: 16px;
padding: 5px;
border-bottom: 1px dotted #ccc;
}
li:last-child {
border-bottom: none;
}
li time {
font-size: 12px;
padding-bottom: 20px;
}
form, .error {
width: 520px;
padding: 30px;
margin-bottom: 50px;
position: relative;
background: white;
}
これでブラウザのページをリロードすればゲストブックのページに対面することになるだろう。ゲストブックにコメントを追加し、それが正しく動作することを確認してみよう。
アプリケーションのパッケージング
アプリケーションは以下のコマンドを実行することによってスタンドアローンでのデプロイ形式にすることができる。
$ lein uberjar
これは以下のようにして実行可能なjarを作成する。
$ java -jar target/guestbook.jar
このチュートリアルの完全なソースファイルはここで取得できる。より完全なサンプルを求める場合はGithub上にあるドキュメントページのソースファイルを読むといいだろう。