スポンサーリンク

2015年8月5日

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

Goal

“Ecto.Schema.has_many/3 :through” の動作を検証する。

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

フォロー機能の実装を行うために、
Ecto.Schema.has_many/3 :through の動作を検証します。
検証は段階に分けて全2記事で行う。
Ectoの参考記事は以下のリンク先。
yoavltさん、素晴らしい記事をありがとうございます!参考にさせて頂きます!!
参考: Qiita - Elixir Phoenixのデータベース操作モジュールEcto入門 多対多
フォロー機能の参考記事は以下のリンク先。
h3potetoさん、素晴らしい記事をありがとうございます!参考にさせて頂きます!!
参考: PartyIX - has_many throughで,class_nameとかforeign_keyをちゃんと復習してみる

Index

Many to many
|> Preparation
|> Get an intermediate table

Preparation

まず準備。新しく検証用のプロジェクトを作成。
>cd プロジェクト作成ディレクトリ
>mix phoenix.new followed_users
... (選択肢はyes)
>cd followed_users
>mix ecto.create
>mix phoenix.server
>ctrl+c
以降、プロジェクトと言えば、
followed_usersを指し示す。
データモデルは以下。
  • ユーザのデータモデル
    • モデル: User
    • テーブル: users
    • 生成カラム: name:string, email:string
    • 自動カラム: id:integer, inserted_at:timestamp, updated_at:timestamp
  • 中間テーブルのデータモデル
    • モデル: Relationship
    • テーブル: relationships
    • 生成カラム: follower_id:integer, followed_id:integer
    • 自動カラム: id:integer, inserted_at:timestamp, updated_at:timestamp
    • インデックス: follower_id, followed_id, follower_idとfollowed_idでの複合インデックス(ユニーク)
Description:
インデックスについては、hexdocs - v0.14.3 Ecto.Migration.index/3を参照して下さい。
Caution:
インデックスがいらない人はやらなくて問題ないです。
私自身が必要になるので、ついでに検証しています。
自動生成でソースコードを生成。
ファイル: web/router.ex
ルーティングは以下のようになる。
scope "/", FollowedUsers do
  pipe_through :browser # Use the default browser stack

  get "/", PageController, :index
  resources "/users", UserController
  resources "/relationships", RelationshipController
end
  • User
>mix phoenix.gen.html User users name:string email:string
示されたルーティングを追加する。
  • Relationship
>mix phoenix.gen.html Relationship relationships follower_id:integer followed_id:integer
示されたルーティングを追加する。
ファイル: priv/repo/timestamp_create_relationship.exs
インデックスを追加。
defmodule FollowedUsers.Repo.Migrations.CreateRelationship do
  use Ecto.Migration
  @disable_ddl_transaction true

  def change do
    create table(:relationships) do
      add :follower_id, :integer
      add :followed_id, :integer

      timestamps
    end

    create index(:relationships, [:follower_id], concurrently: true)
    create index(:relationships, [:followed_id], concurrently: true)
    create index(:relationships, [:follower_id, :followed_id], unique: true, concurrently: true)
  end
end
マイグレーションの実行。
>mix ecto.migrate
...

12:20:31.904 [info]  == Running FollowedUsers.Repo.Migrations.CreateUser.change/0 forward

12:20:31.904 [info]  create table users

12:20:31.932 [info]  == Migrated in 0.2s

12:20:31.982 [info]  == Running FollowedUsers.Repo.Migrations.CreateRelationship.change/0 forward

12:20:31.982 [info]  create table relationships

12:20:31.999 [info]  create index relationships_follower_id_index

12:20:32.010 [info]  create index relationships_followed_id_index

12:20:32.020 [info]  create index relationships_follower_id_followed_id_index

12:20:32.028 [info]  == Migrated in 0.4s
これで準備完了。

Get an intermediate table

中間テーブルとの関係を構築します。
ファイル: web/models/user.ex
Userのschemaを以下のように編集する。
schema "users" do
  field :name, :string
  field :email, :string

  # User who follow
  has_many :followed_users, FollowedUsers.Relationship, foreign_key: :follower_id
  has_many :relationships, through: [:followed_users, :followed_user]

  # Followers the user
  has_many :followers, FollowedUsers.Relationship, foreign_key: :followed_id
  has_many :reverse_relationships, through: [:followers, :follower]

  timestamps
end
Description:
逆転していて分かり辛くなっていると思うので、説明を書いておきます。
followed_users: フォローしているユーザ
follower_idをキーにして、フォローしているユーザを取得している。
has_many :followed_users, FollowedUsers.Relationship, foreign_key: :follower_id
has_many :relationships, through: [:followed_users, :followed_user]
followers: フォロワーのユーザ
followed_idをキーにして、フォローされているユーザを取得している。
has_many :followers, FollowedUsers.Relationship, foreign_key: :followed_id
has_many :reverse_relationships, through: [:followers, :follower]
ファイル: web/models/relationship.ex
Relationshipのschemaを以下のように編集する。
schema "relationships" do
  belongs_to :followed_user, FollowedUsers.User, foreign_key: :follower_id
  belongs_to :follower, FollowedUsers.User, foreign_key: :followed_id

  timestamps
end
テスト用データを適当に画面から用意して、
iex上でデータ取得ができるか試してみましょう。
(二件のユーザと一件のリレーションシップが最低)
iex起動。
何回も書くのが面倒くさいので、aliasを定義。
>iex -S mix
iex(1)> alias FollowedUsers.User
nil
iex(2)> alias FollowedUsers.Relationship
nil
iex(3)> alias FollowedUsers.Repo
nil
iex(4)> import Ecto.Query
nil
データを取得してみる。
iex(5)> [user] = Repo.all(from(u in User, where: u.id == 1, preload: :relationships, preload: :reverse_relationships))
Description:
以下のやり方でも取得できる。
user = Repo.get(User, 1) |> Repo.preload(:relationships) |> Repo.preload(:reverse_relationships)
フォローしているユーザのidを取得。
iex(7)> for followed_user <- user.followed_users do
...(7)> IO.puts followed_user.followed_id
...(7)> end
フォローされているユーザのidを取得。
iex(6)> for follower <- user.followers do
...(6)> IO.puts follower.follower_id
...(6)> end

Speaking to oneself

多対多の関係性に対して認識が間違えてないか不安になってきた。
それはさておき、これでようやっと準備が整いました。
次は、マイクロポストに紐づくように関係の構築をしていきます。
続きの記事: many to many (Part 2)

Bibliography

人気の投稿