Hit the books!!

プログラミング学習記録

ユーザーフォロー機能の実装

以前にも書きましたが、引き続きフィヨルドブートキャンプでRuby on Railsの勉強をしております。

ud-ike.hatenablog.com

この続きでユーザーフォロー機能を作りましたが、難しくて消化不良な感じがするのでまとめてみます。

f:id:ud_ike:20201228142131p:plain

Railsのプラクティスもあと少し!なのになかなか抜け出せない...

要件

  • ユーザー詳細画面でフォローできるようにする
  • フォロー済の場合はフォロー解除のボタンを表示させる
  • ユーザー詳細画面にフォロー人数とフォロワー人数を表示させる
  • フォロー人数からフォロー一覧画面へ遷移させる(URLは/users/:user_id/followings)
  • フォロワー人数からフォロワー一覧画面へ遷移させる(URLは/users/:user_id/followers)

参考

実装

上に書いたRailsチュートリアルを参考に進めていきます。

Relationshipモデルを作る

まずはfollower_idとfollowing_idというカラムを持ったRelationshipモデルを生成する。

follower_id、following_idにはuserテーブルのidが入る。

% bin/rails g model Relationship follower_id:integer following_id:integer

NOT NULL制約とadd_indexを追記する。インデックスを付与すると検索速度が向上します。

そしてfollower_idとfollowing_idの組み合わせはユニークである必要がある。

# db/migrate/20201217040615_create_relationships.rb 

class CreateRelationships < ActiveRecord::Migration[6.0]
  def change
    create_table :relationships do |t|
      t.integer :follower_id, null: false
      t.integer :following_id, null: false

      t.timestamps
    end
    add_index :relationships, :follower_id
    add_index :relationships, :following_id
    add_index :relationships, %i[follower_id following_id], unique: true
  end
end

bin/rails db:migrateを実行する。

User、Relationshipのアソシエーションを追記

ここは最初から理解するのは難しいところです...汗

# app/models/relationship.rb

class Relationship < ApplicationRecord
  belongs_to :follower, class_name: 'User'
  belongs_to :following, class_name: 'User'
end

followerやfollwingというモデルは存在しないが、class_nameを使ってモデルを指定すればモデル名と異なる関連付け名を指定することができる。

このように書くと@user.follower、@user.followingを使ってレコードを取得できるようになる。

# app/models/relationship.rb

class Relationship < ApplicationRecord
  belongs_to :follower, class_name: 'User'
  belongs_to :following, class_name: 'User'
  validates :follower_id, presence: true
  validates :following_id, presence: true
end

presence: trueのバリデーションも追記しておきます。

次に1対多の関連付けを行います。has_manyのうしろは複数形にする。

# app/models/user.rb

class User < ApplicationRecord
  (省略)

  has_many :followers,
           class_name: 'Relationship',
           foreign_key: 'follower_id',
           dependent: :destroy,
           inverse_of: :follower
  has_many :followings,
           class_name: 'Relationship',
           foreign_key: 'following_id',
           dependent: :destroy,
           inverse_of: :following

  (省略)
end

外部キー(foreign_key)は簡単に言うと、この項目から選べってこと。それぞれ、Relationshipのfollower_idカラムとfollowing_idカラムの値を使えということですね。

ユーザーを削除したときにフォローの関係も削除されるようにdependent: :destroyを指定する。また、inverse_of:を使うと双方向の関連付けを行うことができる。

参考:ActiveRecord4, Rails4のinverse_ofについて理解したメモ - Shoken Startup Blog

inverse_of、奥が深そうでよくわからん...が、次に進みます。

has_many through:を使ってユーザーとフォロワー/フォロー中の人を関連付ける。

# app/models/user.rb

class User < ApplicationRecord
  (省略)

  has_many :followers,
           class_name: 'Relationship',
           foreign_key: 'follower_id',
           dependent: :destroy,
           inverse_of: :follower
  has_many :followings,
           class_name: 'Relationship',
           foreign_key: 'following_id',
           dependent: :destroy,
           inverse_of: :following

  # 次の2行を追記
  has_many :following_users, through: :followers, source: :following # followersを通じてfollowingにたどり着く
  has_many :follower_users, through: :followings, source: :follower # followingsを通じてfollowerにたどり着く

  (省略)
end

参考:Active Record の関連付け - Railsガイド

sourceは関連付け元の名前をさす。完成して実際に動かしてみたほうが理解しやすいな。

フォロー機能

こんな感じで進めます。

  • Userモデルに実装
    • フォローする
    • フォローしているか確認する
  • relationshipコントローラーに実装
    • フォローする(create)
    • フォロー外す(destroy)
# app/models/user.rb

class User < ApplicationRecord
  (省略)

  # フォローする
  def follow(other_user_id)
    followers.create(following_id: other_user_id)
  end

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

フォローしているかの確認は、「フォローする」「フォロー解除する」ボタンのどちらを表示させるか判断するところで使うんですね。

(レビュー後の追記)

user.rbに"フォロー解除する"を実装していなかったが、unfollowもあったほうが対称性があってAPI設計としては良いとのことなので、followと同じようにunfollowも実装するように修正した。

Relationshipコントローラーを作る

% bin/rails g controller relationships
# app/controllers/relationships_controller.rb

class RelationshipsController < ApplicationController

  def create
    user = User.find(params[:follow_id])
    current_user.follow(params[:follow_id])
    redirect_to user_path(user)
  end

  def destroy
    Relationship.find_by(follower_id: current_user.id, following_id: params[:id]).destroy
    redirect_to user_path
  end
end

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

Usersコントローラーに追記します。

# app/controllers/users_controller.rb

class UsersController < ApplicationController
  before_action :authenticate_user!
  before_action :set_user, only: %I[show edit update destroy followings followers]

  (省略)

  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

before_action :set_userにfollowingsとfollowersアクションも追記。

ルーティング

member doで先ほどのfollowingsとfollowersアクションのルーティングを追記。ちなみに、以前の課題でi18nを使って日本語と英語に対応するようにしています。

resources :relationshipsの行も追記。

# config/routes.rb

Rails.application.routes.draw do
  (省略)

  scope '(:locale)' do
    resources :books
    resources :users, except: :create do
      member do # 追記
        get :followings, :followers
      end
    end
    resource :user_icons, only: :destroy
    resources :relationships, only: %I[create destroy] # 追記
  end
end

ここでルーティングを確認してみよう。

f:id:ud_ike:20201228180648p:plain

字が小さいけどいい感じ◎

想定した通り、フォロー中ユーザーの一覧のURLが/users/:user_id/followingsの形になってる。

ビューの編集

あとは見た目の編集です。

まずはユーザーの詳細ページで「フォローする」ボタンもしくは「フォロー解除」ボタンを表示させる。

# app/views/users/show.html.erb

(省略)

<p>
<% unless @user == current_user %>
  <% if current_user.following?(@user) %>
    <%= button_to t('button.unfollow'), relationship_path, method: :delete %>
  <% else %>
      <%= form_for(@user.followers.build) do |f| %>
        <%= hidden_field_tag :follow_id, @user.id %>
        <%= f.submit t('button.follow') %>
      <% end %>
    <% end %>
<% end %>
</p>

(省略)

ここも難しいですねー。buildは、親モデルに対する子モデルのインスタンスを生成したいときに使うらしい。

hidden_field_tagはHTML上には表示させないが値を保持したいときに使う(ソースコードには表示されます)。ここではfollow_idというパラメーターに@user.idの情報を渡している。

follow_idはRelationshipsコントローラーのcreateメソッドで使われています。

このままではフォローの関係が何も登録されていないので、ちゃんと実装できているか確認するために、コンソール(bin/rails c)を使ってrelationshipテーブルに直接follower_idとfollowing_idを指定してデータを登録しました。

そして次はフォロー中・フォロワーの人数を表示し、ユーザー一覧にリンクさせます。

# app/views/users/show.html.erb

(省略)

<p>
  <%= link_to "#{@user.followers.count} #{User.human_attribute_name("followings")}", followings_user_path %> <%= link_to "#{@user.followings.count} #{User.human_attribute_name("followers")}", followers_user_path %>
</p>

(省略)

@user.followers.countでフォロワーの人数を取得できる。

これでユーザーの詳細ページはこんな見た目になりました↓

f:id:ud_ike:20201228182902p:plain

ログインしている人(自分)の詳細ページはボタンは表示されない。

f:id:ud_ike:20201228183020p:plain

最後に、フォロー中・フォロワー一覧ページを作成します。

# app/views/users/followings.html.erb(新規)

<h1><%= t("title.followings") %></h1>

<table>
  <% @followings.each do |user| %>
    <tr>
      <td>
        <% if user.icon.attached? %>
          <%= image_tag user.icon.variant(resize_to_limit: [30, 30]) %>
        <% end %>
      </td>
      <td><%= link_to user.username, user_path(user) %></td>
      <td><%= user.profile %></td>
    </tr>
  <% end %>
</table>

<%= link_to t("button.back"), user_path %>
# app/views/users/followers.html.erb(新規)

<h1><%= t("title.followers") %></h1>

<table>
  <% @followers.each do |user| %>
    <tr>
      <td>
        <% if user.icon.attached? %>
          <%= image_tag user.icon.variant(resize_to_limit: [30, 30]) %>
        <% end %>
      </td>
      <td><%= link_to user.username, user_path(user) %></td>
      <td><%= user.profile %></td>
    </tr>
  <% end %>
</table>

<%= link_to t("button.back"), user_path %>

一覧にアイコンはあったほうがいいかと思って表示するようにした。

フォロワー一覧はこんな感じ↓

f:id:ud_ike:20201228183621p:plain

できたーー!

(レビュー後の追記)

followings.html.erbとfollowers.html.erbの内容がほぼ一緒なのでパーシャルビューを使うように変更。最初パーシャルのファイル名をfollowとしていたけど、ユーザー一覧なのでusersのほうがいいのではと指摘された。たしかに〜。

追記:pathのid自動補完について

よくlink_toなんかに例えばuser_path(@user)と書くと、ルーティングで/users/:idとなっているところにリンクされるじゃないですか。このときの(@user)を省略したらエラーになるときと、省略してもRailsが自動補完してくれてエラーにならない場合があるということを知りました。

私の説明よりもレビューしてくださったメンターである伊藤さんの記事を読んだほうがわかりやすくて早い↓

【Railsトリビア】blog_path(@ blog)の代わりにblog_pathと書いても自動的にidが補完される - Qiita

私今までエラーが出たら(@user)って追記する、みたいなスタイルであんまり考えてなかったのですよ...。エラーにならなくても、省略せず明記したほうがいいとのことなので今後はちゃんと確認しなきゃー。

コードレビューって大変だけど(レビューする側が大変そう)ためになるって体感できるし楽しいな。そのことにここにきて気付きました。