Goal
フォローしているユーザのマイクロポストを取得する。
Dev-Environment
OS: Windows8.1
Erlang: Eshell V6.4, OTP-Version 17.5
Elixir: v1.0.4
Phoenix Framework: v0.15
PostgreSQL: postgres (PostgreSQL) 9.4.4
Erlang: Eshell V6.4, OTP-Version 17.5
Elixir: v1.0.4
Phoenix Framework: v0.15
PostgreSQL: postgres (PostgreSQL) 9.4.4
Caution:
さりげなく、Phoenixのバージョン上がっているので注意!!
archiveの方をバージョンアップしてたの忘れていました。
Tutorialの方はv0.13.1なので・・・
さりげなく、Phoenixのバージョン上がっているので注意!!
archiveの方をバージョンアップしてたの忘れていました。
Tutorialの方はv0.13.1なので・・・
Wait a minute
前回に引き続いて動作検証をしていきます。
前回の記事を見ていない方は準備もあるので、そちらを先に参照して下さい。
前回の記事を見ていない方は準備もあるので、そちらを先に参照して下さい。
今回検証することは・・・
フォローしているユーザのマイクロポストを取得する方法を検証します。
フォローしているユーザのマイクロポストを取得する方法を検証します。
第十一章の山場になっている部分を事前に検証して、
記事にするときはサクッと終わらせてしまおうと言う魂胆です。
記事にするときはサクッと終わらせてしまおうと言う魂胆です。
Index
Many to many
|> Tested on iex
|> Preparation
|> Get followed_user_ids
|> Subquery (Use IN Operator)
|> Extra
|> Tested on iex
|> Preparation
|> Get followed_user_ids
|> Subquery (Use IN Operator)
|> Extra
Tested on iex
iexを使って今回やりたいことをテストする。
但し、マイクロポストを作ってないので、ユーザを使って。
但し、マイクロポストを作ってないので、ユーザを使って。
こんなことをやりたい。
aliasとimportを先にしておく。
iex> alias FollowedUsers.User
nil
iex> alias FollowedUsers.Relationship
nil
iex> alias FollowedUsers.Repo
nil
iex> import Ecto.Query
nil
ユーザのデータを取得する。
iex> user = Repo.get(User, 1) |> Repo.preload(:relationships) |> Repo.preload(:reverse_relationships)
フォローしているユーザのIDだけ抽出してリストにする。
また、抽出したIDはリストから文字列へ変換する。
また、抽出したIDはリストから文字列へ変換する。
iex> ids_list = for followed_user <- user.followed_users do Map.get(followed_user, :followed_id) end
iex> string_ids = Enum.join(ids_list, ",")
"2,3"
SQLクエリを作成する。
肝は、Elixirの#{string_ids}、#{user.id}で文字列にElixirの値を組み込んでいるところ。
肝は、Elixirの#{string_ids}、#{user.id}で文字列にElixirの値を組み込んでいるところ。
iex> sql_query = "SELECT * FROM users WHERE id IN (#{string_ids}) OR id = #{user.id};"
"SELECT * FROM users WHERE id IN (2,3) OR id = 1;"
DBからデータを取得してみる。
iex> Ecto.Adapters.SQL.query(Repo, sql_query, [])
[debug] SELECT * FROM users WHERE id IN (2,3) OR id = 1; [] OK query=0.0ms
%{columns: ["id", "name", "email", "inserted_at", "updated_at"],
command: :select, num_rows: 3,
rows: [[1, "hoge", "hoge@hoge.com", {{2015, 8, 5}, {3, 57, 46, 0}},
{{2015, 8, 5}, {3, 57, 46, 0}}],
[2, "huge", "huge@huge.com", {{2015, 8, 5}, {3, 57, 55, 0}},
{{2015, 8, 5}, {3, 57, 55, 0}}],
[3, "foo", "foo@foo.com", {{2015, 8, 5}, {3, 58, 6, 0}},
{{2015, 8, 5}, {3, 58, 6, 0}}]]}
問題ないですね。
それでは、上記の結果をソースコードに落とし込みましょう。
それでは、上記の結果をソースコードに落とし込みましょう。
Caution:
私の場合なのですが、実際にこれを使うアプリケーションでは取得に際して、
ユーザが入力を促すことも、パラメータを送ることもしません。
私の場合なのですが、実際にこれを使うアプリケーションでは取得に際して、
ユーザが入力を促すことも、パラメータを送ることもしません。
なので、SQLインジェクションを対策できるpreparedは使いません。
(今回の場合はですが・・・)
(今回の場合はですが・・・)
もしSQLインジェクション対策をしたいと言う方がいましたら、
以下のリンク先を見てもらえれば、大体分かると思います。
以下のリンク先を見てもらえれば、大体分かると思います。
preparedのやり方が書いてあります。
参考: stackoverflow - Prepared statements with Postgrex & Ecto
参考: stackoverflow - Prepared statements with Postgrex & Ecto
Ectoでpreparedを使った場合、SQLインジェクション対策されているのか書いてあります。
参考: Github - elixir-lang/ecto Use prepared statements when building queries #180
参考: Github - elixir-lang/ecto Use prepared statements when building queries #180
Preparation
前回準備終わったって言わなかったっけ?
あれ?そうでしたっけ?
あれ?そうでしたっけ?
すいませんがお付き合いくだせー
マイクロポストを作らないと検証できないので・・・
マイクロポストを作らないと検証できないので・・・
マイクロポストを生成する。
>mix phoenix.gen.html Micropost microposts content:string user_id:integer
ルーティングの追加をして下さい。
マイグレーションを実行。
>mix ecto.migrate
準備良し!
テストデータは適当に入れておいて下さい。
テストデータは適当に入れておいて下さい。
Get followed_user_ids
取得したDBデータから、フォローしているユーザのidだけを抽出します。
ファイル: web/models/user.ex
以下の関数を追加。
以下の関数を追加。
def to_followed_user_ids_list(followed_users) do
for followed_user <- followed_users do Map.get(followed_user, :followed_id) end
end
def get_followed_user_ids(user) do
user |> to_followed_user_ids_list |> Enum.join(",")
end
Subquery (Use IN Operator)
副問合せを利用して、フォローしているユーザと自分のマイクロポストを取得します。
SQL自体はこんな感じになりますね。
SELECT *
FROM microposts
WHERE user_id IN (followed_user_ids) OR user_id = signin_id;
ファイル: web/models/micropost.ex
以下の関数を追加。
以下の関数を追加。
def from_users_followed_by(user_id, followed_user_ids) do
sql_query = "SELECT * FROM microposts WHERE user_id IN (#{followed_user_ids}) OR user_id = #{user_id};"
Ecto.Adapters.SQL.query(FollowedUsers.Repo, sql_query, [])
end
ファイル: web/controllers/user_controller.ex
showアクションを以下のように修正して下さい。
showアクションを以下のように修正して下さい。
def show(conn, %{"id" => id}) do
user = Repo.get(User, id) |> Repo.preload(:relationships) |> Repo.preload(:reverse_relationships)
microposts = Micropost.from_users_followed_by(user.id, User.get_followed_user_ids(user.followed_users))
render(conn, "show.html", user: user, microposts: microposts)
end
ファイル: web/templates/user/show.html.eex
テンプレートに以下の記述を追加。
テンプレートに以下の記述を追加。
<%= if @microposts do %>
<%= for micropost <- @microposts do %>
<div>Content: <%= micropost.content %></div>
<% end %>
<% end %>
さてここで一つ問題です。
このままでは動きません。
このままでは動きません。
(私の場合・・・)
取得したデータですが以下のようになっています。
Ectoの関数を使って取得するデータとは形式が異なります。
取得したデータですが以下のようになっています。
Ectoの関数を使って取得するデータとは形式が異なります。
現在:
%{columns: ["id", "content", "user_id", "inserted_at", "updated_at"],
command: :select,
num_rows: 4,
rows: [[1, "hoge", 1, {{2015, 8, 7}, {3, 30, 13, 0}}, {{2015, 8, 7}, {3, 30, 13, 0}}],
[2, "hogehoge", 1, {{2015, 8, 7}, {3, 30, 52, 0}}, {{2015, 8, 7}, {3, 30, 52, 0}}],
[3, "huge", 2, {{2015, 8, 7}, {3, 31, 0, 0}}, {{2015, 8, 7}, {3, 31, 0, 0}}],
[4, "foo", 3, {{2015, 8, 7}, {3, 31, 10, 0}}, {{2015, 8, 7}, {3, 31, 10, 0}}]]}
このままでは、eexテンプレートで使いづらいですね。
だから整形します。
だから整形します。
こういった形になれば使いやすくなりますね。
理想:
[%{id: 1, content: "hoge", user_id: 1, inserted_at: {{2015, 8, 7}, {3, 30, 13, 0}}, updated_at: {{2015, 8, 7}, {3, 30, 13, 0}}},
%{id: 2, content: "hogehoge", user_id: 1, inserted_at: {{2015, 8, 7}, {3, 30, 13, 0}}, updated_at: {{2015, 8, 7}, {3, 30, 13, 0}}},
%{id: 3, content: "huge", user_id: 2, inserted_at: {{2015, 8, 7}, {3, 31, 0, 0}}, updated_at: {{2015, 8, 7}, {3, 31, 0, 0}}},
%{id: 4, content: "foo", user_id: 3, inserted_at: {{2015, 8, 7}, {3, 31, 10, 0}}, updated_at: {{2015, 8, 7}, {3, 31, 10, 0}}}]
iex上で整形を試してみます。
この時点での予想ですが、Enumの関数を使えば何とかなるでしょう。
足りなければ、ListかMapの関数を使えば何とかなるでしょう~(楽観)
この時点での予想ですが、Enumの関数を使えば何とかなるでしょう。
足りなければ、ListかMapの関数を使えば何とかなるでしょう~(楽観)
構造体は既に用意してありますね。
iex> %FollowedUsers.Micropost{}
%FollowedUsers.Micropost{
__meta__: %Ecto.Schema.Metadata{source: {nil, "microposts"}, state: :built},
content: nil, id: nil, inserted_at: nil, updated_at: nil, user_id: nil}
データの入っている順番も分かっています。
columns: ["id", "content", "user_id", "inserted_at", "updated_at"]
ならば、取得結果から値を抽出してMicropost構造体に値を束縛。
そして、リストに追加すればできますね。
そして、リストに追加すればできますね。
値の抽出にはパターンマッチを使います。
パターンマッチすげぇ~(笑)
(Enum.eachを使った方がいいかな?)
パターンマッチすげぇ~(笑)
(Enum.eachを使った方がいいかな?)
iex> for row <- result.rows do
...> {id, content, user_id, inserted_at, updated_at} = List.to_tuple(row)
...> end
Description:
パターンマッチは異なる型やサイズではできません。
今回は、Listの値をTupleにマッチさせたいのです。
だから、List.to_tuple/1でTupleに変換しています。
パターンマッチは異なる型やサイズではできません。
今回は、Listの値をTupleにマッチさせたいのです。
だから、List.to_tuple/1でTupleに変換しています。
後は、Tupleの値をマイクロポストの構造体へ束縛させればいいですね。
iex> for row <- result.rows do
...> {id, content, user_id, inserted_at, updated_at} = List.to_tuple(row)
...> %FollowedUsers.Micropost{id: id, content: content, user_id: user_id, inserted_at: inserted_at, updated_at: updated_at}
...> end
おっと、実行結果を見て下さい!
丁度良く、リストになって戻ってきています!!
丁度良く、リストになって戻ってきています!!
[%FollowedUsers.Micropost{__meta__: %Ecto.Schema.Metadata{source: {nil,
"microposts"}, state: :built}, content: "hoge", id: 1,
inserted_at: {{2015, 8, 7}, {3, 30, 13, 0}},
updated_at: {{2015, 8, 7}, {3, 30, 13, 0}}, user_id: 1},
%FollowedUsers.Micropost{__meta__: %Ecto.Schema.Metadata{source: {nil,
"microposts"}, state: :built}, content: "hogehoge", id: 2,
inserted_at: {{2015, 8, 7}, {3, 30, 52, 0}},
updated_at: {{2015, 8, 7}, {3, 30, 52, 0}}, user_id: 1},
%FollowedUsers.Micropost{__meta__: %Ecto.Schema.Metadata{source: {nil,
"microposts"}, state: :built}, content: "huge", id: 3,
inserted_at: {{2015, 8, 7}, {3, 31, 0, 0}},
updated_at: {{2015, 8, 7}, {3, 31, 0, 0}}, user_id: 2},
%FollowedUsers.Micropost{__meta__: %Ecto.Schema.Metadata{source: {nil,
"microposts"}, state: :built}, content: "foo", id: 4,
inserted_at: {{2015, 8, 7}, {3, 31, 10, 0}},
updated_at: {{2015, 8, 7}, {3, 31, 10, 0}}, user_id: 3}]
しかし、まだやることがありますね?
もう面倒くさい?そんなこと言わず付き合って下さい。
もう面倒くさい?そんなこと言わず付き合って下さい。
inserted_atとupdated_atの値がこのままでは使えません。
Ecto.DateTimeを使って変換してやりましょう。
Ecto.DateTimeを使って変換してやりましょう。
Example:
iex> Ecto.DateTime.cast({{2015, 8, 7}, {3, 30, 52, 0}})
{:ok, #Ecto.DateTime<2015-08-07T03:30:52Z>}
これをソースコードに落とし込んでいきます。
ファイル: web/models/micropost.ex
関数を追加するモジュールとして妥当かは分かりませんが、
今回は以下の関数をマイクロポストへと追加します。
今回は以下の関数をマイクロポストへと追加します。
defp cast_date_time_tuple(date_time_tuple) do
result = Ecto.DateTime.cast(date_time_tuple)
case result do
{:ok, date_time} -> date_time
_ -> nil
end
end
defp sql_result_to_microposts(result) do
for row <- result.rows do
{id, content, user_id, inserted_at, updated_at} = List.to_tuple(row)
%FollowedUsers.Micropost{
id: id, content: content, user_id: user_id,
inserted_at: cast_date_time_tuple(inserted_at),
updated_at: cast_date_time_tuple(updated_at)}
end
end
関数を以下のように修正します。
def from_users_followed_by(user_id, followed_user_ids) do
sql_query = "SELECT * FROM microposts WHERE user_id IN (#{followed_user_ids}) OR user_id = #{user_id};"
result = Ecto.Adapters.SQL.query(FollowedUsers.Repo, sql_query, [])
sql_result_to_microposts(result)
end
iexから動作するかテストしてみましょう。
Example:
iex> FollowedUsers.Micropost.cast_date_time_tuple({{2015, 8, 7}, {3, 30, 52, 0}})
#Ecto.DateTime<2015-08-07T03:30:52Z>
Example:
iex> FollowedUsers.Micropost.from_users_followed_by(1, "2,3")
[%FollowedUsers.Micropost{__meta__: %Ecto.Schema.Metadata{source: {nil,
"microposts"}, state: :built}, content: "hoge", id: 1,
inserted_at: #Ecto.DateTime<2015-08-07T03:30:13Z>,
updated_at: #Ecto.DateTime<2015-08-07T03:30:13Z>, user_id: 1},
...
Caution:
nilの場合の対応とかはやってませんので、あしからず。
nilの場合の対応とかはやってませんので、あしからず。
後は、サーバを起動してユーザのshow画面を見てみて下さい。
フォローしているユーザのマイクロポストも表示されます。
フォローしているユーザのマイクロポストも表示されます。
Extra
order_byで投稿された時系列的に並び替えましょう。
ファイル: web/models/micropost.ex
SQLクエリを以下のように修正して下さい。
SQLクエリを以下のように修正して下さい。
sql_query = "SELECT * FROM microposts WHERE user_id IN (#{followed_user_ids}) OR user_id = #{user_id} ORDER BY inserted_at DESC;"
これで投稿日時の最新順に並びます。
Speaking to oneself
これで、第十一章の面倒くさいところが大体終わりましたね。
これを反映して、第十一章に取り掛かるとしましょう。
これを反映して、第十一章に取り掛かるとしましょう。
記事中で重複してしまう部分も出てくると思いますが、
あくまで検証用にやったことなので、勘弁して下さい。
あくまで検証用にやったことなので、勘弁して下さい。