6月22日からFjordBootCampでプログラミングを勉強してます!

Rails ユーザーフォロー機能を追加する

この記事について

まず最初に

フォロー/フォロワーの関係は常に一対一ではなく、一対多の関係です。つまり1人のユーザーは複数人をフォローできるし、複数人にフォローされます。一対多の関係性の実現には、フォロー・フォロワーの情報を保持する中間テーブルを作る必要があります。

中間テーブルの作成

中間テーブルとして relationships テーブルと そのモデルにあたる Relationship クラスを追加します。follower_idfollowed_id カラムには常にinteger(usersテーブルのidカラムと同じ型)が入るよう設定しました。

rails generate model Relationship follower_id:integer followed_id:integer

上記コマンドを実行すると自動で migrate ファイルが出来上がります。follower_idfollowed_idnull になることはないためnull制約を追加しておきます。

# db/migrate/20201106221457_create_relationships.rb
class CreateRelationships < ActiveRecord::Migration[6.0]
  def change
    create_table :relationships do |t|
      t.integer :follower_id, null: false
      t.integer :followed_id, null: false

      t.timestamps
    end
  end
end

更に add_index を使ってテーブルにインデックスを追加していきます。 インデックスを使うと特定のカラムからのデータ検索を楽にしてくれます。ここでは relatiionships テーブルの、 followed_id に対しindexを追加しました。なぜ follower_id にはインデックスを追加しないのかというと、それは次の行にある follower_id, followed_idへの複合インデックスに含まれているからです。 この複合インデックスはfollower_id と、 followed_id の組み合わせを常にユニークにするために追加しています(同じ人を何度もフォローするような状況を防ぎます)。

# db/migrate/20201106221457_create_relationships.rb
class CreateRelationships < ActiveRecord::Migration[6.0]
# ...
    add_index :relationships, :followed_id
    add_index :relationships, %i[follower_id followed_id], unique: true
  end
end

マイグレーションを実行します。

$ rake db:migrate

中間テーブルの作成が完了しました。$ rails dbconsolerelationships テーブルが存在することを確認しておきます。

$ rails dbconsole
sqlite> .headers ON
sqlite> SELECT * FROM relationships;
id|follower_id|followed_id|created_at|updated_at

テーブルが存在するこを確認し、DBは完了です!

モデルの関連付け

モデルの関連付けをしていきます。 Railsのモデルが関連したモデルのインスタンスを取得する際、関連付けの名前から自動的にモデルのクラス名を推測します。しかし実際には belongs_to :followerに対するFollowerbelongs_to :followedに対する Followed というモデルは存在しません。followerfollowed は両方ともUserモデルです。そこでbelongs_toclass_name オプションをつけることで実際に参照するモデル(User)を指定することができます。

モデルの関連付けの際に利用する primary key を保存するカラム名も、関連付けの名前から推測します。relationship.follower の関連の取得には、 follower_id を使って users テーブルからレコードを取得します。 relashionship.followed も同様に followed_id を使って users テーブルからレコードを取得します。

詳細(https://railsguides.jp/association_basics.html#belongs-to関連付け)

# app/models/relationship.rb
class Relationship < ApplicationRecord
  belongs_to :follower, class_name: 'User'
  belongs_to :followed, class_name: 'User'
end

user モデルに has_manyを追加しuserfolloweruserfollowingを一対多の関係にします。 複数あることを示すためここでは followersfollowings のように複数形の名前をつけます。

belongs_to と同様に、Railsが推測する外部キーのカラム名Relashionships テーブルに存在しないuser_idになります。そのため foreign_key オプションを利用し実際に使うカラム名を指定しています。 dependent: :destroy, は、ユーザーが削除された場合、フォロー・フォロワーの関係性も削除されるようにするものです。

# app/models/user.rb
has_many :followers, class_name: 'Relationship',
                    foreign_key: 'follower_id',
                    dependent: :destroy,
                    inverse_of: :follower
has_many :followings, class_name: 'Relationship',
                        foreign_key: 'followed_id',
                    dependent: :destroy,
                    inverse_of: :followed

Userモデルから中間テーブルを介し、following/followerのUserモデルのインスタンスを取得するためにthroughオプションを設定します。

https://railsguides.jp/association_basics.html#has-many-through関連付け

# app/models/user.rb
# ..

# :following_users(フォローする人)は、中間テーブルのfollowersを通り、followedにたどり着きます
# :follower_users(フォローされる人)は、中間テーブルのfollowingsを通り、followerにたどり着きます
has_many :following_users, through: :followers, source: :followed
has_many :follower_users, through: :followings, source: :follower

これで中間モデルの作成と紐付けが完了しました。

フォロー機能・フォロワー機能の作成

Userモデルにフォロー機能、そしてフォローしているかどうかを確認する機能を追加します。

# model/user.rb
# ..
# フォロー
    def follow(user_id)
        followers.create(followed_id: user_id)
    end

# 今フォローしているか確認する
  def following?(user)
    following_users.include?(user)
  end

フォロー・フォロー解除

relationships コントローラーを作成します。

$ rails g controller relationships

relationships コントローラーにフォロー・アンフォローアクションを追加します。元々 destory メソッドで削除するrelationshipをDBから取得する際に find_by メソッドを使っていたのですが、その場合該当のidが見つからなかったときにnilが返るためnil.destoryでエラーとなってしまうというアドバイスをいただきました。そのため該当レコードがない場合に例外を返す find メソッドへ変更しました。またコントローラーのメソッド名を当初は follow unfollow としていましたが、Railsのレールに則り create destory を使用するよう変更しました。

# app/controllers/relationships_controller.rb
class RelationshipsController < ApplicationController
  def create
        user = User.find_by_id(params[:follow_id])
        if user
            current_user.follow(params[:follow_id]) unless current_user.following?(user)
        end
        redirect_to root_path
  end

  def destroy
        Relationship.find(params[:id]).destroy
      redirect_to root_path
  end
end

フォロー・フォロワー一覧

フォローフォロワー一覧の機能をUsersコントローラーにfollowingsfollowersとして追加します。@user は全てのメソッド内で利用するため、 before_actionで生成するようにしました。

# app/controllers/users_controller.rb
class UsersController < ApplicationController
    before_action :set_user
    
    def show
        @relationship = @user.followings.find_by(follower_id: current_user.id)
    end
    
    def followings
        @followings = @user.following_users
    end
    
    def followers
        @followers = @user.follower_users
    end
    
    private
    def set_user
        @user = User.find(params[:id])
    end
end

これでコントローラーの実装は完了です。

ルーティングとビューの実装

まずroutesを設定します。 今回はフォローしているユーザー一覧、フォローされているユーザー一覧ページも作成します。

# config/routes.rb
    ..
    
  resources :users do
    member do
      get :followings, :followers # 今回追加したルーティング
    end
  end

    resources :relationships, only: %i[create destroy] # 今回追加したルーティング
  resources :users, only: :show
  resources :books
end

ユーザーのプロフィールページに、フォロー人数、フォロワー人数を表示します。また、現在開いているページが自分自身のページでない場合、フォロー・アンフォローボタンを表示します。

# app/viwes/users/show.html.erb
<p><%= link_to 'show.follow'+": #{@user.followers.count}", followings_user_path(@user.id) %></p>
<p><%= link_to 'show.follower'+": #{@user.followings.count}", followers_user_path(@user.id) %></p>

<% unless @user == current_user %>
  <% if current_user.following?(@user) %>
    <%= button_to t('show.unfollow'), relationship_path(@relationship), method: :delete %>
  <% else %>
        <%= form_for(@user.followers.build) do |f| %>
      <%= hidden_field_tag :follow_id, @user.id %>
      <%= f.submit t('show.follow'), class: 'btn btn-primary btn-block' %>
    <% end %>
  <% end %>
<% end %>

フォロー一覧ページ、フォロワー一覧ページを作成します。

# app/viwes/users/followings.html.erb
<% @followings.each do |user| %>
  <table>
    <tr>
      <td>  <%= user.name %> </td>
      <td><%= link_to 'follow.profile', user_path(user) %></td>
    </tr>
  </table>
<% end %>
# app/viwes/users/followers.html.erb
<% @followers.each do |user| %>
  <table>
    <tr>
      <td>  <%= user.name %> </td>
      <td><%= link_to 'profile', user_path(user) %></td>
    </tr>
  </table>
<% end %>

以上で終了です!

感想

この課題では、Railsがリレーションの際に自動的につける名前のルールを理解するのに苦労しました。また提出後にメンターの方から「Railsのルールに則るとこうした方が良い」というレビューをいくつかいただき、自分がRailsのルールではなく自己流でやってしまっていた部分を修正しました。Railsのルールに則るとシンプルで読みやすいコードになったため、今後はRailsルールを意識してコードを書いていこうと思います。丁寧なレビューをありがとうございました。また、アドベントカレンダーに参加しブログを書くことで良い復習になりました。今後も学習を続けていきます!