スポンサーリンク

2015年8月7日

[Elixir+Phoenix]many to many (Part 2)

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
Caution:
さりげなく、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

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はリストから文字列へ変換する。
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の値を組み込んでいるところ。
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
Ectoでpreparedを使った場合、SQLインジェクション対策されているのか書いてあります。
参考: 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アクションを以下のように修正して下さい。
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の関数を使って取得するデータとは形式が異なります。
現在:
%{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の関数を使えば何とかなるでしょう~(楽観)
構造体は既に用意してありますね。
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を使った方がいいかな?)
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に変換しています。
パターンマッチの基本を知りたい方は、以前記事を書いています。
よかったら参考にして下さい。
参考: パターンマッチの基本を習得する
後は、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を使って変換してやりましょう。
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の場合の対応とかはやってませんので、あしからず。
後は、サーバを起動してユーザのshow画面を見てみて下さい。
フォローしているユーザのマイクロポストも表示されます。

Extra

order_byで投稿された時系列的に並び替えましょう。
ファイル: web/models/micropost.ex
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

これで、第十一章の面倒くさいところが大体終わりましたね。
これを反映して、第十一章に取り掛かるとしましょう。
記事中で重複してしまう部分も出てくると思いますが、
あくまで検証用にやったことなので、勘弁して下さい。

Bibliography

人気の投稿