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
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
|> Add All users link
|> Pagination
|> Refactoring
|> Minus page number
|> Extra
View all users
全てのユーザを表示することから始めます。
ファイル: web/controllers/user_controller.ex
indexアクション関数を追加。
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がないので作成します。
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>
これで全てのユーザを表示することはできました。
Add All users link
ユーザ一覧のページへのリンクを追加します。
ファイル: 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
ページネーションを実装します。
Caution:
もしかしたら、依存関係でぶつかるかもしれません。
その場合は慌てずにmix.exs、mix.lockの内容を書き換えて下さい。
私の場合、二つ依存関係が衝突したので書き換えをしました。
もしかしたら、依存関係でぶつかるかもしれません。
その場合は慌てずに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の値は任意で変えて下さい)
@page_size 1
@start_page 1
ファイル: web/controllers/user_controller.ex
indexアクション関数を以下のように修正して下さい。
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">«</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">»</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部分を内部に隠ぺいしてしまいましょう。
恥ずかしい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">«</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">»</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からエラーが発生する。
paramsのselect_pageがマイナス値だとpostgrexからエラーが発生する。
Description:
リダイレクト先がstatic_pagesのhomeなのは、
同じページにリダイレクトするとリダイレクトループになるため。
リダイレクト先が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の値へ叩き込まれても大丈夫でしょう。
これでマイナス値を直接URLの値へ叩き込まれても大丈夫でしょう。
Extra
iex起動
>iex -S mix
>alias SampleApp.User
nil
検索キーを:nameで指定。
hugeの値を検索している。
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の部分一致ができるか確認。
(できなかった)
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の値を検索している。
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ライクなものを生成していた。
今までメールアドレスでの検索を行うために、
Ecto.QueryでSQLライクなものを生成していた。
しかし上記の関数を使えば、そのような複雑なことをしなくても
一行でDBからデータが取得できる。これは便利だ。
一行でDBからデータが取得できる。これは便利だ。
なのでUserModelの関数を変更する。
ファイル: web/models/user.ex
ファイル: 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
あ~こんだけの処理を作るのにえらく時間かかりました。
ページネーションを実装するのはひどく面倒です。
ページネーションを実装するのはひどく面倒です。
どこの誰が理解してるって?
まったくどの口(記事)で言ってたんだか・・・(笑)
まったくどの口(記事)で言ってたんだか・・・(笑)