Help us understand the problem. What is going on with this article?

Elixir / Phoenix で CRUD アプリを作成!

More than 3 years have passed since last update.

概要

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 を入力して設定します。

config/dev.exs
# 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/ を訪れると、以下のようなページが表示されています。

スクリーンショット 2017-08-28 22.52.55.png

Post モデルの作成

$ mix phoenix.gen.model Post posts title:string body:text

実行すると以下の migration ファイルと model が作成されます。

priv/repo/migrations/2017XXXXXXXXXX_create_post.exs
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 に値を渡します。

web/controllers/post_controller.ex
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 です。

web/views/post_view.ex
defmodule BlogApp.PostView do
  use BlogApp.Web, :view
end

Template の設定

web/templates/post/new.html.eex
<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 を訪れると以下のように表示されていると思います。

スクリーンショット 2017-08-28 23.21.27.png

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 アクションの追加

コントローラーに以下を追加します。

web/controllers/post_controller.ex

  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 より構造体を作りレコードに保存しています。

投稿に成功すると以下のように表示されるはずです。

スクリーンショット 2017-08-29 7.10.16.png

失敗した場合は次のようになります。
ちゃんと失敗した理由まで表示されていますね。

スクリーンショット 2017-08-29 7.13.49.png

index アクション

次は投稿の一覧を表示する index を作っていきます。

controller に以下を追加します。
Phoenix も Rails 同様に Ecto と呼ばれる OR マッパーがあります。
詳しくはこちらの記事を読むとよいでしょう。

ActiveRecord vs Ecto | Rails使いがElixirのEctoを勉強した時のまとめ

web/controllers/post_controller.ex
  def index(conn, _params) do
    posts = Post |> Repo.all
    render conn, "index.html", posts: posts
  end

次にそれらを表示するための Template を作成します。

web/templates/post/index.html.eex
<h1>All Posts</h1>

<%= for post <- @posts do %>
<div class="row">
  <h2><%= post.title %></h2>
</div>
<% end %>


スクリーンショット 2017-08-29 7.28.05.png

最後に、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 を追加します。

web/templates/post/show.html.eex
<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 %>


リンクをクリックすると以下のような画面が表示されているはずです。

スクリーンショット 2017-08-29 7.36.50.png

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 に渡す
web/templates/post/show.html.eex

<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 ページよりリンクを押して確認すると、画像のように表示されているはずです。

スクリーンショット 2017-08-29 8.16.46.png

部分テンプレートの使用

このままでも良いのですが、冗長的なので部分テンプレートを使います。

$ touch  web/templates/post/form.html.eex

Rails と使い方は同じです。部分テンプレート内の変数は元のテンプレートより渡す必要があります。

web/templates/post/form.html.eex

<%= 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 において部分テンプレートを使用します。

web/templates/post/edit.html.eex
<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 のみが異なります。

new.html.eex
<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 ページより更新すると以下のように表示されているはずです。

スクリーンショット 2017-08-29 8.31.15.png

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 %>

リンクを押すと削除されているのがわかります。

スクリーンショット 2017-08-29 8.35.39.png

最後に

以上で Phoenix で CRUD アプリを作成を終わります!

Phoenix は日本語文献がまだまだ少ないので、
勉強するときは公式のドキュメントを読んで勉強することをおすすめします。

不明点やご指摘あればコメントいただければ幸いです。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away