スポンサーリンク

2015年7月23日

[Elixir+Phoenix]All users (implements pagination)

Goal

ユーザの一覧を表示するページを実装する。
ページネーションを実装する。

Dev-Environment

OS: Windows8.1
Erlang: Eshell V6.4, OTP-Version 17.5
Elixir: v1.0.4
Phoenix Framework: v0.13.1
PostgreSQL: postgres (PostgreSQL) 9.4.4

Wait a minute

この記事では、ユーザの一覧を表示させます。
また、ユーザが増えた時のことを考えて、
ページネーションを実装します。

Index

AllUsers
|> View all users
|> Add All users link
|> Pagination
|> Refactoring
|> Minus page number
|> Extra

View all users

全てのユーザを表示することから始めます。
ファイル: web/controllers/user_controller.ex
indexアクション関数を追加。
def index(conn, _params) do
  users = Repo.all(User)
  render(conn, "index.html", users: users)
end
ユーザ一覧のページはサインインしていない状態では参照できないようにする。
plug :signed_in_user? when action in [:index, :show, :edit, :update]
ファイル: web/templates/user/index.html.eex
index.html.eexがないので作成します。
<h2>All users</h2>

<table class="table">
  <thead>
    <tr>
      <th>ProfileImage</th>
      <th>Name</th>
    </tr>
  </thead>
  <tbody>
<%= for user <- @users do %>
    <tr>
      <td>
        <div class="gravatar" style="float: left; margin-right: 10px;">
          <img src="<%= get_gravatar_url(user) %>" class="gravatar">
        </div>
      </td>
      <td><h4><%= user.name %><h4></td>
    </tr>
<% end %>
  </tbody>
</table>
これで全てのユーザを表示することはできました。
ユーザ一覧のページへのリンクを追加します。
ファイル: web/templates/layout/header.html.eex
以下のようにリンクを追加
<%= if current_user(@conn) do %>
  <li><%= link "Profile", to: user_path(@conn, :show, current_user(@conn).id) %><li>
  <li><%= link "All Users", to: user_path(@conn, :index) %><li>
  <li><%= link "Sign-out", to: session_path(@conn, :delete) %></li>
<% else %>
  <li><%= link "Sign-in", to: session_path(@conn, :new) %></li>
<% end %>

Pagination

ページネーションを実装します。
ライブラリの利用をします。
以下のページを参考にライブラリの利用準備をして下さい。
Phoenixでもページネーションがしたい!!(その2)
Caution:
もしかしたら、依存関係でぶつかるかもしれません。
その場合は慌てずにmix.exs、mix.lockの内容を書き換えて下さい。
私の場合、二つ依存関係が衝突したので書き換えをしました。
参考までに変更した内容を記述しておきます。
  • mix.lockのEctoをv0.14.1へ修正
  • mix.exsのpostgrexをv0.9.1へ修正
ファイル: web/models/user.ex
ページネーション用の関数を追加。
def paginate(params) do
  select_page = params["select_page"]

  if !select_page do
    select_page = @start_page
  end

  SampleApp.User
  |> order_by([u], asc: u.name)
  |> SampleApp.Repo.paginate(page: select_page, page_size: @page_size)
end
定数を定義。
(page_sizeの値は任意で変えて下さい)
@page_size 1
@start_page 1
ファイル: web/controllers/user_controller.ex
indexアクション関数を以下のように修正して下さい。
def index(conn, params) do
  page = SampleApp.User.paginate(params)

  render(conn, "index.html",
         users: page.entries,
         current_page: page.page_number,
         total_pages: page.total_pages,
         page_list: Range.new(1, page.total_pages))
end
ファイル: web/templates/user/index.html.eex
<h2>All users</h2>

<div>CurrentPage: <%= @current_page %></div>

<nav>
  <ul class="pagination">

  <!-- previous link -->
  <%= if @current_page > 1 do %>
    <li>
      <a href="<%= user_path(@conn, :index) %>?select_page=<%= @current_page - 1 %>" aria-label="Previous">
        <span aria-hidden="true">&laquo;</span>
      </a>
    </li>
  <% end %>

  <!-- page link -->
  <%= for page_number <- @page_list do %>
    <%= if page_number == @current_page do %>
      <li class="active">
        <a href="<%= user_path(@conn, :index) %>?select_page=<%= page_number %>">
          <%= page_number %><span class="sr-only">(current)</span>
        </a>
      </li>
    <% else %>
      <li><a href="<%= user_path(@conn, :index) %>?select_page=<%= page_number %>"><%= page_number %></a></li>
    <% end %>
  <% end %>

  <!-- next link -->
  <%= if @current_page < @total_pages do %>
    <li>
      <a href="<%= user_path(@conn, :index) %>?select_page=<%= @current_page + 1 %>" aria-label="Next">
        <span aria-hidden="true">&raquo;</span>
      </a>
    </li>
  <% end %>

  </ul>
</nav>

<table class="table">
  <thead>
    <tr>
      <th>Profile Image</th>
      <th>Name</th>
    </tr>
  </thead>
  <tbody>
  <%= for user <- @users do %>
    <tr>
      <td>
        <div class="gravatar" style="float: left; margin-right: 10px;">
          <img src="<%= get_gravatar_url(user) %>" class="gravatar">
        </div>
      </td>
      <td><h4><%= user.name %><h4></td>
    </tr>
  <% end %>
  </tbody>
</table>
URLを使って直接パラメータを送っている。
あまり賢いやり方ではないと思う。
より良い方法があれば改善しようと思います。

Refactoring

eexに直接記述している、
恥ずかしいURL部分を内部に隠ぺいしてしまいましょう。
ファイル: web/views/user_view.ex
関数を三つ追加します。
前のページのURLを取得。
def get_previous_page_url(conn, current_page) do
  get_page_url(conn, current_page - 1)
end
次のページのURLを取得。
def get_next_page_url(conn, current_page) do
  get_page_url(conn, current_page + 1)
end
ページ番号で指定されたページのURLを取得。
def get_page_url(conn, page_number) do
  "#{user_path(conn, :index)}?select_page=#{page_number}"
end
ファイル: web/templates/user/index.html.eex
<h2>All users</h2>

<div>CurrentPage: <%= @current_page %></div>

<nav>
  <ul class="pagination">

  <!-- previous link -->
  <%= if @current_page > 1 do %>
    <li>
      <a href="<%= get_previous_page_url(@conn, @current_page) %>" aria-label="Previous">
        <span aria-hidden="true">&laquo;</span>
      </a>
    </li>
  <% end %>

  <!-- page link -->
  <%= for page_number <- @page_list do %>
    <%= if page_number == @current_page do %>
      <li class="active">
        <a href="<%= get_page_url(@conn, page_number) %>">
          <%= page_number %><span class="sr-only">(current)</span>
        </a>
      </li>
    <% else %>
      <li><a href="<%= get_page_url(@conn, page_number) %>"><%= page_number %></a></li>
    <% end %>
  <% end %>

  <!-- next link -->
  <%= if @current_page < @total_pages do %>
    <li>
      <a href="<%= get_next_page_url(@conn, @current_page) %>" aria-label="Next">
        <span aria-hidden="true">&raquo;</span>
      </a>
    </li>
  <% end %>

  </ul>
</nav>

<table class="table">
  <thead>
    <tr>
      <th>Profile Image</th>
      <th>Name</th>
    </tr>
  </thead>
  <tbody>
  <%= for user <- @users do %>
    <tr>
      <td>
        <div class="gravatar" style="float: left; margin-right: 10px;">
          <img src="<%= get_gravatar_url(user) %>" class="gravatar">
        </div>
      </td>
      <td><h4><%= user.name %><h4></td>
    </tr>
  <% end %>
  </tbody>
</table>
これで大分ましになりました。

Minus page number

さてまだ問題が残っています。
マイナスの値を直接入力されたらどうなるでしょうか?
勿論、実行時エラーが発生します。
それに対応します。
ファイル: web/models/user.ex
params[“select_page”]がnilか判定。
def is_nil_page?(params) do
  params["select_page"] == nil
end
params[“select_page”]がマイナスか判定。
def is_minus_page_number?(params) do
  String.to_integer(params["select_page"]) < @start_page
end
外で判定の処理をするので、ifの部分を削除。
def paginate(params) do
  select_page = params["select_page"]

  SampleApp.User
  |> order_by([u], asc: u.name)
  |> SampleApp.Repo.paginate(page: select_page, page_size: @page_size)
end
ファイル: web/controllers/user_controller.ex
以下のように修正。
def index(conn, params) do
  if !SampleApp.User.is_nil_page?(params) && !SampleApp.User.is_minus_page_number?(params) do
    page = SampleApp.User.paginate(params)
  end

  if page do
    render(conn, "index.html",
           users: page.entries,
           current_page: page.page_number,
           total_pages: page.total_pages,
           page_list: Range.new(1, page.total_pages))
  else
    conn
    |> put_flash(:error, "Invalid page number!!")
    |> redirect(to: static_pages_path(conn, :home))
  end
end
Caution:
paramsのselect_pageがマイナス値だとpostgrexからエラーが発生する。
Description:
リダイレクト先がstatic_pagesのhomeなのは、
同じページにリダイレクトするとリダイレクトループになるため。
ファイル: web/views/layout_view.ex
def user_index_first_page(conn) do
  "#{user_path(conn, :index)}?select_page=1"
end
ファイル: web/templates/layout/header.html.eex
変更前
<li><%= link "All Users", to: user_path(@conn, :index) %><li>
変更後
<li><%= link "All Users", to: user_index_first_page(@conn) %><li>
修正が多い・・・
これでマイナス値を直接URLの値へ叩き込まれても大丈夫でしょう。

Extra

Ecto.Repoですごく使い勝手の良い関数を見つけました。
get_by/3と言う関数です。
参考: hexdocs - Ecto.Repo v0.14.2
iex起動
>iex -S mix
>alias SampleApp.User
nil
検索キーを:nameで指定。
hugeの値を検索している。
iex(1)> SampleApp.Repo.get_by(User, name: "huge")
[debug] SELECT u0."id", u0."name", u0."email", u0."password_digest", u0."inserted_at", u0."updated_at" FROM "users" AS u0 WHERE (u0."name" = $1) ["huge"] OK query=604.0ms queue=3.0
ms
%SampleApp.User{__meta__: %Ecto.Schema.Metadata{source: "users",
  state: :loaded}, email: "huge@huge.com", id: 1,
 inserted_at: #Ecto.DateTime<2015-07-22T07:07:17Z>, name: "huge", password: nil,
 password_digest: "bDA1bThpSk5IUmlCUEFEekx6U0w2Zz09LS1HT1NxUGY2TVRKenFrSjlrWVNmejhBPT0=--5334DAFFB7EAF18D4C85CDCBC4DBC6778BD5F370",
 updated_at: #Ecto.DateTime<2015-07-22T07:59:15Z>}
検索キーを:emailで指定。
hugeの部分一致ができるか確認。
(できなかった)
iex(2)> SampleApp.Repo.get_by(User, email: "huge")
[debug] SELECT u0."id", u0."name", u0."email", u0."password_digest", u0."inserted_at", u0."updated_at" FROM "users" AS u0 WHERE (u0."email" = $1) ["huge"] OK query=1.0ms
nil
検索キーを:emailで指定。
huge@huge.comの値を検索している。
iex(3)> SampleApp.Repo.get_by(User, email: "huge@huge.com")
[debug] SELECT u0."id", u0."name", u0."email", u0."password_digest", u0."inserted_at", u0."updated_at" FROM "users" AS u0 WHERE (u0."email" = $1) ["huge@huge.com"] OK query=1.0ms
%SampleApp.User{__meta__: %Ecto.Schema.Metadata{source: "users",
  state: :loaded}, email: "huge@huge.com", id: 1,
 inserted_at: #Ecto.DateTime<2015-07-22T07:07:17Z>, name: "huge", password: nil,
 password_digest: "bDA1bThpSk5IUmlCUEFEekx6U0w2Zz09LS1HT1NxUGY2TVRKenFrSjlrWVNmejhBPT0=--5334DAFFB7EAF18D4C85CDCBC4DBC6778BD5F370",
 updated_at: #Ecto.DateTime<2015-07-22T07:59:15Z>}
これができると何ができるのかと言うと、
今までメールアドレスでの検索を行うために、
Ecto.QueryでSQLライクなものを生成していた。
しかし上記の関数を使えば、そのような複雑なことをしなくても
一行でDBからデータが取得できる。これは便利だ。
なのでUserModelの関数を変更する。
ファイル: web/models/user.ex
変更前
def find_user_from_email(email) do
  query = from user in SampleApp.User,
          where: user.email == ^email,
          select: user
  SampleApp.Repo.all(query) |> List.first
end
変更後
def find_user_from_email(email) do
  SampleApp.Repo.get_by(SampleApp.User, email: email)
end
うん。すっきりした。

Speaking to oneself

あ~こんだけの処理を作るのにえらく時間かかりました。
ページネーションを実装するのはひどく面倒です。
どこの誰が理解してるって?
まったくどの口(記事)で言ってたんだか・・・(笑)

Bibliography

人気の投稿