まさ@ブログ書き込み中

まさ@ブログ書き込み中

まさの旅、英語、プログラミング、プライベートについて、色々記録しています。

Railsチュートリアル14章 (2) 【コントローラー&ビュー編】

 

おはようございます、まさです。

この投稿でここ最近続いてきたRailsチュートリアルまとめシリーズを終えます。

今回は、前回の記事のモデルの関連づけを行ったことを踏まえて、フォロー機能やフォローフォームなどについてまとめていきます。

 

 

統計とFollowフォーム

ルーティング

まずは、「following」や「followers」というリンクをクリックすると対応するフォロワーやフォローしている人たちを表示できるよう、ルーティングをします。

config/routes.rb

  resources :users do
    member do
      get :following, :followers
    end
  end

 

このmemberメソッドで/users/1/following や /users/1/followers のようなルーティングが可能になります。このfollowingとfollowersはActiveRecordで関連づけされたあのfollowingとfollowersですね。

 

統計のパーシャル

次に、そのfollowingやfollowersのリンクをがある統計情報のパーシャルをつくります。

app/views/shared/_stats.html.erb

<% @user ||= current_user %>
<div class="stats">
  <a href="<%= following_user_path(@user) %>">
    <strong id="following" class="stat">
      <%= @user.following.count %>
    </strong>
    following
  </a>
  <a href="<%= followers_user_path(@user) %>">
    <strong id="followers" class="stat">
      <%= @user.followers.count %>
    </strong>
    followers
  </a>
</div>

 

特に真新しい点はありませんが、

<% @user ||= current_user %>

の意味を少し説明します。

 

この統計情報(_stats.html.erb)はHomeページにもProfileページにも使われる予定です。なのでProfileページ(params[:id]によって@userに値が与えられている)の場合とHomeページ(current_userによってユーザーが把握されている)場合のどちらにも対応ができるように実装されています。

 

フォロー・フォロー解除のフォーム

次に、フォロー・フォロー解除のフォームを実装していきます。

まずは以下のように、フォローとアンフォローに分けてパーシャルを振り分けるためのパーシャルをつくります。

app/views/users/_follow_form.html.erb

<% unless current_user?(@user) %>
  <div id="follow_form">
  <% if current_user.following?(@user) %>
    <%= render 'unfollow' %>
  <% else %>
    <%= render 'follow' %>
  <% end %>
  </div>
<% end %>

 

ユーザーをフォローするフォームと解除するフォームは以下のとおりです。

app/views/users/_follow.html.erb

<%= form_for(current_user.active_relationships.build) do |f| %>
  <div><%= hidden_field_tag :followed_id, @user.id %></div>
  <%= f.submit "Follow", class: "btn btn-primary" %>
<% end %>

app/views/users/_unfollow.html.erb

<%= form_for(current_user.active_relationships.find_by(followed_id: @user.id),
             html: { method: :delete }) do |f| %>
  <%= f.submit "Unfollow", class: "btn" %>
<% end %>

 

ここではform_for(ヘルパーメソッド)を使ってRelationshipモデルオブジェクトを操作しています。僕はここで少し混乱しました。なぜなら、今までは

 

<%= form_for(@user) do |f| %>

 <%= f.text-field :name %>

<%= end %>

 

として@userにform_forとして値を送る形しか見ておらず(params[:user][:name]のようにして取得できます)、今回のコードでは

 

<%= form_for(current_user.active_relationships.build) do |f| %>

<%= form_for(current_user.active_relationships.find_by(followed_id: @user.id), html: { method: :delete }) do |f| %>

 のようになっていて、form_forに与えられた引数を単純にシンボルとして考えられない形になっていたからです。

 

しかし、form_forが引数に何らかの値を任意のHTTPリクエストで送るメソッドだと考えると納得がいきます。

 

フォローフォームのcurrent_user.active_relationships.buildとはRelationshipモデルを生成(create)する操作ですが、followed_idがまだ与えられていません。

 

そこで

<%= hidden_field_tag :followed_id, @user.id %>

という値をform_forでPOSTリクエストで送ることによって、関係の生成が完了します。

 

一方、アンフォローフォームのcurrent_user.active_relationships.find_by(followed_id: @user.id)ではすでにあるリレーションシップを@userから見つけ出し、DELETEリクエストを送ることによってリレーションシップの削除(destroy)を行っています。

 

 

Follow/Unfollowボタンを動作させる 

次に、フォローボタンの機能部分の実装についてまとめていきます。

$ rails generate controller Relationships

でコントローラーを作成して、

 

app/controllers/relationships_controller.rb

class RelationshipsController < ApplicationController
  before_action :logged_in_user

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

  def destroy
    user = Relationship.find(params[:id]).followed
    current_user.unfollow(user)
    redirect_to user
  end
end

のようにcreateとdestroyアクションを定義します。当たり前ですが、これらのアクションはフォロー・アンフォローするユーザーがいないといけませんので、before_actionメソッドでlogged_in_userを宣言しています。

 

これだけでも問題ないのですが、上のコードは、Relationshipモデルの生成や削除を行った後、その結果をページに反映させるために同じページにリダイレクトをしています。再度ページを描画する際に変更が加えられたリレーションシップが反映されるというわけですね。

 

しかし、Ajaxという技術を使えば「Webページからサーバーに非同期で、ページを移動することなくリクエストを送信することができます」。わざわざ同期しなくていいというわけですね。

 

RailsAjaxを使うようにするには

form_for ..., remote: true

とやるだけそうな。便利すぎw

 

これによって、HTMLフォームは以下のように生成されるようです。

<form action="/relationships/117" class="edit_relationship" data-remote="true"
      id="edit_relationship_117" method="post">
  .
  .
  .
</form>

 このdata-remote="true"によって「JavaScriptによるフォーム操作を許可することをRailsに知らせる」ことになるみたいです。

 

また、HTTPリクエストとAjaxリクエストによって応答を変えるよう、Relationshipコントローラーのcreate, destroyアクションのリダイレクトについてのコードを以下のように変えます。

respond_to do |format|
  format.html { redirect_to user }
  format.js
end

 

またAjaxJavaScriptに関連する技術なので、ブラウザ側でJavaScriptが無効になってAjaxリクエストが送れない場合でもうまく動くようにするために以下のコードを付け加えます。

config/application.rb

require File.expand_path('../boot', __FILE__)
.
.
.
module SampleApp
  class Application < Rails::Application
    .
    .
    .
    # 認証トークンをremoteフォームに埋め込む
    config.action_view.embed_authenticity_token_in_remote_forms = true
  end
end

 

これで基本的な設定は完了しましたが、まだAjaxリクエストが送られた際に何をするかを定義していません。次にこれらを実装していきましょう。

app/views/relationships/create.js.erb

$("#follow_form").html("<%= escape_javascript(render('users/unfollow')) %>");
$("#followers").html('<%= @user.followers.count %>');

app/views/relationships/destroy.js.erb

$("#follow_form").html("<%= escape_javascript(render('users/follow')) %>");
$("#followers").html('<%= @user.followers.count %>');

 

このコードの意味ですが、create.js.erbではフォローフォームをunfollowパーシャルで更新し、フォロワーのカウントをERbで更新しています。同じく、destroy.js.erbではフォローフォームをfollowパーシャルで更新しています。

 

 

ステータスフィード

最後に、自分の投稿とフォローしているユーザーの投稿を一度に表示するステータスフィードについてまとめます。

 

基本的には以下のように

app/models/user.rb

  # ユーザーのステータスフィードを返す
  def feed
    Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)
  end

feedメソッドでフィードを作成するデータを取ってくる際に、following_idsメソッドを利用しています。

 

このメソッドについてですが、本章では

このfollowing_idsメソッドは、has_many :followingの関連付けをしたときにActive Recordが自動生成したものです。これにより、user.followingコレクションに対応するidを得るためには、関連付けの名前の末尾に_idsを付け足すだけで済みます。

と説明されています。

 

サブセレクト

本章によると、このコードには問題点があります。

コードの問題点は、following_idsでフォローしているすべてのユーザーをデータベースに問い合わせし、さらに、フォローしているユーザーの完全な配列を作るために再度データベースに問い合わせしている点です。

 

そこで解決策としてSQLのサブセレクト(subselect)を利用します。

  # ユーザーのステータスフィードを返す
  def feed
    Micropost.where("user_id IN (:following_ids) OR user_id = :user_id",
     following_ids: following_ids, user_id: id)
  end

 

following_idsは以下のような意味があるので

following_ids = "SELECT followed_id FROM relationships
                 WHERE  follower_id = :user_id"

丸括弧でくくり、集合のロジックを (Railsではなく) データベース内に保存させ効率的にデータを取得させるようにできます。

 

そうすることによって、SQL文としては以下のような意味になります。

SELECT * FROM microposts
WHERE user_id IN (SELECT followed_id FROM relationships
                  WHERE  follower_id = 1)
      OR user_id = 1

 

 以上で、Railsチュートリアルの最終章である14章のまとめは終わりです。

長かったRailsチュートリアルのまとめもここで一旦終わりにします。

 

ではまた。