#はじめに
今回はフォロー機能を作成します。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.各モデルの関連付け、フォロー機能のメゾット作成
作成されたマイグレーションファイルを確認します。
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回フォローできなくしています。
各モデルの関連付けはこのような形になります。
belongs_to :follower, class_name: "User"
belongs_to :following, class_name: "User"
validates :follower_id, presence: true
validates :following_id, presence: true
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を取得できるようになります。
###フォロー関連のメゾットを作成します。
#すでにフォロー済みであれば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というメゾットもあります。)
resources :users do
member do
get :following, :followers
end
end
resources :follow_relationships, only: [:create, :destroy]
一覧ページ用のアクションはこのようになります。(kaminariのページネーション機能を利用しています。)
※それぞれ対応するビューを作成してください。
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に記載したメゾットになります。
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が該当するファイルになります。
※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ページにリンクを用意してます。※フォロー機能には直接の関係はありません。
<%= 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してます。
<% 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ボタンを表示するようにしています。
次は実際のボタンの中身です。
<%= 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を入れるようにしています。
※クライアント側には見えずに、ボタンを押した時にパラメーターに入ります。
<%= 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アクション後なのでレンダリングするのはフォロー解除ボタンになります。
$("#follow_form").html("<%= j(render("users/unfollow")) %>");
$("#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)
以上となります。
かなり長くなってしまいましたが、最後までお読みいただきありがとうございました。