06. Web 開発
この章では、 Crystal を用いて Web 開発を行う方法について解説します。
Crystal での Web 開発の前提知識
まず、 Crystal では HTTP でのサービスをどのように処理するかについて解説します。
前の章でも既に解説済みですが、改めて Crystal での Web 開発の基本となる部分について解説します。
以下のコードで解説します。
require "http/server"
server = HTTP::Server.new do |context|
context.response.content_type = "text/plain"
context.response.print "Hello world"
end
puts "Listening on http://0.0.0.0:8080"
server.listen(8080)
ポート 8080 での HTTP アクセスを Listen する最小のプログラムです。
以下のコードを実行することで簡易 HTTP サーバとして機能します。
localhost:8080 にアクセスすると Hello world
が表示されます。
$ crystal server.cr
Listening on http://0.0.0.0:8080
HTTP ハンドラ
基本的に Crystal の HTTP サーバが返却するパラメータは レスポンス本文、ヘッダ、ステータスコードの3種です。
その3種を持っている限りにおいて、処理をスタックすることが出来ます。
この辺りの規約は Ruby でいうところの Rack に似ているといえます。
HTTP ハンドラを実装するには Rack 同様に call メソッドを定義し、引数に HTTP::Server::Context
型のオブジェクトを渡すという形式になっています。
以下に例を記載します。
require "http/server"
class InterruptTestFirst
include HTTP::Handler
def call(context)
response = call_next(context)
puts "aa"
response
end
end
class InterruptTestSecond
include HTTP::Handler
def call(context)
response = call_next(context)
puts "bb"
response
end
end
server = HTTP::Server.new([InterruptTestFirst.new, InterruptTestSecond.new]) do |context|
context.response.content_type = "text/plain"
context.response.print "Hello world"
end
puts "Listening on http://0.0.0.0:8080"
server.listen(8080)
上記の例で言いますと、 InterruptTestFirst
InterruptTestSecond
の順にスタックに積まれます。
実行されるのは先入れ後出しなので、後に積まれた InterruptTestSecond
からになります。
curl -X GET http://localhost:8080
を実行することで以下の内容がコンソールに出力されます。
bb
aa
主に Crystal で Web 開発を行う上での基本的な内容ですが、実際に開発する場合は何らかの Web フレームワークを用いることになるかと思います。
次項から Web フレームワークについて解説します。
Crystal の Web フレームワーク
Crystal で現在最も活発に開発されている Web フレームワークが Kemal です。
Ruby でいうところの Sinatra に似た設計思想で作られており、シンプルな実装で Web アプリを作成することが出来ます。
本書では主に Kemal を用いて簡単な Web アプリを作成しながら Web 開発の方法を解説していきます。
- Web サイト
- GitHub
また Kemal 以外の主なフレームワークとして以下のものがあります。
フルスタックの Web フレームワークです。
様々なコードジェネレータや、 OR/M を備えています。 Rails 的な規約重視のフレームワークです。
Kemal による Web 開発
それでは本章から Kemal による Web 開発を、サンプルを交えながら説明します。
また本サンプルに使用する Kemal のバージョンは 0.24.0
を想定しています。
Kemal インストール
適当な作業用のディレクトリ以下で、以下のコードを実行してみます。ディレクトリが作成され幾つかのファイルがひな形から作成されます。
本稿ではサンプルアプリを kemal-sample として進めます。
$ crystal init app kemal-sample
$ cd kemal-sample
shard.yml ファイルに以下の内容を追記します。
dependencies:
kemal:
github: kemalcr/kemal
version: 0.27.0
以下のコマンドを実行することで Kemal 本体をインストールすることが出来ます。
$ shards install
Kemal FirstStep
インストールまで問題なく動かせたら続いて簡単なサンプルを作成してみましょう。
カレントの src ディレクトリ以下の kemal-sample.cr
に以下の追記を行います。
まず行頭に以下の行を足します。
require "kemal"
続いて module Kemal::Sample
内に以下の内容を追記します。
get "/hello" do |env|
hello = "Hello World"
render "src/views/hello.ecr"
end
末尾の行に以下の内容を追記します。
Kemal.run
画面側の処理を作成します。
src
以下に views ディレクトリを作成し、以下の hello.ecr
ファイルを views
ディレクトリ内に作成します。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>hello</title>
</head>
<body>
<%= hello %>
</body>
</html>
Kemal の基本的な使い方として、 REST の動詞 GET POST PUT DELETE を以下の形で記述します。
get "/path" do |env|
# 処理
end
post "/path" do |env|
# 処理
end
詳しい内容については後述します。
先ずは記述したら、以下のコマンドでビルド、実行します。
$ crystal build src/kemal-sample.cr
$ ./kemal-sample
ブラウザで http://localhost:3000/hello
でアクセスします。
Hello World
と表示されていれば問題ありません。
DB と連動する
通常、 Web アプリでは DB が必須です。 Kemal で作ったアプリから DB にアクセスすることも可能です。
ライブラリを使用することで PostgreSQL か、もしくは MySQL を使用することが出来ます。
今回は PostgreSQL を使用します。
カラム名 |
型 |
id |
serial |
title |
text |
content |
text |
DB を作成します。
$ createdb kemal_sample_development -O your_owner
DDL を作成しロードします。
create table articles (
id serial primary key,
title text,
content text
);
$ psql -U your_owner -d kemal_sample_development -f sql/create_articles.sql
作成した後、 shard.yml ファイルに以下の内容を追記します。
dependencies:
kemal:
github: kemalcr/kemal
version: 0.27.0
db:
github: crystal-lang/crystal-db
pg:
github: will/crystal-pg
編集後、以下のコマンドを実行します。
$ shards install
投稿一覧ページの編集
これからいよいよ Web アプリらしく記事の一覧ページと詳細ページ、新規投稿ページをそれぞれ作成していきます。
まず全ページで共通で使用するテンプレートは別に作成します。
テンプレートヘッダに新規投稿ページと投稿リストページヘのリンクを表示し、ページ内に各ページのコンテンツを表示するように修正していきます。
レイアウトページの作成
まずひな形のページを application.ecr
という名前で src/views
ディレクトリ内に作成します。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>kemal sample</title>
<!-- bootstrapを使用する -->
<link
rel="stylesheet"
href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
<link rel="stylesheet" href="/css/custom.css">
</head>
<body>
<header class="navbar navbar-fixed-top navbar-inverse">
<div class="container">
<a id="logo">sample app</a>
<nav>
<ul class="nav navbar-nav navbar-right">
<li><a href="/articles">ArticleList</a></li>
<li><a href="/articles/new">新規投稿</a></li>
</ul>
</nav>
</div>
</header>
<div class="container">
<%= content %>
</div>
</body>
</html>
本サンプルでは BootStrap を使用します。
CDN を使用しますので特にダウンロード不要ですが、ダウンロードする場合は別途 http://getbootstrap.com/
から必要なファイルをダウンロードし、プロジェクトカレントの /public
以下に配置してください。
CSS の作成
ページ修飾用の CSS を custom.css
という名前で public/css
ディレクトリ内に作成します。本サンプルでは rails tutorial をそのまま参考にします。
body {
padding-top: 60px;
}
section {
overflow: auto;
}
textarea {
resize: vertical;
}
.center {
text-align: center;
}
.center h1 {
margin-bottom: 10px;
}
h1, h2, h3, h4, h5, h6 {
line-height: 1;
}
h1 {
font-size: 3em;
letter-spacing: -2px;
margin-bottom: 30px;
text-align: center;
}
h2 {
font-size: 1.2em;
letter-spacing: -1px;
margin-bottom: 30px;
text-align: center;
font-weight: normal;
color: #777;
}
p {
font-size: 1.1em;
line-height: 1.7em;
}
#logo {
float: left;
margin-right: 10px;
font-size: 1.7em;
color: #fff;
text-transform: uppercase;
letter-spacing: -1px;
padding-top: 9px;
font-weight: bold;
}
#logo:hover {
color: #fff;
text-decoration: none;
}
一覧ページの作成
記事一覧ページを index.ecr
という名前で src/views
ディレクトリ内に作成します。
<h2>Article List</h2>
<table class="table table-striped">
<thead>
<tr>
<td>title</td>
</tr>
<tbody>
<% articles.each do |article| %>
<tr>
<td>
<a href="/articles/<%=article["id"]? %>" target="_top">
<%=article["title"]? %>
</a>
</td>
</tr>
<% end %>
</tbody>
</table>
新規投稿ページ
記事投稿用のページを new.ecr
という名前で src/views/articles
ディレクトリ内に新規作成します。
以下のファイルを追加します。
<h2>新規投稿</h2>
<form method="post", action="/articles">
<input type="text" name="title" size="10" maxlength="10" />
<br />
<br />
<textarea name="content" cols="40" rows="4"></textarea>
<br />
<br />
<input type="submit" value="post">
</form>
詳細ページ
一覧ページから記事タイトルをクリックした時に遷移する詳細ページを show.ecr
という名前で src/views/articles
ディレクトリ内に作成します。
データへの更新については後述します。
<h2>Article</h2>
<% articles.each do |article| %>
<h3><%=article["title"] %></h3>
<p><%=article["content"] %></p>
<!-- 更新 -->
<a href="/articles/<%=article["id"] %>/edit" target="_top" class="btn btn-primary">edit</a>
<br />
<% end %>
Kemal プログラム改修
kemal-sample.cr を以下の内容で修正します。
行頭を以下の内容に修正します。
require "kemal"
require "db"
require "pg"
続いて module Kemal::Sample
内に以下の内容を追記します。
database_url = "postgres://localhost:5432/kemal_sample_development"
db = DB.open(database_url)
["/", "/articles"].each do |path|
get path do |env|
articles = [] of Hash(String, String | Int32)
db.query("select id, title, content from articles") do |rs|
rs.each do
article = {} of String => String | Int32
article["id"] = rs.read(Int32)
article["title"] = rs.read(String)
article["content"] = rs.read(String)
articles << article
end
end
db.close
render "src/views/index.ecr", "src/views/application.ecr"
end
end
get "/articles/new" do |env|
render "src/views/articles/new.ecr", "src/views/application.ecr"
end
post "/articles" do |env|
# env.params.bodyでformのvalueを取得できます
title_param = env.params.body["title"]
content_param = env.params.body["content"]
params = [] of String
params << title_param
params << content_param
# update, insert, deleteは以下のようにexecでアップデートを実行します
db.exec("insert into articles(title, content) values($1::text, $2::text)", params)
db.close
env.redirect "/"
end
get "/articles/:id" do |env|
articles = [] of Hash(String, String | Int32)
article = {} of String => String | Int32
id = env.params.url["id"].to_i32
params = [] of Int32
params << id
sql = "select id, title, content from articles where id = $1::int8"
article["id"], article["title"], article["content"] =
db.query_one(sql, params, as: {Int32, String, String})
articles << article
db.close
render "src/views/articles/show.ecr", "src/views/application.ecr"
end
修正は以上です。次項から処理について解説します。
クエリ
まずクエリは以下のように記述します。
db.query("select id, title, content from articles") do |rs|
rs.each do
article = {} of String => String | Int32
article["id"] = rs.read(Int32)
article["title"] = rs.read(String)
article["content"] = rs.read(String)
articles << article
end
end
query メソッドに SQL クエリを記述し、ループ内で結果を格納していきます。
1件だけ取得したい場合は query_one
もしくは query_one?
を使います。後者は1件もデータが無い場合がありうる場合に使います。
sql = "select id, title, body from articles where id = $1::int8"
article["id"], article["title"], article["body"] =
db.query_one(sql, params, as: {Int32, String, String})
query_one
の戻り値は、 as
で指定した型の Tuple
になります。
パラメータを SQL 文に渡す場合は上記例で言うと
$1::int8
と、連番で指定していきます。
これは PostgreSQL の例です。 MySQL の場合は?で指定します。
指定できる型番は以下の形式です。
-
text
-
boolean
-
int8 int4 int2
-
float4 float8
-
timestamptz date timestamp
-
json and jsonb
-
uuid
-
bytea
-
numeric/decimal
-
varchar
-
regtype
-
geo types
-
array types: int8 int4 int2 float8 float4 bool text
データの更新
更新系のクエリは以下のような記述で行います。
データの投入は以下のように行います。
db.exec("insert into articles(title, content) values($1::text, $2::text)", params)
SQL 文に update や delete も設定ができます。
レンダリング
View の構造はテンプレート形式で設定することが出来ます。
render "src/views/articles/show.ecr", "src/views/application.ecr"
の形式で ecr ファイルをレンダリングすることで、 Rails のようにファイルを入れ子の形で描画することが出来ます。
Web アプリ再起動後に http://localhost:3000/
にアクセスすると、まだ記事が投稿されていないので空欄になっています。
http://localhost:3000/articles/new
にアクセスすると、タイトルと本文を入力する画面が表示されており、入力後に一覧に遷移すると成功です。
記事タイトルをクリックすると詳細画面に遷移すると成功です。
オブジェクトの CRUD
Kemal は RESTful に対応しており、 GET POST 以外にも、 PUT DELETE にも対応しております。
記事の編集をサンプルに追加しながら説明します。
新規ページで src/views/articles/edit.ecr
を追加します。
データの編集画面
<h2>投稿編集</h2>
<% articles.each do |article| %>
<form method="post", action="/articles/<%=article["id"] %>">
<!--
hiddenフィールドにname="_method"、
valueにputを設定する。
-->
<input type="hidden", name="_method", value="put" />
<input type="text" name="title" size="10" maxlength="10" value="<%=article["title"] %>" />
<br />
<br />
<textarea name="content" cols="40" rows="4"><%=article["content"] %></textarea>
<br />
<br />
<input type="submit" value="edit" class="btn btn-primary">
</form>
<% end %>
ブラウザによっては form が get post 以外には対応していない場合、以下の hidden フィールドの設定で PUT や DELETE で送信することが出来ます。
kemal-sample.cr
を以下の内容で修正します。
get "/articles/:id/edit" do |env|
articles = [] of Hash(String, String | Int32)
article = {} of String => String | Int32
id = env.params.url["id"].to_i32
params = [] of Int32
params << id
sql = "select id, title, content from articles where id = $1::int8"
article["id"], article["title"], article["content"] =
db.query_one(sql, params, as: {Int32, String, String})
articles << article
db.close
render "src/views/articles/edit.ecr", "src/views/application.ecr"
end
put "/articles/:id" do |env|
id = env.params.url["id"].to_i32
title_param = env.params.body["title"]
content_param = env.params.body["content"]
params = [] of String | Int32
params << title_param
params << content_param
params << id
db.exec("update articles set title = $1::text, content = $2::text where id = $3::int8", params)
db.close
env.redirect "/articles/#{id}"
end
これで、データの追加から一覧表示、編集までの処理が行えるようになります。
また、 DELETE を追加する場合は以下のように行います。
delete "/articles/:id" do |env|
id = env.params.url["id"].to_i32
params = [] of Int32
params << id
db.exec("delete from articles where id = $1::int8", params)
db.close
env.redirect "/"
end
これでデータの作成、表示、更新、削除の機能を持ったアプリを作ることが出来ます。
本稿では省略しますが、セッションを用いて認証機能を追加することも出来ます。
補注
本稿のコードでは取り上げませんでしたが、 Web アプリを書く上で注意すべき事項を以下に記載します。
トランザクション
データの原子性を保つ上で、トランザクション制御は必須です。
以下の設定でトランザクションのコミット、およびロールバックを設定します。
database_url = "postgres://localhost:5432/kemal_sample_development"
db = DB.open(database_url)
db.transaction do |tx|
result = tx.connection.exec("insert into articles(title, content) values('hoge', 'huga')")
# エラー時
if !result
tx.rollback
else
tx.commit
end
end
MySQL
MySQL を使用する場合は、以下の設定を行います。
shard.yml
に以下の記述を行います。
dependencies:
mysql:
github: crystal-lang/crystal-mysql
require "db"
require "mysql"
DB.open "mysql://root@localhost/test" do |db|
# 実行系
db.exec "insert into contacts values (?, ?)", "John", 30
end