フォロー機能を実装する
ポートフォリオで使用するために一度試しに実装してみたいと思います。事前にユーザー登録、ログインはsorceryで実装済みです。
参考にしたサイト Railsでユーザーフォロー機能を実装する(Ajax使うよ)① - Qiita
テーブル設計
ユーザーフォロー機能の実装のために用意したテーブル設計は下記のようになります。本当に分かりづらいのですが、RelationshipsテーブルがUsersテーブル(フォローするユーザー)とUsersテーブル(フォローされるユーザー)の中間テーブルとしての役割を担っています。

Relationshipモデルを作成する
まずはRealtionshipモデルを作成します。
$ bundle exec rails g model Relationship
Running via Spring preloader in process 1766
invoke active_record
create db/migrate/20210531014628_create_relationships.rb
create app/models/relationship.rb
外部キーのuser_idカラムとfollower_idカラムを作成します。ここでuser_idはフォローする人、follower_idはフォローされる人を指します。
follower_idの参照先のテーブルはUsersテーブルにしてあげたいので、foreign_key: {to_table: :users}としてあげてます。この設定をしないと存在しないfollowerテーブルを参照しようとします。この設定をすることで、followerを探すときはUsersテーブルのfollower_idをみるようになります。
class CreateRelationships < ActiveRecord::Migration[6.1] def change create_table :relationships do |t| t.references :user t.references :follower, foreign_key: { to_table: :users } t.timestamps t.index [:user_id, :follower_id], unique: true end end end
外部キー制約をつけたので、インデックスは自動で付与されるので、add_index :relationships, :follower_idという記載は不要です。
t.index [:user_id, :follower_id], unique: trueこの記載は、unique制約で同じ人を2回フォローできないようにするために必要です。
マイグレーションファイルの修正が終わったので、テーブルを作成します。
$ rails db:migrate == 20210531014628 CreateRelationships: migrating ============================== -- create_table(:relationships) -> 0.1209s == 20210531014628 CreateRelationships: migrated (0.1210s) =====================
次からはフォローする、フォローされるの2つの状況を1つずつ分けて理解していきたいと思います。まず最初はユーザーが他の人をフォローするという状況を説明していきます。
フォローする
Userモデルのアソシエーションの記述は下記のようになります。
class User < ApplicationRecord has_many :relationships, dependent: :destroy has_many :followings, through: :relationships, source: :follower end
一行目は、UserはたくさんのRelationshipsを持っています。userが消去されたら結びつくrelationshipのデータを消去するという意味です。
二行目のfollowingsは「フォローしている人を取得するため」のアソシエーションの別名としてつけました。followingモデルは存在しないので、参照するモデルとしてRelationshipを指定しています。この設定をすることで、user.followingsと打つだけで、userが中間テーブルrelationshipsを取得し、 relationshipのfollower_idからフォローしている人を取得しています。source: :followerというオプションをつけて関連先のモデル名としてfollowerを参照するようにします。しかしfollowerテーブルは作っていないので、どうなるのかというと、、次のRelationshipモデルのアソシエーションで定義しています。
Relationshipモデルのアソシエーションの記載は下記のようになります。
class Relationship < ApplicationRecord belongs_to :follower, class_name: 'User' end
class_name: 'User'と補足設定することで、Followerクラスという存在しないクラスを参照することを防ぎ、Userクラスであることを明示しています。
実際にユーザーを作成してフォローすることができているか確認します。まず最初にユーザーデータを3つ作成しました。
irb(main):002:0> user = User.new(email: 'sayo@example.com', password: 'password', password_confirmation: 'password')
=> #<User id: nil, email: "sayo@example.com", crypted_password: nil, salt: nil, created_at: nil, updated_at: nil>
irb(main):003:0> user.save
TRANSACTION (0.2ms) BEGIN
User Exists? (0.3ms) SELECT 1 AS one FROM `users` WHERE `users`.`email` = 'sayo@example.com' LIMIT 1
User Create (87.0ms) INSERT INTO `users` (`email`, `crypted_password`, `salt`, `created_at`, `updated_at`) VALUES ('sayo@example.com', '$2a$10$tXrj8J450XST3ON.PlaGdetehohD1BQj92d0SctEZ6wfO5BfLOJ2u', 'hrxhPytqWsU4N__UzSZQ', '2021-05-31 02:00:17.356071', '2021-05-31 02:00:17.356071')
TRANSACTION (2.1ms) COMMIT
=> true
irb(main):006:0> user = User.new(email: 'eri@example.com', password: 'password', password_confirmation: 'password')
=> #<User id: nil, email: "eri@example.com", crypted_password: nil, salt: nil, created_at: nil, updated_at: nil>
irb(main):007:0> user.save
TRANSACTION (2.4ms) BEGIN
User Exists? (0.8ms) SELECT 1 AS one FROM `users` WHERE `users`.`email` = 'eri@example.com' LIMIT 1
User Create (41.4ms) INSERT INTO `users` (`email`, `crypted_password`, `salt`, `created_at`, `updated_at`) VALUES ('eri@example.com', '$2a$10$tZCz10GG/8h.8TJKdaOyjO2P2XxsQwGh9jtrh4yj6RApLHO1J1Exa', 'MdzTjVF715m3aVxN39tN', '2021-05-31 02:01:46.998133', '2021-05-31 02:01:46.998133')
TRANSACTION (0.8ms) COMMIT
=> true
irb(main):008:0> user = User.new(email: 'yuka@example.com', password: 'password', password_confirmation: 'password')
=> #<User id: nil, email: "yuka@example.com", crypted_password: nil, salt: nil, created_at: nil, updated_at: nil>
irb(main):009:0> user.save
TRANSACTION (0.2ms) BEGIN
User Exists? (0.5ms) SELECT 1 AS one FROM `users` WHERE `users`.`email` = 'yuka@example.com' LIMIT 1
User Create (40.3ms) INSERT INTO `users` (`email`, `crypted_password`, `salt`, `created_at`, `updated_at`) VALUES ('yuka@example.com', '$2a$10$dJsYPcoFqBl3Q95JUxJNR.cQu20uwxiBrjkUsdIqBhr2EV85PhDTy', 'tbwU-CfEuFXmxJXKNyc5', '2021-05-31 02:02:11.111278', '2021-05-31 02:02:11.111278')
TRANSACTION (0.4ms) COMMIT
=> true
次に作成したユーザー(id: 3)が(id: 2)のユーザーをフォローします。
irb(main):010:0> user.relationships.create!(follower_id: 2) TRANSACTION (0.2ms) BEGIN User Load (0.4ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 2 LIMIT 1 Relationship Create (65.1ms) INSERT INTO `relationships` (`user_id`, `follower_id`, `created_at`, `updated_at`) VALUES (3, 2, '2021-05-31 02:02:21.616116', '2021-05-31 02:02:21.616116') TRANSACTION (41.8ms) COMMIT => #<Relationship id: 1, user_id: 3, follower_id: 2, created_at: "2021-05-31 02:02:21.616116000 +0000", updated_at: "2021-05-31 02:02:21.616116000 +0000"> irb(main):011:0> user.followings User Load (40.6ms) SELECT `users`.* FROM `users` INNER JOIN `relationships` ON `users`.`id` = `relationships`.`follower_id` WHERE `relationships`.`user_id` = 3 /* loading for inspect */ LIMIT 11 => #<ActiveRecord::Associations::CollectionProxy [#<User id: 2, email: "eri@example.com", crypted_password: "$2a$10$tZCz10GG/8h.8TJKdaOyjO2P2XxsQwGh9jtrh4yj6RA...", salt: "MdzTjVF715m3aVxN39tN", created_at: "2021-05-31 02:01:46.998133000 +0000", updated_at: "2021-05-31 02:01:46.998133000 +0000">]>
これでフォローするというアソシエーションが機能していることがわかりました。
フォローされている
次は、あるユーザーがたくさんの人にフォローされているという関係を作成します。
Userモデルのアソシエーションの記述は下記のようになります。
class User < ApplicationRecord has_many :passive_relationships, class_name: 'Relationship', foreign_key: 'follower_id', dependent: :destroy has_many :followers, through: :passive_relationships, source: :user end
一行目は、has_many :relationshipsでいいかな?と思ったのですが、ユーザーをフォローするという関係性でrelationshipモデルを参照してしまっていてuser.relationshipsのように使うことができなくなってしまうので、別名としてpassive_relationshipsと名付けました。class_name: 'Relationship'でRelationshipモデルを参照するように指定しています。foreign_key: 'follower_id'はrelaitonshipsテーブルにアクセスする時、follower_idを元にアクセスしてくださいということを意味しています。
二行目は、Userはたくさんのfollowersをpassive_relationshipsを通じて持っています。passive_relationshipsは、直前の定義によりRelationshipクラスを参照するようになっています。
Relationshipモデルのアソシエーションの記載は下記のようになります。
class Relationship < ApplicationRecord belongs_to :user end
コンソールでid:3のユーザーがid:2のユーザーをフォローするというデータは作ったので、id:2のユーザーは、id:3のユーザーにフォローされているはずです。
irb(main):001:0> user2 = User.second User Load (1.9ms) SELECT `users`.* FROM `users` ORDER BY `users`.`id` ASC LIMIT 1 OFFSET 1 => #<User id: 2, email: "eri@example.com", crypted_password: "$2a$10$tZCz10GG/8h.8TJKdaOyjO2P2XxsQwGh9jtrh4yj6RA...", salt: "MdzTjVF715m3aVxN39tN", created_at: "2021-05-31 02:01:46.998133000 +0000", updated_at: "2021-05-31 02:01:46.998133000 +0000"> irb(main):002:0> user2.followers User Load (42.8ms) SELECT `users`.* FROM `users` INNER JOIN `relationships` ON `users`.`id` = `relationships`.`user_id` WHERE `relationships`.`follower_id` = 2 /* loading for inspect */ LIMIT 11 => #<ActiveRecord::Associations::CollectionProxy [#<User id: 3, email: "yuka@example.com", crypted_password: "$2a$10$dJsYPcoFqBl3Q95JUxJNR.cQu20uwxiBrjkUsdIqBhr...", salt: "tbwU-CfEuFXmxJXKNyc5", created_at: "2021-05-31 02:02:11.111278000 +0000", updated_at: "2021-05-31 02:02:11.111278000 +0000">]>
最終的なUserモデルとRelationshipモデルのアソシエーションを下記にまとめておきます。
class User < ApplicationRecord has_many :relationships, dependent: :destroy has_many :followings, through: :relationships, source: :follower has_many :passive_relationships, class_name: 'Relationship', foreign_key: 'follower_id', dependent: :destroy has_many :followers, through: :passive_relationships, source: :user end
class Relationship < ApplicationRecord belongs_to :user belongs_to :follower, class_name: 'User' end
relationshipsコントローラを作成する
次にフォロー機能を定義するrelationshipsコントローラを作成します。
$ rails g controller relationships
Running via Spring preloader in process 2378
create app/controllers/relationships_controller.rb
invoke erb
create app/views/relationships
invoke helper
create app/helpers/relationships_helper.rb
invoke assets
invoke scss
create app/assets/stylesheets/relationships.scss
作成したコントローラを実装していきます。
class RelationshipsController < ApplicationController def create @other_user = User.find(params[:follower]) current_user.follow(@other_user) redirect_back fallback_location: root_path end def destroy @user = current_user.relationships.find(params[:id]).follower current_user.unfollow(params[:id]) redirect_back fallback_location: root_path end end
createアクションはフォローするという処理、destroyアクションはフォローを解除するという処理です。ここで出てきたfollowとunfollowメソッドはモデルに定義します。
Userモデルにフォローする、フォローしているか確認する、フォロー解除するというメソッドをUserモデルに作成しました。
def follow(other_user) return if self == other_user relationships.find_or_create_by!(follower: other_user) end def following?(user) followings.include?(user) end def unfollow(relathinoship_id) relationships.find(relathinoship_id).destroy! end
return if self == other_userで、フォローしようとしているother_userが自分自身ではないかを検証しています。
ルーティングとビュー
フォロー機能のルーティングとユーザー一覧機能のルーティングの追加もしておきます。
Rails.application.routes.draw do resources :users, only: [:new, :create, :index] resources :relationships, only: [:create, :destroy] end
ルーティングを追加したのでusersコントローラにindexアクションも追加しました。
class UsersController < ApplicationController # ユーザー一覧 def index @users = User.all end end
ユーザー一覧のビュー(index.html.erb)も準備します。
$ touch app/views/users/index.html.erb
フォローボタン、解除ボタンをそれぞれパーシャルに切り出して、管理しやすいようにしておきたいので、それぞれのファイルを作成します。
$ touch app/views/relationships/_follow_button.html.erb $ touch app/views/relationships/_unfollow_button.html.erb
relationships/_follow_button.html.erb <%= button_to 'フォロー', relationships_path(follower: user), method: :post %>
relationships/_unfollow_button.html.erb <%= button_to 'フォロー解除', relationship_path(current_user.relationships.find_by(follower: user)), method: :delete %>
上記で作ったテンプレートを埋め込んでユーザー一覧画面を作成します。
<p id="notice"><%= notice %></p> <h1>Users</h1> <table> <thead> <tr> <th>Email</th> <th colspan="3"></th> </tr> </thead> <tbody> <% @users.each do |user| %> <tr> <td><%= user.email %></td> <td> <% if logged_in? && current_user != user %> <div class="form-group"> <% if current_user.following?(user) %> <%= render 'relationships/unfollow_button', user: user %> <% else %> <%= render 'relationships/follow_button', user: user %> <% end %> </div> <% end %> </td> </tr> <% end %> </tbody> </table> <br> <%= link_to 'New User', new_user_path %>
ここまでの設定でログイン中のユーザーにはボタンが出ず、そのほかのユーザーにはボタンが出るようになって、また「フォロー」ボタンを押して画面をリロード後「フォロー解除」ボタンが現れるようになりました。

この時の処理をメモとして残しておきます。フォローした時の状況です。
Started POST "/relationships?follower=2" for ::1 at 2021-06-01 11:32:08 +0900
Processing by RelationshipsController#create as HTML
Parameters: {"authenticity_token"=>"[FILTERED]", "follower"=>"2"}
User Load (12.4ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 2 LIMIT 1
↳ app/controllers/relationships_controller.rb:3:in `create'
User Load (43.7ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1
↳ app/controllers/relationships_controller.rb:4:in `create'
Relationship Load (93.0ms) SELECT `relationships`.* FROM `relationships` WHERE `relationships`.`user_id` = 1 AND `relationships`.`follower_id` = 2 LIMIT 1
↳ app/models/user.rb:22:in `follow'
TRANSACTION (13.2ms) BEGIN
↳ app/models/user.rb:22:in `follow'
Relationship Create (90.3ms) INSERT INTO `relationships` (`user_id`, `follower_id`, `created_at`, `updated_at`) VALUES (1, 2, '2021-06-01 02:32:09.536051', '2021-06-01 02:32:09.536051')
↳ app/models/user.rb:22:in `follow'
TRANSACTION (30.1ms) COMMIT
↳ app/models/user.rb:22:in `follow'
Redirected to http://localhost:3000/users
Completed 302 Found in 743ms (ActiveRecord: 325.5ms | Allocations: 5074)
Started GET "/users" for ::1 at 2021-06-01 11:32:09 +0900
Processing by UsersController#index as HTML
Rendering layout layouts/application.html.erb
Rendering users/index.html.erb within layouts/application
User Load (100.0ms) SELECT `users`.* FROM `users`
↳ app/views/users/index.html.erb:14
User Load (2.7ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1
↳ app/views/users/index.html.erb:18
User Exists? (64.6ms) SELECT 1 AS one FROM `users` INNER JOIN `relationships` ON `users`.`id` = `relationships`.`follower_id` WHERE `relationships`.`user_id` = 1 AND `users`.`id` = 2 LIMIT 1
↳ app/models/user.rb:26:in `following?'
Relationship Load (1.6ms) SELECT `relationships`.* FROM `relationships` WHERE `relationships`.`user_id` = 1 AND `relationships`.`follower_id` = 2 LIMIT 1
↳ app/views/users/index.html.erb:23
User Exists? (1.3ms) SELECT 1 AS one FROM `users` INNER JOIN `relationships` ON `users`.`id` = `relationships`.`follower_id` WHERE `relationships`.`user_id` = 1 AND `users`.`id` = 3 LIMIT 1
↳ app/models/user.rb:26:in `following?'
Relationship Load (2.0ms) SELECT `relationships`.* FROM `relationships` WHERE `relationships`.`user_id` = 1 AND `relationships`.`follower_id` = 3 LIMIT 1
↳ app/views/users/index.html.erb:23
Rendered users/index.html.erb within layouts/application (Duration: 211.0ms | Allocations: 5505)
[Webpacker] Everything's up-to-date. Nothing to do
Rendered layout layouts/application.html.erb (Duration: 268.0ms | Allocations: 10045)
Completed 200 OK in 274ms (Views: 99.5ms | ActiveRecord: 172.1ms | Allocations: 10499)