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
RelationshipModel
|> Create relationship model
|> User/Relationship of association
|> Validation
|> Utility Methods
|> Extra
|> Create relationship model
|> User/Relationship of association
|> Validation
|> Utility Methods
|> Extra
Create relationship model
まずは、中間テーブルとして機能させる、Relationshipモデルを作成していきます。
データモデルは以下のようになります。
- 中間テーブルのデータモデル
- モデル: Relationship
- テーブル: relationships
- 生成カラム: follower_id:integer, followed_id:integer
- 自動カラム: id:integer, inserted_at:timestamp, updated_at:timestamp
- インデックス: follower_id, followed_id, follower_idとfollowed_idでの複合インデックス(ユニーク)
自動生成のコマンドで生成。
>mix phoenix.gen.model Relationship relationships follower_id:integer followed_id:integer
ファイル: priv/repo/[timestamp]_create_relationship.exs
マイグレーションファイルの編集。
マイグレーションファイルの編集。
defmodule SampleApp.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
Description:
さて、複合インデックスにユニークを指定している理由ですが、
フォローしているのに、またフォローができたらおかしなことになりますね。
それを防止するために、ユニークを指定しています。
さて、複合インデックスにユニークを指定している理由ですが、
フォローしているのに、またフォローができたらおかしなことになりますね。
それを防止するために、ユニークを指定しています。
マイグレーションの実行。
>mix ecto.migrate
User/Relationship of association
User/Relationshipモデルの関連付けを行います。
ファイル: web/models/user.ex
Userのschemaを以下のように編集する。
Userのschemaを以下のように編集する。
schema "users" do
field :name, :string
field :email, :string
field :password_digest, :string
field :password, :string, virtual: true
has_many :microposts, SampleApp.Micropost
# User who follow
has_many :followed_users, SampleApp.Relationship, foreign_key: :follower_id
has_many :relationships, through: [:followed_users, :followed_user]
# Followers the user
has_many :followers, SampleApp.Relationship, foreign_key: :followed_id
has_many :reverse_relationships, through: [:followers, :follower]
timestamps
end
Description:
逆転していて分かり辛くなっていると思うので、説明を書いておきます。
逆転していて分かり辛くなっていると思うので、説明を書いておきます。
follower_id: フォローしているユーザ自身のid
followed_id: フォローしている相手ユーザのid
followed_id: フォローしている相手ユーザのid
follower_idを検索キーにして、自分自身”が”フォローしているユーザのidを取得している。
has_many :followed_users, SampleApp.Relationship, foreign_key: :follower_id
has_many :relationships, through: [:followed_users, :followed_user]
followed_idを検索キーにして、自分自身”を”フォローしているユーザのidを取得している。
(followed_idに自分自身のidを指定すれば、自分自身をフォローしているユーザが取得できる)
(followed_idに自分自身のidを指定すれば、自分自身をフォローしているユーザが取得できる)
has_many :followers, SampleApp.Relationship, foreign_key: :followed_id
has_many :reverse_relationships, through: [:followers, :follower]
ファイル: web/models/relationship.ex
Relationshipのschemaを以下のように編集する。
Relationshipのschemaを以下のように編集する。
schema "relationships" do
belongs_to :followed_user, SampleApp.User, foreign_key: :follower_id
belongs_to :follower, SampleApp.User, foreign_key: :followed_id
timestamps
end
Validation
changesetでバリデーションを行いたいと思います。
実施するバリデーションは存在性(presence)です。
確か同じことをUserモデルでもやっていましたね。
確か同じことをUserモデルでもやっていましたね。
同じ機能の関数を定義はしたくありません。
なので、補助用のヘルパーモジュールでも用意するとしましょう。
なので、補助用のヘルパーモジュールでも用意するとしましょう。
ファイル: lib/helpers/validate_helper.ex
モジュールを新しく定義。
モジュールを新しく定義。
defmodule SampleApp.Helpers.ValidateHelper do
# my presence check validation
def validate_presence(changeset, field_name) do
field_data = Ecto.Changeset.get_field(changeset, field_name)
cond do
field_data == nil ->
add_error changeset, field_name, "#{field_name} is nil"
field_data == "" ->
add_error changeset, field_name, "No #{field_name}"
true ->
changeset
end
end
end
ファイル: web/web.ex
全モデルで使えるようにweb.exのmodelにimportを追加。
全モデルで使えるようにweb.exのmodelにimportを追加。
def model do
quote do
use Ecto.Model
# My validate helper
import SampleApp.Helpers.ValidateHelper
end
end
ファイル: web/models/relationship.ex
chagesetにバリデーションを追加。
chagesetにバリデーションを追加。
def changeset(model, params \\ :empty) do
model
|> cast(params, @required_fields, @optional_fields)
|> validate_presence(:followed_user)
|> validate_presence(:follower)
end
ファイル: web/models/user.ex
User.validate_presence/2は削除して下さい。
User.validate_presence/2は削除して下さい。
Utility Methods
フォローしたり、フォローを解除するための補助関数を用意します。
ファイル: web/models/relationship.ex
以下の関数を追加。
以下の関数を追加。
フォローする。
def follow!(signed_id, follow_user_id) do
changeset = SampleApp.Relationship.changeset(
%SampleApp.Relationship{}, %{follower_id: signed_id, followed_id: follow_user_id})
if changeset.valid? do
SampleApp.Repo.insert!(changeset)
true
else
false
end
end
フォローしているか確認する。
def following?(signed_id, follow_user_id) do
relationship = SampleApp.Repo.all(
from(r in SampleApp.Relationship,
where: r.follower_id == ^signed_id and r.followed_id == ^follow_user_id))
!Enum.empty?(relationship)
end
フォローを解除する。
def unfollow!(signed_id, follow_user_id) do
[relationship] = SampleApp.Repo.all(
from(r in SampleApp.Relationship,
where: r.follower_id == ^signed_id and r.followed_id == ^follow_user_id, limit: 1))
SampleApp.Repo.delete!(relationship)
end
Extra
ちょっと嵌まったので書いておく・・・
以下の形で取得すると・・・
relationship = SampleApp.Repo.all(
from(r in SampleApp.Relationship,
where: r.follower_id == ^signed_id and r.followed_id == ^follow_user_id, limit: 1))
以下の結果が得られる。
[%SampleApp.Relationship{__meta__: %Ecto.Schema.Metadata{source: {nil,
"relationships"}, state: :loaded}, followed_id: 2,
followed_user: #Ecto.Association.NotLoaded<association :followed_user is not loaded>,
follower: #Ecto.Association.NotLoaded<association :follower is not loaded>,
follower_id: 1, id: 3, inserted_at: #Ecto.DateTime<2015-08-10T09:09:17Z>,
updated_at: #Ecto.DateTime<2015-08-10T09:09:17Z>}]
しかし、上記の出力結果をRepo.delete!/2へ渡すとエラーになる。
%SampleApp.Relationship{__meta__: %Ecto.Schema.Metadata{source: {nil,
"relationships"}, state: :loaded}, followed_id: 2,
followed_user: #Ecto.Association.NotLoaded<association :followed_user is not loaded>,
follower: #Ecto.Association.NotLoaded<association :follower is not loaded>,
follower_id: 1, id: 3, inserted_at: #Ecto.DateTime<2015-08-10T09:09:17Z>,
updated_at: #Ecto.DateTime<2015-08-10T09:09:17Z>}
そのため、以下のような形で取得を行っている。
[relationship] = SampleApp.Repo.all(
from(r in SampleApp.Relationship,
where: r.follower_id == ^signed_id and r.followed_id == ^follow_user_id, limit: 1))
覚えておいた方が良いのは、
Repo.all/2で取得すると一件でもリストになると言うこと。
そして、それでは受け付けてもらえない関数があると言うこと。
Repo.all/2で取得すると一件でもリストになると言うこと。
そして、それでは受け付けてもらえない関数があると言うこと。
一瞬、Repo.delete_all/2を使うことも考えたが・・・
複数件の削除でないことと、!(感嘆符)がないので処理に失敗した場合、
例外を投げるか分からなかったため使用はしなかった。
(なくても、あからさまおかしければ、例外かエラーは発生するが・・・)
例外を投げるか分からなかったため使用はしなかった。
(なくても、あからさまおかしければ、例外かエラーは発生するが・・・)
!(感嘆符)がある関数は、常に正常に動作する必要がある。
insert!やdelete!など末尾に!(感嘆符)のある関数は、
処理に失敗した場合には例外を発生させなければならない。
insert!やdelete!など末尾に!(感嘆符)のある関数は、
処理に失敗した場合には例外を発生させなければならない。
Speaking to oneself
Relationshipの補助関数は後で変更する可能性がありますね。
validate_presence/1があるモジュールだが、適切な名前と作成位置だったのか自信がない。
Ectoに作ってくれないですかね?このバリデーション。
Ectoに作ってくれないですかね?このバリデーション。
でも待てよ・・・実は不要だから作ってないのでは?
そうすると同等の機能を持っている何かがあるのかもしれない。
そうすると同等の機能を持っている何かがあるのかもしれない。
う~む、調べないと分からないな・・・
そも、@required_fields に指定していれば、
値がない場合、エラーを出してくれるから、
そちらで対応しているのかもしれない。
値がない場合、エラーを出してくれるから、
そちらで対応しているのかもしれない。