18
22

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

[Ruby on Rails] フォロー機能を実装しよう

Last updated at Posted at 2020-03-23

#はじめに
今回はフォロー機能を作成します。railsチュートリアルではform_forを利用していましたが、今回はform_withを利用して作成します。
(題材は自分のポートフォリオです。Userモデルはある前提で進めます。)

###対象読者
railsチュートリアル終わったレベルくらいの人

###作成の流れ
1.relationshipモデルの作成
2.各モデルの関連付け、フォロー機能のメゾット作成
3.対応するコントローラーの作成
4.form_withを利用したフォローボタン作成
5.jsファイルの作成
6.フォロー機能の流れ

###今回のAjaxの流れ
①fomr_withで作成したフォロー/アンフォローボタンを押すとPost/deleteリクエストが送られる。

②対応するコントローラのcreate/destroyアクションを実行

③js.erbファイルをレンダリング

④Ajax対象範囲のフォロー/アンフォローボタンを非同期で切り替え

#1.relationshipモデルの作成
relationshipモデルの中身はこんな感じです。中間テーブルを作成しますが別にフォローしているユーザーの情報を記録するようなテーブルは作りません。実際のフォローする際のテーブルの流れは
Userモデル→Follow_relationship→Userモデルと帰ってくる流れになります。
follower,followingと名前がついていますがどちらもUserのidが入ります。

カラム
id integer
follower_id integer
following_id integer

モデルを作成します。
それぞれのカラムに外部キー制約とindexをこの後つけるのでreferencesを付けて生成します。(referencesをつけると自動で〇〇_idの形にしてくれます。)

rails g model Follow_Relationship follower:references following:references

#2.各モデルの関連付け、フォロー機能のメゾット作成
作成されたマイグレーションファイルを確認します。

db/migrate
class CreateFollowRelationships < ActiveRecord::Migration[5.1]
  def change
    create_table :follow_relationships do |t|
      t.references :follower, foreign_key: { to_table: :users }
      t.references :following, foreign_key: { to_table: :users }

      t.timestamps
    end
    add_index :follow_relationships, [:follower_id, :following_id], unique: true
  end
end

ここでforegin_key(外部キー)を設定しています。よく外部キーを設定する時に使う「foreign_key: true」としてしまうと存在しないfollowersデーブルを参照してしまうので「to_table: :users」として参照はusersテーブルのidであることを指定してます。
外部キーを設定することによりindexの追加と存在するuserのidのみをdbに保存するようになります。
また、unique: trueとすることにより、同じ組み合わせでデータを保存するのを防ぐようにしているので同じユーザーを2回フォローできなくしています。

各モデルの関連付けはこのような形になります。

app/model/follow_relationship.rb
belongs_to :follower, class_name: "User"
belongs_to :following, class_name: "User"

validates :follower_id, presence: true
validates :following_id, presence: true
app/model/user.rb
has_many :following_relationships,foreign_key: "follower_id", class_name: "FollowRelationship",  dependent: :destroy
has_many :followings, through: :following_relationships
has_many :follower_relationships,foreign_key: "following_id",class_name: "FollowRelationship", dependent: :destroy
has_many :followers, through: :follower_relationships

それぞれのオプションの意味は
foreign_key: "follower_id"  外部キーの名前を直接参照します。
※マイグレーションで設定した物はあくまでもUserのidがこのカラムに入りますと指定しただけなのでフォローしているユーザーを検索等するときはfollowingカラムを参照するよと指定しないといけません。
class_name:関連名(following_relationships)としていますがそのようなテーブルは存在しないので実際に参照するテーブルであるFollowRelationshipを指定してあげます。
dependent: :destroy Userが削除された時にrelationshipのuseridが削除されるようにしています。

has_many :followings, through: :following_relationships
とすると先ほど指定したfollowing_relationshipsを通してフォローしているユーザーのidを取得できるようになります。

###フォロー関連のメゾットを作成します。

app/model/user.rb
#すでにフォロー済みであればture返す
  def following?(other_user)
    self.followings.include?(other_user)
  end
  
  #ユーザーをフォローする
  def follow(other_user)
    self.following_relationships.create(following_id: other_user.id)
  end
  
  #ユーザーのフォローを解除する
  def unfollow(other_user)
    self.following_relationships.find_by(following_id: other_user.id).destroy
  end

#3.コントローラーの作成
まずはコントローラーを作成します。

rails g controller follow_relationships

:
続いてルーディングの設定です。フォロー/フォロワーの一覧ページ用のアクションはUserオブジェクト絡みの機能なのでコントローラー内に記載します。
そのために、resources: userにmemberを使用してルートを追加します。
memberを使用するとURL内にユーザーを識別するidが追加されます。
idは後ほどUserコントローラーの対応するアクションないで必要となるのでmemberでルートを追加しないといけません。(idを追加しないcollectionというメゾットもあります。)

confing.routes.rb
resources :users do
      member do
        get :following, :followers
      end
    end
resources :follow_relationships, only: [:create, :destroy]

一覧ページ用のアクションはこのようになります。(kaminariのページネーション機能を利用しています。)
※それぞれ対応するビューを作成してください。

app/controllers/users.rb
def followings
    @user =User.find(params[:id])
    @users =@user.followings.page(params[:page]).per(5)
    render 'show_followings'
  end
  
  def followers
    @user =User.find(params[:id])
    @users =@user.followers.page(params[:page]).per(5)
    render 'show_followers'
  end

follow_relationshipのコントローラーはこんな感じです。
followとunfollowが先ほどUser.rbに記載したメゾットになります。

app/controllers/follow_relationships.rb
def create
    @user =User.find(params[:follow_relationship][:following_id])
    current_user.follow(@user)
    respond_to do |format|
      format.html {redirect_back(fallback_location: root_url)}
      format.js
    end
  end
  
  def destroy
    @user = User.find(params[:follow_relationship][:following_id])
    current_user.unfollow(@user)
    respond_to do |format|
      format.html {redirect_back(fallback_location: root_url)}
      format.js
    end
  end
end

respond_to do |format|はリクエストの種類によってフォロー/アンフォローした際にレンダリングするビューを指定しています。クライアント側の設定でjsが無効になっている場合にhtml側の処理を書いておかないとエラーが出るので注意してください。
[format.html]ではredirect_backメゾットで直前のページを表示、表示できなければroot_urlに戻しています。※redirect_toを使用してflashを表示するパターンも設定できます。
jsでリクエストがくれば[format.js]を通って、follow_relationshipのcreate.js.erbへ処理が向かいます。format.jsの後にrender先を指定しない場合は自動的にアクションに対応するjsファイルを読み込みます。この場合はこの後記載するcreate.js.erbが該当するファイルになります。

app/controllers/follow_relationships.rb
flashバージョン
def create
    @user =User.find(params[:follow_relationship][:following_id])
    current_user.follow(@user)
    respond_to do |format|
      format.html { redirect_to @user, flash: {success: 'フォローしました!'} }
      format.js
    end
  end

#4.フォローボタン作成
コントローラーのアクションができたのフォローボタンを作成します。その前にフォロー一覧とフォロワー一覧ページを作成しておきます。今回はUserのshowページにリンクを用意してます。※フォロー機能には直接の関係はありません。

app/views/users/show_html
<%= link_to "フォロー(#{@user.followings.count})", followings_user_path(@user), class: "nav-link" %>
  
<%= link_to "フォロワー(#{@user.followers.count})", followers_user_path(@user), class: "nav-link" %>

フォローボタンも同じくshowページに配置しますがコードがグチャグチャにならないようにボタンのフォームはrenderしてます。

app/views/users/show_html
<% if logged_in? && @user != current_user%>
  <div id="follow_form">
    <% if current_user.following?(@user) %>
      <%= render "unfollow" %>
    <% else %>
      <%= render "follow" %>
    <% end %>
  </div>
<% end %>

if logged_in? && @user != current_user
→ログイン済みであることと、このshowページが自分以外のユーザーのページであることを確認しています。&&はAND条件を表すので全ての条件を満たさないとボタンが
表示されません。!=は等しくないときtureとなります。
if current_user.following?(@user)
→ログイン中のユーザーがすでにフォロー済みであればunfollowをまだフォローしていなければfollowボタンを表示するようにしています。
次は実際のボタンの中身です。

app/views/users/follow_html
<%= form_with(model: current_user.following_relationships.build) do |f| %>
  <%= f.hidden_field :following_id, value: @user.id %>
  <%= f.submit "フォローする", class: "btn btn-outline-secondary" %>
<% end %>

from_withはデフォルトでAjax通信をするようになっています。
modelにはモデルクラスのインスタンス(@userとか)を渡す必要があるので、空のfollow_relationshipインスタンスを作成してpostリクエストを動作させ、follow_relationshipのコントローラーのcreateアクションに渡します。(userを作成するときにコントローラーのアクションでUser.newをする意味と同じです。多分)
f.hidden_fieldでfollowing_idにユーザーidを入れるようにしています。
※クライアント側には見えずに、ボタンを押した時にパラメーターに入ります。

app/views/users/unfollow_html
<%= form_with(model: current_user.following_relationships.find_by(following_id: @user.id),method: :delete) do |f| %>
  <%= f.hidden_field :following_id %>  
  <%= f.submit "フォロー", class: "btn btn-outline-secondary" %>
<% end %>

フォロー解除ボタンはフォローボタンとは違ってdeleteリクエストを指定してdestroyアクションを動作させています。

#5.jsファイルの作成
フォームからのPostリクエストをcreateアクションで処理したあとはcreate.js.erbにたどり着きます。そこでAjaxでレンダリングする部分(今回の場合はボタン)を指定します。create.js.erbにたどり着くのはcreateアクション後なのでレンダリングするのはフォロー解除ボタンになります。

app/views/follow_relationships/create.js.erb
$("#follow_form").html("<%= j(render("users/unfollow")) %>");
app/views/follow_relationships/destroy.js.erb
$("#follow_form").html("<%= j(render("users/follow")) %>");

それぞれ対応するフォームを読み込みます。follow_formはshowページに記載したcssのidです。
以上でフォロー機能が動作するようになりました。
最後にフォロー機能の流れだけまとめで記載しておきます。

6.フォロー機能の流れ
実際にフォローボタンを押すとこのような動きをサーバーのログから確認できます。
※流れを追うだけなので一部省略してます。

Started POST "/follow_relationships"→フォローボタンが押される
Processing by FollowRelationshipsController#create as JS
Parameters:"follow_relationship"=>{"following_id"=>"3"}→パラメータにfollow_relationshipをキーとしたフォロー対象のユーザーidが入ります。
コントローラーのアクション完了!
Rendering follow_relationships/create.js.erb→アクション後にjsのファイル読み込み。
Rendered users/_unfollow.html.erb (2.1ms)
Rendered follow_relationships/create.js.erb (3.2ms)

以上となります。
かなり長くなってしまいましたが、最後までお読みいただきありがとうございました。

18
22
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
22

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?