【未経験プログラミング】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 を使用すると覚えておきましょう。

 

https://techacademy.s3.amazonaws.com/bootcamp/webapp/twitter-clone/has_many_through_rails.png

今度は 自分フォローしている User 達 を取得します。

has_many :followers, through: :reverses_of_relationship, source: :user も、順方向に対して、逆の設定をしているだけです。through: には逆方向の :reverses_of_relationship を指定しており、 source: :user で relationships 中間テーブルの user_id のほうが取得したい User だと指定しています。これで、user.followers によって、「自分をフォローしている User 達」を取得することができます。

https://techacademy.s3.amazonaws.com/bootcamp/webapp/twitter-clone/has_many_through_r_rails.png

次にこのフォローの関係を手軽に作成したり外したり出来るように、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])