概要
Phoenix という次世代の WEB フレームワークを使って、シンプルな CRUD を作成します。
Elixir / Phoenix の魅力については以下の記事を参考にしてください
[翻訳] Elixir - 次に来る大物Web言語
Ruby / Rails よりも10倍くらい早くて非常に可読性の高い言語なのでおすすめです。
ソースコードは以下よりご確認ください。
phoenix-crud | GitHub
環境構築
環境構築をしていない方は以下の記事を参考にしてください。
Elixir / Phoenix の環境構築!HelloWorld!
プロジェクトの作成
ブログの記事を投稿したり、削除したりするようなアプリを作っていきます。
以下のコマンドを実行することで、プロジェクトが作成されます。
$ mix phoenix.new blog_app
特に指定しなければ、DB は postgres
になります。
config/dev.exs
にて以下のように記述されていると思います。
以下の通りに postgres のユーザを作成するか、既存のユーザの username, password を入力して設定します。
# Configure your database
config :blog_app, BlogApp.Repo,
adapter: Ecto.Adapters.Postgres,
username: "postgres",
password: "postgres",
database: "blog_app_dev",
hostname: "localhost",
pool_size: 10
設定が正しければ、データベースを作成できます。
$ mix ecto.create
#=> The database for BlogApp.Repo has been created
次に確認のために、サーバーを起動してみましょう。
$ mix phoenix.server
http://localhost:4000/ を訪れると、以下のようなページが表示されています。
Post モデルの作成
$ mix phoenix.gen.model Post posts title:string body:text
実行すると以下の migration ファイルと model が作成されます。
defmodule BlogApp.Repo.Migrations.CreatePost do
use Ecto.Migration
def change do
create table(:posts) do
add :title, :string
add :body, :text
timestamps()
end
end
end
null: false
を追加して migrate します。
def change do
create table(:posts) do
- add :title, :string
+ add :title, :string, null: false
- add :body, :text
+ add :body, :text, null: false
timestamps()
end
end
$ mix ecto.migrate
New アクションの実装
Router の設定
では、Post を作成するための機能を作っていきます。
ルーティングを設定します。特定のルーティングと Controller を合致させます。
ルーティングは、web/router.ex
で行います。
scope "/", BlogApp do
pipe_through :browser # Use the default browser stack
get "/", PageController, :index
+ resources "/posts", PostController
end
以下のように表示されていれば問題なしです。
$ mix phoenix.routes
mix phoenix.routes is deprecated. Use phx.routes instead.
Compiling 6 files (.ex)
page_path GET / BlogApp.PageController :index
post_path GET /posts BlogApp.PostController :index
post_path GET /posts/:id/edit BlogApp.PostController :edit
post_path GET /posts/new BlogApp.PostController :new
post_path GET /posts/:id BlogApp.PostController :show
post_path POST /posts BlogApp.PostController :create
post_path PATCH /posts/:id BlogApp.PostController :update
PUT /posts/:id BlogApp.PostController :update
post_path DELETE /posts/:id BlogApp.PostController :delete
Controller の設定
次に Controller を作成します。役割は Rails のコントローラーと同じです。
ユーザからのリクエストを処理してページを表示したりデータベースを更新したりします。
$ touch web/controllers/post_controoler.ex
以下のようにして changeset を定義して、template に値を渡します。
defmodule BlogApp.PostController do
use BlogApp.Web, :controller
alias BlogApp.Post
def new(conn, _params) do
changeset = Post.changeset(%Post{})
render conn, "new.html", changeset: changeset
end
end
Changeset について詳しく知りたい人は以下の記事を読むと良いでしょう。
Ecto.Changeset | 公式ドキュメント
View の設定
次に View を設定します。
Rails でいうところの Helper のようなものです。
$ touch web/views/post_view.ex
特に必要なメソッドがないので空の状態で ok です。
defmodule BlogApp.PostView do
use BlogApp.Web, :view
end
Template の設定
<h1>New</h1>
<%= form_for @changeset, post_path(@conn, :create), fn f -> %>
<div class="form-group">
<label>タイトル</label>
<%= text_input f, :title, class: "form-control" %>
<%= error_tag f, :title%>
</div>
<div class="form-group">
<label>本文</label>
<%= textarea f, :body, class: "form-control" %>
<%= error_tag f, :body %>
</div>
<%= submit "Submit", class: "btn btn-primary" %>
<% end %>
/posts/new
を訪れると以下のように表示されていると思います。
Create アクションの実装
次に new で作った画面から記事を作成できるようにしましょう。
Validation の追加
model の changeset メソッドにおいて validation を追加します。
changeset は受け取った params を Struct に変換して、validation を行う役割があります。
ここでは、title の長さを最大30までに制限します。
defmodule BlogApp.Post do
use BlogApp.Web, :model
schema "posts" do
field :title, :string
field :body, :string
timestamps()
end
def changeset(struct, params \\ %{}) do
struct
|> cast(params, [:title, :body])
|> validate_required([:title, :body])
+ |> validate_length(:title, max: 30)
end
end
Create アクションの追加
コントローラーに以下を追加します。
def create(conn, %{"post" => post_params}) do
changeset = Post.changeset(%Post{}, post_params)
case Repo.insert(changeset) do
{:ok, _} ->
conn
|> put_flash(:info, "投稿しました")
|> redirect(to: post_path(conn, :new))
{:error, changeset} ->
conn
|> put_flash(:error, "投稿に失敗しました")
|> render("new.html", changeset: changeset)
end
end
create アクションでは、第二引数でパターンマッチングを行い post_params を取得します。
そして、changeset メソッドによって params より構造体を作りレコードに保存しています。
投稿に成功すると以下のように表示されるはずです。
失敗した場合は次のようになります。
ちゃんと失敗した理由まで表示されていますね。
index アクション
次は投稿の一覧を表示する index を作っていきます。
controller に以下を追加します。
Phoenix も Rails 同様に Ecto と呼ばれる OR マッパーがあります。
詳しくはこちらの記事を読むとよいでしょう。
ActiveRecord vs Ecto | Rails使いがElixirのEctoを勉強した時のまとめ
def index(conn, _params) do
posts = Post |> Repo.all
render conn, "index.html", posts: posts
end
次にそれらを表示するための Template を作成します。
<h1>All Posts</h1>
<%= for post <- @posts do %>
<div class="row">
<h2><%= post.title %></h2>
</div>
<% end %>
最後に、Post 作成後のリダイレクトを index に変更します
def create(conn, %{"post" => post_params}) do
changeset = Post.changeset(%Post{}, post_params)
case Repo.insert(changeset) do
{:ok, _} ->
conn
|> put_flash(:info, "投稿しました")
- |> redirect(to: post_path(conn, :new))
+ |> redirect(to: post_path(conn, :index))
{:error, changeset} ->
conn
|> put_flash(:error, "投稿に失敗しました")
|> render("new.html", changeset: changeset)
end
Show
controller に以下を追加する。
def show(conn, %{"id" => id}) do
post = Post |> Repo.get(id)
render conn, "show.html", post: post
end
パラメーターの id をパターンマッチングで取得します。
そして、Repo.get にて該当する post を取得して、Template 側に渡しています。
template を追加します。
<h1>Show</h1>
<div class="row">
<%= @post.title %>
</div>
<div class="row">
<%= @post.body %>
</div>
最後に index.html.eex にてリンクを追加する。
<%= for post <- @posts do %>
<div class="row">
<h2><%= post.title %></h2>
+ <%= link 'detail', to: post_path(@conn, :show, post) %>
</div>
<% end %>
リンクをクリックすると以下のような画面が表示されているはずです。
Edit
Controller にメソッドを追加します。
def edit(conn, %{"id" => id}) do
post = Post |> Repo.get(id)
changeset = Post.changeset(post)
render(conn, "edit.html", post: post, changeset: changeset)
end
以下のことをやっています
- post を id によって取得
- その post によって changeset を作成
- post と changeset を template に渡す
<h1>Edit</h1>
<%= form_for @changeset, post_path(@conn, :update, @post), fn f -> %>
<div class="form-group">
<label>タイトル</label>
<%= text_input f, :title, class: "form-control" %>
<%= error_tag f, :title%>
</div>
<div class="form-group">
<label>本文</label>
<%= textarea f, :body, class: "form-control" %>
<%= error_tag f, :body %>
</div>
<%= submit "Submit", class: "btn btn-primary" %>
<% end %>
template は path 以外は、new と一緒です。
最後に、index テンプレートに edit ページへのリンクを追加します。
<h1>All Posts</h1>
<%= for post <- @posts do %>
<div class="row">
<h2><%= post.title %></h2>
<%= link 'detail', to: post_path(@conn, :show, post) %>
+ <%= link 'edit', to: post_path(@conn, :edit, post) %>
</div>
<% end %>
index ページよりリンクを押して確認すると、画像のように表示されているはずです。
部分テンプレートの使用
このままでも良いのですが、冗長的なので部分テンプレートを使います。
$ touch web/templates/post/form.html.eex
Rails と使い方は同じです。部分テンプレート内の変数は元のテンプレートより渡す必要があります。
<%= form_for @changeset, @path, fn f -> %>
<div class="form-group">
<label>タイトル</label>
<%= text_input f, :title, class: "form-control" %>
<%= error_tag f, :title%>
</div>
<div class="form-group">
<label>本文</label>
<%= textarea f, :body, class: "form-control" %>
<%= error_tag f, :body %>
</div>
<%= submit "Submit", class: "btn btn-primary" %>
<% end %>
edit template において部分テンプレートを使用します。
<h1>Edit</h1>
+ <%= render "form.html", changeset: @changeset, path: post_path(@conn, :create) %>
- <%= form_for @changeset, post_path(@conn, :update, @post), fn f -> %>
- <div class="form-group">
- <label>タイトル</label>
- <%= text_input f, :title, class: "form-control" %>
- <%= error_tag f, :title%>
- </div>
- <div class="form-group">
- <label>本文</label>
- <%= textarea f, :body, class: "form-control" %>
- <%= error_tag f, :body %>
- </div>
- <%= submit "Submit", class: "btn btn-primary" %>
- <% end %>
new.html.eex においても同様に修正してください。
edit とは path のみが異なります。
<h1>New</h1>
<%= render "form.html", changeset: @changeset, path: post_path(@conn, :create) %>
Update
controller を追加します。
def update(conn, %{"id" => id, "post" => post_params}) do
post = Post |> Repo.get(id)
changeset = Post.changeset(post, post_params)
case Repo.update(changeset) do
{:ok, _} ->
conn
|> put_flash(:info, "投稿を更新しました")
|> redirect(to: post_path(conn, :index))
{:error, changeset} ->
conn
|> put_flash(:error, "更新に失敗しました")
|> render("edit.html", changeset: changeset, post: post)
end
end
update の場合は id を取得する必要がありますが、それ以外は create とほとんど同じです。
id から取得したレコードによって changeset を作成して、update します。
edit ページより更新すると以下のように表示されているはずです。
Delete
最後に Delete です。
controller はシンプルです。レコードを取得して削除するだけです。
def delete(conn, %{"id" => id}) do
post = Post |> Repo.get(id)
Repo.delete(post)
conn
|> put_flash(:info, "削除に成功しました。")
|> redirect(to: post_path(conn, :index))
end
index ページにリンクを追加して終了です。
<h1>All Posts</h1>
<%= for post <- @posts do %>
<div class="row">
<h2><%= post.title %></h2>
<%= link 'detail', to: post_path(@conn, :show, post) %>
<%= link 'edit', to: post_path(@conn, :edit, post) %>
+ <%= link 'delete', to: post_path(@conn, :delete, post), method: :delete %>
</div>
<% end %>
リンクを押すと削除されているのがわかります。
最後に
以上で Phoenix で CRUD アプリを作成を終わります!
Phoenix は日本語文献がまだまだ少ないので、
勉強するときは公式のドキュメントを読んで勉強することをおすすめします。
不明点やご指摘あればコメントいただければ幸いです。