【未経験プログラミング】twitterクローン多対多【42日】
ModelとModelの関係(リレーション)の種類は『一対多』だけでなく、『多対多』という関係
中間テーブルが必要
belongs_to と has_many のメソッドによって両者の関係をモデルファイルに記述することで関係を定義し、 user.microposts
や micropost.user
が使用可能になった
①中間テーブルを作る
rails g model Relationship user:references follow:references
t.references :follow, foreign_key: { to_table: :users }
の { to_table: :users }
というオプションによって、外部キーとしてusersテーブルを参照するという指定を行っています。
また t.index [:user_id, :follow_id], unique: true
という記述も追加しています。
これは user_id
と follow_id
のペアで重複するものが保存されないようにするデータベースの設定です。
rails db:migrate
rails db:migrate:status
エラーでました。
t.references :follow, foreign_key: true
t.references :follow, foreign_key: { to_table: :users }
に変更忘れでした。
Mysql2::Error: Can't create table テーブルエラーでした。Syntax Errorだったら別の原因を探す。
外部キーとしてuserテーブルを参照するって↑に書いてるのに忘れるとは・・・。
app/models/relationship.rb
で命名規則を変更しているフォローについて補足する。
belongs_to :follow から
belongs_to :follow, class_name: 'User'
関連モデルでの追加
バリテーション(共通化)
app/models/user.rb
has_many に追加
has_many :relationships
順方向:has_many :followings, through: :relationships, source: :follow
逆方向has_many :reverses_of_relationship, class_name: 'Relationship', foreign_key: 'follow_id'
逆方向:has_many :followers, through: :reverses_of_relationship, source: :user
ここで思い出して欲しいのは、『多対多』の関係は2つの『一対多』の関係の組み合わせだと言うことです。
has_manyメソッド
一対多の関係とは、ある1つの Model インスタンス(A)に対して、複数の Model インスタンス(B, B, …)を保持する関係のことです。例えば、今回の User と Micropost では、1人の User は複数の Micropost をツイート(has_many
)することが可能であり、 Micropost は 必ず1人の User に所属(belongs_to
)することが決まっています。
has_many:relationships:多対多 の多の部分 ※犬猫など
has_many :reverses_of_relationship, class_name: 'Relationship', foreign_key: 'follow_id'
は
has_many :relationships
の逆方向(reverse)です。
:reverses_of_relationship
は筆者が命名したものなので、class_name: 'Relationship'
で参照するクラスを指定しています。
また、Rails の命名規則により、User から Relationship を取得するとき、user_id
が使用されます。
そのため、逆方向では、foreign_key: 'follow_id'
と指定して、 user_id
側ではないことを明示します。
これらのオプションは、順方向であった has_many :relationships
では必要ありませんでしたが、Rails の命名規則に沿っていない逆方向では必要なオプションです。
ここまでの内容で気をつけることは、 has_many :relationships
などの書き方は、あくまでRelationshipモデルへの参照であるということです。つまりUserから見た中間テーブルとの関係になります。
この人がフォローしているの人は誰か?という処理をコードなしに実装しているのが
user.followings
と書けば、該当の user
がフォローしている User 達を取得できるようにします。
その機能を提供するのが has_many ..., through: ...
です
has_many :followings, through: :relationships, source: :follow # <= この行
has_many :followers, through: :reverses_of_relationship, source: :user
の2行。
has_many :followings, through: :relationships, source: :follow
の場合、まず、has_many :followings
という関係を新しく命名して『フォローしているUser達』を表現しています。
through: :relationships
という記述により、has_many: relationships
の結果を中間テーブルとして指定しています。
その中間テーブルのカラムの中でどれを参照先の id とすべきかを source: :follow
で、選択しています。
結果として、user.followings
というメソッドを用いると、 user
が中間テーブル relationships
を取得し、その1つ1つの relationship
の follow_id
から 自分がフォローしている User 達 を取得するという処理が可能になります。
中間テーブルを経由して相手の情報を取得できるようにするためには throught
を使用すると覚えておきましょう。
今度は 自分をフォローしている User 達 を取得します。
has_many :followers, through: :reverses_of_relationship, source: :user
も、順方向に対して、逆の設定をしているだけです。through:
には逆方向の :reverses_of_relationship
を指定しており、 source: :user
で relationships 中間テーブルの user_id
のほうが取得したい User だと指定しています。これで、user.followers
によって、「自分をフォローしている User 達」を取得することができます。
次にこのフォローの関係を手軽に作成したり外したり出来るように、Userモデルにメソッドを追加していきましょう。
メソッドの使用イメージは特定のUserが user.follow(other_user)
や、 user.unfollow(other_user)
として、該当のUserをフォロー/アンフォローできるようにするというものです。
また、既にフォローしているかどうかも分かるように following?
メソッドも作成しておきます。
フォロー/アンフォローは中間テーブルのレコードを保存/削除にあたる
def follow(other_user)
unless self == other_user
self.relationships.find_or_create_by(follow_id: other_user.id)
はunless self == other_user
によって、フォローしようとしている other_user が自分自身ではないかを検証しています。
self.relationships.find_or_create_by(follow_id: other_user.id)
は、見つかれば Relation を返し、見つからなければ self.relationships.create(follow_id: other_user.id)
としてフォロー関係を保存(create = build + save)することができます。これにより、既にフォローされている場合にフォローが重複して保存されることがなくなります。
selfはをメソッドの中で呼び出すとオブジェクトを見ている。
def unfollow(other_user)
relationship = self.relationships.find_by(follow_id: other_user.id)
relationship.destroy if relationship
フォローがあればアンフォローしています。また、relationship.destroy if relationship
は、relationship
が存在すれば destroy
します。if文はこのように書けます。
def following?(other_user)
self.followings.include?(other_user)
self.followings
によりフォローしている User 達を取得し、include?(other_user)
によって other_user
が含まれていないかを確認しています。含まれている場合には、true
を返し、含まれていない場合には、false
を返します。
> user1.follow(user2) # user1がuser2をフォローする
> user1.reload # user1を再取得
> user1.followings # user1のフォローしている人たちを取得
また、 reload
メソッドを入れたのは、 user1
のインスタンスを再取得するためです。
理由をみていきます。user1.follow(user2)
をすると、データベースは更新され、フォローしたことになります。
しかし、 user1.followings
を実行してもまだ user2
が followings
に出てきません。
理由は、 user1.follow(user2)
しても、user1
にその結果が反映されないからです。
そのため、もう一度 user1 = User.first
などとして更新した状態のレコードを再取得する必要があります。その代わりに、reload
とすれば、自動的にインスタンス(レコード)を再取得してくれます。
config/routes.rb
Rails.application.routes.draw do root to: 'toppages#index' get 'login', to: 'sessions#new' post 'login', to: 'sessions#create' delete 'logout', to: 'sessions#destroy' get 'signup', to: 'users#new' resources :users, only: [:index, :show, :new, :create] do member do get :followings get :followers end end resources :microposts, only: [:create, :destroy] resources :relationships, only: [:create, :destroy] end
ログインユーザがユーザをフォロー/アンフォローできるようにするルーティングは、 resources :relationships, only: [:create, :destroy]
保存/削除
Relationship は中間テーブルなので index や show でユーザに見せるようなリソースではありません。フォロー/アンフォローボタンは View で設置します。
フォロー中のユーザとフォローされているユーザ一覧を表示するページは必要です。そのためのルーティングが下記コードになります。member do ... end
が追加されています。
Prefix Verb URI Pattern Controller#Action
followings_user GET /users/:id/followings(.:format) users#followings
followers_user GET /users/:id/followers(.:format) users#followers
resources
には member
と collection
という URL を深掘りするオプションを付与することができます。
collection do
get :search
end
collection は member と違って、/users/search
のように :id
が含まれない URL を生成します。大きな違いは、ユーザを :id
によって特定する必要があるページかどうかで member か collection かを選択します。今回のように、ユーザの検索結果を表示する /users/search
の場合、ユーザを特定してしまっては検索の意味がないので、:id
は不要です。そのため、collection を使用します。
app/controllers/application_controller.rb
def counts(user) @count_microposts = user.microposts.count @count_followings = user.followings.count @count_followers = user.followers.count end
これでカウントの保存ができるようになる。
app/views/users/show.html.erb 実装画面
<li class="<%= 'active' if current_page?(user_path(@user)) %>"><%= link_to user_path(@user) do %>Microposts <span class="badge"><%= @count_microposts %></span><% end %></li>
フォローボタン実装
app/views/relationships/_follow_button.html.erb
<%= form_for(current_user.relationships.build) do |f| %> <%= hidden_field_tag :follow_id, user.id %> <%= f.submit 'Follow', class: 'btn btn-primary btn-block' %> <% end %>
上記がフォローボタンになる。
<%= hidden_field_tag :follow_id, user.id %>
は、<input type="hidden" name="follow_id" value="ユーザの id">
を生成します。
type="hidden"
はユーザに見せないフォームの隠しデータとなります。relationships#create にフォームデータが送信されたとき、フォローすべきユーザの user.id
を知りたいのですが、このように hidden
として送信する以外に user.id
を手に入れる方法がありませんので、このようにしています。Controller 側では params[:follow_id]
として取得します。
タイムラインの実装
def feed_microposts
.where(user_id: self.following_ids + [self.id])
Controller
@microposts = current_user.microposts.order('created_at DESC').page(params[:page])
@microposts = current_user.feed_microposts.order('created_at DESC').page(params[:page])