ユーザーフォロー機能の実装
以前にも書きましたが、引き続きフィヨルドブートキャンプでRuby on Railsの勉強をしております。
この続きでユーザーフォロー機能を作りましたが、難しくて消化不良な感じがするのでまとめてみます。
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
ここでルーティングを確認してみよう。
字が小さいけどいい感じ◎
想定した通り、フォロー中ユーザーの一覧の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
でフォロワーの人数を取得できる。
これでユーザーの詳細ページはこんな見た目になりました↓
ログインしている人(自分)の詳細ページはボタンは表示されない。
最後に、フォロー中・フォロワー一覧ページを作成します。
# 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 %>
一覧にアイコンはあったほうがいいかと思って表示するようにした。
フォロワー一覧はこんな感じ↓
できたーー!
(レビュー後の追記)
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)って追記する、みたいなスタイルであんまり考えてなかったのですよ...。エラーにならなくても、省略せず明記したほうがいいとのことなので今後はちゃんと確認しなきゃー。
コードレビューって大変だけど(レビューする側が大変そう)ためになるって体感できるし楽しいな。そのことにここにきて気付きました。