This article was contributed by a member of the Heroku community
Its contents might not always reflect updates to the Heroku platform.
データベース支援型の Clojure Web アプリケーションの構築
この記事の英語版に更新があります。ご覧の翻訳には含まれていない変更点があるかもしれません。
最終更新日 2021年03月05日(金)
Table of Contents
この記事では、データベース支援型の Clojure Web アプリケーションの作成について説明します。
このアプリは Shouter という名前の小さな Twitter クローンで、ユーザーが “シャウト” を入力すると、PostgreSQL データベースに保存され、アプリのフロントページに表示されます。Heroku にデプロイされた Shouter の完成した例を確認するか、完成したソースを表示することができます。
それでは始めましょう。
前提条件
- Heroku スターターガイド (Clojure) を読み、内容を理解していること。
- PostgreSQL データベースサーバーがローカルでインストールおよび実行されていること。
- Heroku ユーザーアカウント。 無料ですぐにサインアップできます。
clojure.java.jdbc を使用した PostgreSQL への接続
データベースの永続性は、このサンプルアプリを含め、多くの Web アプリケーションにとって重要です。幸いにも、Clojure で正式サポートされているライブラリには、JDBC 標準に沿ったデータベース永続性のための clojure.java.jdbc が含まれています。
まず、shouter
という名前の新しいプロジェクトを Leiningen を使用して作成します。
$ lein new shouter
shouter
プロジェクトで、Clojure JDBC および PostgreSQL ドライバーの依存関係を追加します。ここで示しているのは、この記事の執筆時点で最新のバージョン番号であることに注意してください。今後、バージョン番号がこれよりも新しくなる可能性がありますが、互換性は保証されていません。
project.clj
(defproject shouter "0.0.2"
:description "Shouter app"
:url "https://github.com/technomancy/shouter"
:min-lein-version "2.0.0"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies [[org.clojure/clojure "1.7.0"]
[org.clojure/java.jdbc "0.6.1"]
[org.postgresql/postgresql "9.4-1201-jdbc41"]])
一部のパッケージマネージャでは、デフォルトで PostgreSQL がシステムレベルのバックグラウンドデーモンとして実行されますが、開発作業時は、アクセス許可の問題を回避して可視性を向上させるために、手動で postgres
を起動することをお勧めします。
$ initdb pg
$ postgres -D pg &
これらの実行可能ファイルが見つからない場合は、/usr/lib/postgresql/*/bin
を $PATH
に追加してみてください。
次に、開発作業用のローカル PostgreSQL データベースを作成します。
$ createdb shouter
コマンドラインから、またはエディタ内で REPL を起動して実験を開始します。
$ lein repl
これからまず行うのは、データベースにテーブルを作成することです。JDBC 関数を導入します。
user=> (require '[clojure.java.jdbc :as sql])
nil
これにより、clojure.java.jdbc
名前空間が解決され、これから使用する sql
というエイリアスが設定されます。 次のようにしてテーブルを作成します。
user=> (sql/db-do-commands "postgresql://localhost:5432/shouter"
(sql/create-table-ddl :testing [[:data :text]]))
(0)
data
テキストフィールドを持つ testing
テーブルがデータベース内に作成されます。data
フィールドにデータを入れてみましょう。
user=> (sql/insert! "postgresql://localhost:5432/shouter"
:testing {:data "Hello World"})
({:data "Hello World"})
適当なデータを作成したので、データベースをクエリしてそのデータを取得してみます。
user=> (sql/query "postgresql://localhost:5432/shouter"
["select * from testing"])
({:data "Hello World"})
上出来です。データは簡単に取得できます。後で邪魔にならないようにテーブルを削除します。
user=> (sql/db-do-commands "postgresql://localhost:5432/shouter"
"drop table testing")
(0)
始めるために必要なデータベースの基本は以上です。その他の一般的な疑問点に対する解説は、clojuredocs.org のこちらのガイドを参照してください。
Compojure を使用した Web バインディング
Compojure は、Clojure での Web 開発に広く利用されるライブラリであり、Shouter のコア部分です。
Compojure は、Ruby の Rack に似た汎用 Web アプリケーションライブラリの Ring をベースに構築されています。Ring はアプリの低レベル接着剤の多くを実装する一方、Compojure はアプリケーションロジックを定義するための簡潔な構文を提供します。
Jetty HTTP サーバーアダプターと共に、Compojure を project.clj
ファイルに追加します。
project.clj
Leiningen の古いバージョンを使用している場合、更新された依存関係をここで取得するために lein deps
を手動で実行することが必要な場合があります。
(defproject shouter "0.0.2"
:description "Shouter app"
:url "https://github.com/technomancy/shouter"
:min-lein-version "2.0.0"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies [[org.clojure/clojure "1.7.0"]
[org.clojure/java.jdbc "0.6.1"]
[org.postgresql/postgresql "9.4-1201-jdbc41"]
[ring/ring-jetty-adapter "1.4.0"]
[compojure "1.4.0"]])
ここで、src/shouter/web.clj
に初期コードを追加します。
src/shouter/web.clj
(ns shouter.web
(:require [compojure.core :refer [defroutes GET]]
[ring.adapter.jetty :as ring]))
(defroutes routes
(GET "/" [] "<h2>Hello World</h2>"))
(defn -main []
(ring/run-jetty #'routes {:port 8080 :join? false}))
依存関係を変更したので、古い REPL プロセスは失効しています。終了して新しい REPL プロセスを開始してください。
$ lein repl
user> (require 'shouter.web)
nil
user> (shouter.web/-main)
Creating database structure... done
2015-08-26 19:30:07.277:INFO:oejs.Server:nREPL-worker-0: jetty-9.2.10.v20150310
2015-08-26 19:30:07.333:INFO:oejs.ServerConnector:nREPL-worker-0: Started ServerConnector@249942f5{HTTP/1.1}{0.0.0.0:8080}
2015-08-26 19:30:07.334:INFO:oejs.Server:nREPL-worker-0: Started @8655ms
#object[org.eclipse.jetty.server.Server 0x5f92ac2e "org.eclipse.jetty.server.Server@5f92ac2e"]
shouter.web=>
ブラウザで http://localhost:8080
にアクセスして、作業の成果を確認できるようになりました。次の手順は HTML テンプレートです。
Hiccup を使用した HTML テンプレート処理
Clojure にはさまざまな HTML テンプレートライブラリがありますが、最もシンプルなのは Hiccup です。Hiccup テンプレートは、呼び出されると HTML を出力する単なる Clojure 関数です。Hiccup を使用して単純な HTML テンプレート処理をアプリに追加しましょう。
まず、Hiccup を project.clj
ファイルに追加します。
project.clj
(defproject shouter "0.0.2"
:description "Shouter app"
:url "https://github.com/technomancy/shouter"
:min-lein-version "2.0.0"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies [[org.clojure/clojure "1.7.0"]
[org.clojure/java.jdbc "0.6.1"]
[org.postgresql/postgresql "9.4-1201-jdbc41"]
[ring/ring-jetty-adapter "1.4.0"]
[compojure "1.4.0"]
[ring/ring-defaults "0.1.2"]
[hiccup "1.0.5"]])
ここで REPL を開始し、Hiccup の動作をざっと見てみます。
user=> (require '[hiccup.core :as h])
nil
user=> (h/html [:h1 "Hello Word"])
"<h1>Hello Word</h1>"
現在の Compojure アプリケーションの内部で Hiccup を使用して、必要なすべての HTML を生成できます。src/shouter/web.clj
で、hiccup.page
を ns
宣言に追加します。
(:require [hiccup.page :as page])
ここで、単純な index
関数を追加します。
(defn index []
(page/html5
[:head
[:title "Hello World"]]
[:body
[:div {:id "content"} "Hello World"]]))
最後に、index
関数をルートに追加します。ルートは最終的に次のようになります。
(defroutes routes
(GET "/" [] (index)))
アプリケーションを再起動します。好みに応じて、repl から -main
を呼び出す代わりに、lein run -m shouter.web
を使用してサーバーを起動することができます。http://localhost:8080
にアクセスすると、前回よりも小さなテキストで "Hello World"
と表示されるはずです。 ソースを確認すると、適切な HTML ドキュメントも生成されているはずです。
Compojure と同様、Hiccup の世界はもっと奥深いものです。ここでは、当面の現実的な目標に向けて、以上の簡単な紹介にとどめています。
集大成
一通りの基本は説明したため、完全な Shouter アプリケーションの構築に進みます。
Shouter アプリケーションの完全なコードは、一連のスタイルが入った resources
ディレクトリを含め、こちらで参照できます。 同じルックアンドフィールを得るために、このディレクトリを自由にプロジェクトにコピーしてください。
アプリケーション本体について、src/shouter/web.clj
から見ていきます。
src/shouter/web.clj
(ns shouter.web
(:require [compojure.core :refer [defroutes]]
[ring.adapter.jetty :as ring]
[compojure.route :as route]
[ring.middleware.defaults :refer [wrap-defaults site-defaults]]
[shouter.controllers.shouts :as shouts]
[shouter.views.layout :as layout]
[shouter.models.migration :as schema])
(:gen-class))
(defroutes routes
shouts/routes
(route/resources "/")
(route/not-found (layout/four-oh-four)))
(def application (wrap-defaults routes site-defaults))
(defn start [port]
(ring/run-jetty application {:port port
:join? false}))
(defn -main []
(schema/migrate)
(let [port (Integer. (or (System/getenv "PORT") "8080"))]
(start port)))
index
関数が削除され、いくつかのルートが追加されていることがわかります。新しい -main
関数はコマンドラインからアプリケーションを起動するときの処理であり、start
関数は REPL 内部から操作するときの処理です。コントローラーの名前空間 shouts が追加されています。コードを入力します。
src/shouter/controllers/shouts.clj
(ns shouter.controllers.shouts
(:require [compojure.core :refer [defroutes GET POST]]
[clojure.string :as str]
[ring.util.response :as ring]
[shouter.views.shouts :as view]
[shouter.models.shout :as model]))
(defn index []
(view/index (model/all)))
(defn create
[shout]
(when-not (str/blank? shout)
(model/create shout))
(ring/redirect "/"))
(defroutes routes
(GET "/" [] (index))
(POST "/" [shout] (create shout)))
上記のコードは、ユーザーアクションを処理するためのインフラストラクチャです。 下から上に見ていくと、web.clj
でリンクされたコードの断片である routes が見つかります。ここでは、このコントローラーでのルートの処理方法を記述しています。 この手法を使用すると、参照用にルートを各コントローラーに保持しておき、後から web.clj
に含まれるグローバルルートで参照することができます。 index
および create
関数は単純な応答処理にすぎません。次に示すのはビューレイヤです。
src/shouter/views/layout.clj
(ns shouter.views.layout
(:require [hiccup.page :as h]))
(defn common [title & body]
(h/html5
[:head
[:meta {:charset "utf-8"}]
[:meta {:http-equiv "X-UA-Compatible" :content "IE=edge,chrome=1"}]
[:meta {:name "viewport" :content
"width=device-width, initial-scale=1, maximum-scale=1"}]
[:title title]
(h/include-css "/stylesheets/base.css"
"/stylesheets/skeleton.css"
"/stylesheets/screen.css")
(h/include-css "http://fonts.googleapis.com/css?family=Sigmar+One&v1")]
[:body
[:div {:id "header"}
[:h1 {:class "container"} "SHOUTER"]]
[:div {:id "content" :class "container"} body]]))
(defn four-oh-four []
(common "Page Not Found"
[:div {:id "four-oh-four"}
"The page you requested could not be found"]))
これは、ビューをレンダリングするための基礎です。 重複を減らすために、表示関数で呼び出せるテンプレートをセットアップします。 four-oh-four
関数は、グローバルルートでセットアップされる小さな拡張機能にすぎません。次に、実際のビューに進みます。
src/shouter/views/shouts.clj
(ns shouter.views.shouts
(:require [shouter.views.layout :as layout]
[hiccup.core :refer [h]]
[hiccup.form :as form]
[ring.util.anti-forgery :as anti-forgery]))
(defn shout-form []
[:div {:id "shout-form" :class "sixteen columns alpha omega"}
(form/form-to [:post "/"]
(anti-forgery/anti-forgery-field)
(form/label "shout" "What do you want to SHOUT?")
(form/text-area "shout")
(form/submit-button "SHOUT!"))])
(defn display-shouts [shouts]
[:div {:class "shouts sixteen columns alpha omega"}
(map
(fn [shout] [:h2 {:class "shout"} (h (:body shout))])
shouts)])
(defn index [shouts]
(layout/common "SHOUTER"
(shout-form)
[:div {:class "clear"}]
(display-shouts shouts)))
ここは表示ロジックの要所です。layout.clj
にコードが追加されたので見通しが良くなっています。フロントエンドは完成したので、データレイヤに移ります。
src/shouter/models/migration.clj
(ns shouter.models.migration
(:require [clojure.java.jdbc :as sql]
[shouter.models.shout :as shout]))
(defn migrated? []
(-> (sql/query shout/spec
[(str "select count(*) from information_schema.tables "
"where table_name='shouts'")])
first :count pos?))
(defn migrate []
(when (not (migrated?))
(print "Creating database structure...") (flush)
(sql/db-do-commands shout/spec
(sql/create-table-ddl
:shouts
[[:id :serial "PRIMARY KEY"]
[:body :varchar "NOT NULL"]
[:created_at :timestamp
"NOT NULL" "DEFAULT CURRENT_TIMESTAMP"]]))
(println " done")))
次に示すのは、shouts
テーブルをデータベースに作成して必要なフィールドを定義するコードです。これを実行してシャウトを保存します。
src/shouter/models/shout.clj
(ns shouter.models.shout
(:require [clojure.java.jdbc :as sql]))
(def spec (or (System/getenv "DATABASE_URL")
"postgresql://localhost:5432/shouter"))
(defn all []
(into [] (sql/query spec ["select * from shouts order by id desc"])))
(defn create [shout]
(sql/insert! spec :shouts [:body] [shout]))
実際のデータモデルは実にシンプルです。 大事なことは入力であり、すべてのシャウトを収集することです。
エントリポイント
“uberjar” がデプロイ用に生成されます。これは単に、アプリのコードとその依存関係のすべてをまとめた jar アーカイブのことです。uberjar の行き先を明確にするために、アプリの project.clj
の :main
で、エントリポイントの名前空間を Leiningen に指示する必要があります。
(defproject shouter "0.0.2"
:description "Shouter app"
:url "https://github.com/technomancy/shouter"
:min-lein-version "2.0.0"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies [[org.clojure/clojure "1.7.0"]
[org.clojure/java.jdbc "0.6.1"]
[org.postgresql/postgresql "9.4-1201-jdbc41"]
[ring/ring-jetty-adapter "1.4.0"]
[compojure "1.4.0"]
[ring/ring-defaults "0.1.2"]
[hiccup "1.0.5"]]
:main ^:skip-aot shouter.web
:uberjar-name "shouter-standalone.jar"
:plugins [[lein-ring "0.8.13"]]
:ring {:handler shouter.web/application
:init shouter.models.migration/migrate}
:profiles {:dev {:dependencies [[javax.servlet/servlet-api "2.5"]
[ring-mock "0.1.5"]]}
:uberjar {:aot :all}})
ビルドプロセス中に限って AOT (ahead-of-time: 事前) コンパイルをトリガーするために使用される :uberjar
プロファイルもあります。project.clj
の一番上の階層に :aot :all
を配置できますが、これによって開発中に問題が起きる可能性があるため、このコンパイルはデプロイ中にのみ実行するのが最善です。最後に Heroku では、Procfile
において、プラットフォームに依存しない形でエントリポイントを宣言する必要があります。
web: java $JVM_OPTS -jar target/shouter-standalone.jar
これで、アプリケーションが完成しました。
ローカルでのテスト
ローカルで uberjar を生成して、正しく機能することを確認します。
$ lein uberjar
Compiling shouter.web
Compiling shouter.views.shouts
Compiling shouter.views.layout
Compiling shouter.models.shout
Compiling shouter.models.migration
Compiling shouter.controllers.shouts
Created /home/phil/src/shouter/target/shouter-0.0.1.jar
Created /home/phil/src/shouter/target/shouter-standalone.jar
最後に、Procfile
に定義した内容に従ってアプリケーションを起動します。
$ java $JVM_OPTS -jar target/shouter-standalone.jar
2014-01-20 16:29:23.309:INFO:oejs.Server:jetty-7.x.y-SNAPSHOT
2014-01-20 16:29:23.372:INFO:oejs.AbstractConnector:Started SelectChannelConnector@0.0.0.0:8080
http://localhost:8080
にアクセスすると、完全な機能を備えた Shouter アプリが実行されているはずです。
デプロイ
すべてが期待どおりに機能することをローカルで確認したので、いよいよアプリを Heroku にデプロイします。
まず、アプリを Git にコミットします。
$ git init
$ git add .
$ git commit -m "init"
次に、Heroku 上でアプリを作成します。
$ heroku create
Creating stormy-fog-408... done, stack is heroku-18
http://stormy-fog-408.herokuapp.com/ | git@heroku.com:stormy-fog-408.git
Git remote heroku added
ここで、アプリ用のデータベースをプロビジョニングする必要があります。以前は、createdb
コマンドを使用してローカル PostgreSQL データベースをプロビジョニングしました。Heroku では、Heroku PostgreSQL データベースアドオンを使用してリモートデータベースをプロビジョニングできます。
$ heroku addons:create heroku-postgresql
-----> Adding heroku-postgresql to stormy-fog-408... done, v2 (free)
これにより、DATABASE_URL
がアプリの環境に追加されます。アプリでは実行時にこれを使用して、そのリモートデータベースリソースに接続します。Heroku Postgres Hobby Tier アドオンは無料です。
データベースがプロビジョニングされたので、コードをデプロイして Web dyno をスケールアップできます。
$ git push heroku master
Fetching repository, done.
[...]
To git@heroku.com:stormy-fog-408.git
50084da..4b015a9 master -> master
$ heroku ps:scale web=1
Scaling web processes... done, now running 1
この時点で、アプリケーションが実行されているはずです。次のようにして、Web プロセスが稼働していることを確認します。
$ heroku ps
=== web (Free): `java $JVM_OPTS -jar target/shouter-standalone.jar`
web.1: starting 2015/08/25 10:38:48 (~ 6s ago)