1
0

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.

Rails初学者によるRailsチュートリアル学習記録⑯ 第14章

Posted at

#目次

#1. はじめに

  • この記事は、Rails初学者の工業大学三年生がRailsチュートリアルの学習記録を
    つけるための記事です。
  • 筆者自体がRailsやWebについて知識が少ないので、内容の解釈などに
    間違いがある可能性があります。(その時はコメントで指摘してくださると助かります!)
  • Railsチュートリアル内ではRailsの内容以外にも、gitでのバージョン管理やHerokuを使ったデプロイも
    学習しますが、gitに関しては既に私が学習済みのため学習記録には記述しません。
  • 演習の記録も省略します。
  • コードや説明のための画像はRailsチュートリアル内から引用しています。

#2. 第14章の概要
この章では、他のユーザーをフォローする機能を実装し、
自分がフォローしているユーザーの投稿と自分の投稿を表示するフィードを実装します。

フォローはユーザーのプロフィールページにボタンを配置し、
ボタンをクリックしたらフォローする仕様にします。
フォローしているユーザーのフォローボタンは、Unfollowボタンに変えてそのボタンでフォロー解除できるようにします。
また、プロフィールページに、そのユーザーフォロー数・フォロワー数を表示します。
最後に、自分がフォローしているユーザーのマイクロポストと、自分のマイクロポストを表示する
フィード機能を実装して終了です。

  1. ユーザーのフォロー
    1. データモデルの設計
    2. フォローしているユーザーの関連付け
    3. フォロワーの関連付け
  2. フォローしているユーザー、フォロワー
    1. フォロー数・フォロワー数を表示する
    2. フォローをするメソッド
  3. フォローしているユーザー、フォロワーを一覧する
    1. 一覧ページを表示するアクション
    2. 一覧ページのビュー
    3. フィード機能の実装

#3. 学習内容
###1. ユーザーのフォロー

####1-1. データモデルの設計
フォロー機能を実装するためには、データモデルの構成が必要です。
一見、has_manyの関連付けを用いて、1人のユーザーが複数のユーザーをフォローして、
1人のユーザーには複数のフォロワーがいるというように表現できそうです。

しかし、この方法だとデータモデルは以下の図のようになります。(Railsチュートリアル内から引用)
image.png
このデータモデルの欠陥は、無駄が多いことです。
フォローしているユーザーそれぞれの情報があり、この情報はusersテーブルに載っている情報と同じです。
この状態ではusersテーブルが更新された時、図のfollowingテーブルの更新も必要になってしまいます。

この欠陥をなくすため、図のfollowed_idを用いてusersテーブルを参照する仕様にします。
よって、usersテーブルからidを取得し、そのユーザーにフォローされているユーザーのidも
usersテーブルから取得した、第3のテーブルを図のusers, followingテーブルの間に配置します。

以下の図が欠陥を無くしたデータモデルの図です。
image.png
図の中にも書かれているように、この3つのテーブルの関連付けをhas_many throughという関連付けを行います。
詳細は後述するので、第3のテーブルのモデルとなるRelationshipモデルを以下に示します。

属性名 データ型
id integer
follower_id integer
followed_id integer
created_at datetime
updated_at datetime

最後に、このモデルを生成するために、以下のコマンドをを実行します。
rails generate model Relationship follower_id:integer followed_id:integer

####1-2. フォローしているユーザー/フォロワーの関連付け
ここから、UserモデルとRelationshipモデルの関連付けを行います。
1人のユーザーは複数のリレーションシップを持つため、has_mebyの関連付けが行われます。
リレーションシップ側から見たユーザーとの関連は、1つのリレーションシップの中で
フォローしているユーザーというのは1人なので、1対1のbelongs_toの関連付けが行われます。

上記の2つの関連付けを反映させたコードが以下のコードです。

app/models/user.rb
class User < ApplicationRecord
  has_many :microposts, dependent: :destroy
  has_many :active_relationships, class_name: "Relationship", #リレーションシップとの関連付け
                                  foreign_key: "follower_id",
                                  dependent: :destroy
.
.
.
end
app/models/relationship.rb
class Relationship < ApplicationRecord
  belongs_to :follower, class_name: "User" # フォローをしているユーザーとの関連付け
  belongs_to :followed, class_name: "User" # フォロワーの関連付け
end

Userモデル側のhas_manyメソッドは、他の関連付けと書き方が異なっています。
まず、なぜrelationshipsテーブルが、active_relationshipsテーブルという名前で関連付けられているのか、
というところから説明します。

relationshipsテーブルには、「フォローしているユーザー」の集合を取得するという役割と、
「フォロワー」の集合を取得するという役割の2つを持っています。
そして、役割ごとに集合を取得するときに参照する属性が変わります。

この変化に対応するために、フォローしているユーザーの集合を取るときには、relationshipsテーブルを、
active_relationshipsテーブルとみなし、
フォロワーの集合を取るときには、passive_relationshipsテーブルとみなします。

上記のことから、has_manyメソッドに渡されているclass_nameの意味が分かります。
class_nameは、テーブル名とクラス名が一致していないため、Railsにテーブルがどのクラスに対応しているかを
伝えなければいけません。

今回は、active_relationshipsテーブルはRelationshpクラスと対応しているので、その名前を渡しています。
foreign_keyはユーザーのidを扱う属性の属性名を渡しています。
デフォルトでは、user_idとなります。

active_relationshipsテーブルではフォローしているユーザーの属性とフォロワーの属性という2つの属性があり、
どちらもユーザーのidを値として持っているため、user_idでは区別できません。
よって、それぞれfollower_idとfollowed_idという属性名になるようにします。

これでUserとRelationshipの関連付けの一部ができました。
これにより、以下のメソッドが使用できます。

メソッド 用途
active_relationship.follower フォロワーを返す
active_relationship.followed フォローしているユーザーを返す
user.active_relationships.create
(followed_id: other_user.id)
userに紐づけてリレーションシップを作成・登録する
user.active_relationships.create!
(followed_id: other_user.id)
user に紐づけてリレーションシップを作成・登録する
(失敗時にエラーを出力)
user.microposts.build(arg) user に紐付いた新しい Micropost オブジェクトを返す
user.active_relationships.build
(followed_id: other_user.id)
user に紐づけて新しいRelationshipオブジェクトを返す

ここまでで、ユーザーとリレーションシップの関連付けができました。
ここからhas_many throughを使用したフォローしているユーザーとフォロワーの関連付けを行います。
テーブル単位で考えると、これまでに行った関連付けは、usersテーブルとrelationshipsテーブルの関連付けで、
ここから行うのがusersテーブルとfollowingテーブル(自分がフォローしているユーザーの集合のテーブル)の関連付けです。

has_many throughメソッドは関連付けるテーブルと、その中間テーブルを渡すことで、
中間テーブルを通して2つのテーブルを関連付けるという処理を行います。

ここではusersテーブルとfollowingテーブルを、active_rerationshipsテーブルを通して
関連付けを行いたいのでコードは以下のようになります。

app/models/user.rb
class User < ApplicationRecord
  has_many :microposts, dependent: :destroy
  has_many :active_relationships, class_name: "Relationship",
                                  foreign_key: "follower_id",
                                  dependent: :destroy
  # ユーザーとフォロワーの関連付け
  has_many :following, through: :active_relationships, source: :followed
.
.
.
end

上記のコード内で2つのテーブル名とは別に存在する、source: :followedというのは、
followingテーブルのユーザーはfollowed属性のユーザーのidが元になっていることを表しています。

####1-3. フォロワーの関連付け
1-2までで、ユーザーとリレーションシップの関連付け、
ユーザーとそのユーザーがフォローしているユーザーとの関連付けができました。

リレーションシップを実装する上で、あと1つ足りない要素があります。
それは、フォローされているユーザー、つまりフォロワーから見た時のユーザーとの関連付けです。

これは、先ほどのフォローしているユーザーとフォロワーの関連付けの逆になります。
この関連付けの目的は、user.followersメソッドを使用できるようにすることです。
このメソッドでは、自分のフォロワーの配列を取得します。

このメソッドを実装するために必要な情報は、relationshipsテーブルに既にあります。
フォローしているユーザーとの関連付けに使用した、active_relationshipsの2つの列を
入れ替えればここでも活用できます。

image.png
1-1で記載した、図14-7と比較すると、followed_idとfollower_idの位置が逆になった、
passive_relationshipsというテーブルが作成されていることが分かります。

このようなデータモデルを実装するために、以下のコードで関連付けを追加します。

app/models/user.rb
class User < ApplicationRecord
  has_many :microposts, dependent: :destroy
  has_many :active_relationships, class_name: "Relationship",
                                  foreign_key: "follower_id",
                                  dependent: :destroy

  # フォロワーとリレーションシップの関連付け
  has_many :passive_relationships, class_name: "Relationship",
                                   foreign_key: "followed_id",
                                   dependent: :destroy

  has_many :following, through: :active_relationships, source: :followed
  # フォロワーとユーザーの関連付け
  has_many :followers, through: :passive_relationships, source: :follower
.
.
.
end

これでユーザーとフォロワーの関連付けができ、
フォロワーを取得するメソッドが使用できるようになりました。

###2.フォローしているユーザー、フォロワー
####2-1. フォロー数・フォロワー数を表示する
Relationshipモデルが完成したので、リレーションシップを作成することで
フォローをするという動作ができるようになりました。

ここからは、フォロー機能を実装していきます。
まずはフォローボタンを配置したり、フォロー数・フォロワー数が見れるフォームを
表示するといったUIの実装を行います。

この段階で、後で作成するフォロー・フォロワー一覧ページ用のルーティングと
Relationshipsリソース用のルーティングを実装します。

追加するルーティング
Rails.application.routes.draw do
  resources :users do
    member do
      get :following, :followers
    end
  end

  resources :relationships, only: [:create, :destroy]

end

このルーティングは、memberメソッドを使用しています。
これによりユーザーidを含んだURLを扱えるようになり、
/users/1/following(フォロー一覧ページ)、/users/1/followers(フォロワー一覧ページ)という
URLを使用できるようにしています。

ここからいくつかのパーシャルを作成していきます。
まずは、下の画像のようにフォロー数とフォロワー数を表示するパーシャルです。
image.png

この画像のように現在のフォロー数・フォロワー数を取得するには、following・followersメソッドを使って
それぞれの集合を取り出してからcountメソッドを呼び出します。

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>

このパーシャルはHomeページとプロフィールページに配置します。
image.png

次に作成するパーシャルは、Follow / unfollowボタン用のパーシャルです。

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 %>

このパーシャルでは、自分以外のユーザーのプロフィールページにに
Follow / unfollowボタンを表示させます。

<% if current_user.following?(@user) %>でそのユーザーをフォローしていなければ、
フォローをするためのfollow_formというパーシャルを表示させます。
フォローしていない場合は、unfollowというパーシャルを表示させます。

それらのパーシャルのコードを下に記載します。

app/views/users/_follow.html.erb
<%= form_with(model: current_user.active_relationships.build, local: true) 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_with(model: current_user.active_relationships.find_by(followed_id: @user.id),
              html: { method: :delete }, local: true) do |f| %>
<%= f.submit "Unfollow", class: "btn" %>
<% end %>

この2つのパーシャルは、どちらもリレーションシップを操作しています。
followでは、リレーションシップをcreateして、unfollowではリレーションシップをdestroyしています。
image.png

image.png

####2-2. フォローをするメソッド
フォロー機能を扱うためのUIが準備できたので、ボタンの動作を実装します。
RailsチュートリアルではAjaxを使用したフォロー動作を実装していますが、
ここでは、Ajaxを使用しない処理を実装します。

フォローとフォロー解除は、それぞれリレーションシップの作成と削除に対応しています。
よって、はじめにRelationshipsコントローラーをrails generate controller Relationshipsで生成します。

このコントローラー内で、フォロー・フォロー解除を実装するので、
その時に使用するユーザーをフォローするメソッドとフォロー解除のためのメソッド、
そしてフォロー中になったことを確認するためのメソッドを実装します。

app/models/user.rb
# ユーザーをフォローする
def follow(other_user)
  following << other_user
end

# ユーザーをフォロー解除する
def unfollow(other_user)
  active_relationships.find_by(followed_id: other_user.id).destroy
end

# 現在のユーザーがフォローしてたら true を返す
def following?(other_user)
  following.include?(other_user)
end

followメソッドでは引数に渡されたユーザーを、followingに追加しています。
フォロー解除は、引数に渡されたユーザーのidを
active_relationshipsテーブル内で検索して削除しています。

メソッドが準備できたので、Relationshipsコントローラのcreateアクションとdestroyアクションを作成します。
これらのアクションはそこまで複雑でなく、指定されたユーザーをフォロー、フォロー解除をするだけです。

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

これらのアクションはbefore_actionフィルターをかけ、
ログインしているユーザーのみが実行できるようにしています。
この2つのアクションを実装することでフォローボタンが動作するようになります。

フォロー機能が実装でき、フォロー数・フォロワー数も表示できるようになりました。
この次はフォロー、フォロワーの一覧ページの作成と、
自分がフォローしているユーザーのマイクロポストを表示するフィードという機能の実装を行います。

###3. フォローしているユーザー、フォロワーを一覧する
####3-1. 一覧ページを表示するアクション
フォロー数、フォロワー数のリンクを動くようにするために、
それぞれの一覧ページを作成します。

これら2つのページは表示される一覧が、フォローしているユーザーの一覧か、
フォロワーの一覧かという点しか違いが無く、そのほかのレイアウトなどの要素は全く同じです。
そのような場合に2つのビューを作成し、使い分けるのは効率が悪いです。

ここでは、受け取ったユーザーの集合を一覧で表示するというビューを1つ作成し、
フォローしているユーザーの集合とフォロワーの集合をそれぞれ渡すことで、
1つのビューから2つの一覧ページを表示できるようにします。

渡す集合の切り替えは、usersコントローラのfollowingアクションとfollowersアクションの
2つのアクションを使って行います。

app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :logged_in_user, only: [:index, :edit, :update, :destroy,
                                        :following, :followers]
.
.
.
  def following  # フォローしているユーザーの一覧ページを作成
    @title = "Following"
    @user = User.find(params[:id])
    @users = @user.following.paginate(page: params[:page])  # フォローしているユーザーの集合を取得する
    render 'show_follow'
  end

  def followers  # フォロワーの一覧ページを作成  
    @title = "Followers"
    @user = User.find(params[:id])
    @users = @user.followers.paginate(page: params[:page])  # フォロワーの集合を取得する
    render 'show_follow'
  end

end

2つのアクションの最後のrenderメソッドを見てもらうと、どちらも表示しているビューが同じことが分かります。
重要な違いは、その1行上の@users変数の定義の違いです。

followingアクションではフォローしているユーザーの集合を取得しているのに対し、
followersアクションはフォロワーの集合を取得しています。

この変数の定義の違いにより、同じビューでも表示される内容が変わります。

####3-2. 一覧ページのビュー
2つのアクションから呼び出されるビューが以下のコードです。

app/views/users/show_follow.html.erb
<% provide(:title, @title) %>
<!-- サイドバー開始 -->
<div class="row">
  <aside class="col-md-4">
    <section class="user_info">
      <%= gravatar_for @user %>
      <h1><%= @user.name %></h1>
      <span><%= link_to "view my profile", @user %></span>
      <span><b>Microposts:</b> <%= @user.microposts.count %></span>
    </section>
    <section class="stats">
      <%= render 'shared/stats' %>
      <% if @users.any? %>
        <div class="user_avatars">
          <% @users.each do |user| %>
            <%= link_to gravatar_for(user, size: 30), user %>
          <% end %>
        </div>
      <% end %>
    </section>
  </aside>
<!-- サイドバー終了 -->

<!-- ユーザー一覧開始 -->
  <div class="col-md-8">
    <h3><%= @title %></h3>
    <% if @users.any? %>
      <ul class="users follow">
         <%= render @users %>
      </ul>
      <%= will_paginate %>
    <% end %>
  </div>
</div>
<!-- ユーザー一覧終了 -->

長いコードですが、重要なのは下のブロックのユーザー一覧です。
先ほどアクションで定義した@users変数を表示しています。

####3-3. フィード機能の実装
最後にフォロー機能を活用したフィードを実装していきます。
フィードを実装する上で重要なポイントは、どのように自分とフォローしているユーザーのマイクロポストを取得するかです。

必要な処理としては、micropostsテーブルから、自分がフォローしているユーザーのidを持つマイクロポストを
選択する処理です。
クエリとして書き表すと以下のようになります。
SELECT * FROM microposts WHERE user_id IN (<list of ids>) OR user_id = <user id>

このクエリを利用するためには、フォローしているユーザーのidの配列が必要です。
配列を取得するにはmapメソッドを使用します。
user.following.map(&:id)と記述すると、user.followingにある各要素のidを配列として扱うことができます。

この方法は便利でよく使用されるため、上記の方法をメソッドにしたfollowing_idsメソッドが
Active Record内で用意されています。
このメソッドはhas_many :followingの関連付けを行った時に生成されたものです。

following_idsメソッドを使用して最初のクエリを書き換えたfeedアクションが以下のコードです。

app/models/user.rb
def feed
  Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)
end

これで自分がフォローしているユーザーと自分のマイクロポストすべて取得できるようになりました。
しかし、このコードは1つの問題を抱えています。

その問題は、このアプリの利用者が増え、フォローしているユーザーが
5000人や1万人などになったときに動作が遅くなるという問題です。
上記のコードはデータベースに対し、フォローしているすべてのユーザーの問い合わせを行い、
なおかつ配列を作るときにも問い合わせを行っています。

この問題を解決するために、SQLのサブセレクトという機能を使用します。
サブセレクトとはWHERE句の中にSELECT文を用いるという手法です。
今までは集合を取得し、その集合がuser_idに内包されているかを確認するという2回の問い合わせを行っていました。
サブセレクトにより2回の問い合わせを1回に減らすことができます。

app/models/user.rb
def feed
  following_ids = "SELECT followed_id FROM relationships
                   WHERE follower_id = :user_id"

  Micropost.where("user_id IN (#{following_ids})
                  OR user_id = :user_id", user_id: id)
end

上記のコードがリファクタリング後のコードです。
following_idsにSQLを代入して扱うことで、フォローしているユーザーの集合を取得する処理をRailsではなく
データベース内で行います。
これによってより効率的にマイクロポストを取得できるようになりました。

最後に取得したマイクロポストを表示するビューのコードを記載して終わりにします。

app/views/shared/_feed.html.erb
<% if @feed_items.any? %>
  <ol class="microposts">
    <%= render @feed_items %>
  </ol>
  <%= will_paginate @feed_items %>
<% end %>

このパーシャルをHomeページに配置してフィード機能の完成です。

#4. 終わりに
この章でRailsチュートリアルが終わりました。
約3か月間で、実際に利用できるWebアプリケーションを開発しました。
最後の方は特に難しい内容が多く、内容を100%理解できたわけではないですが、
個人開発を行うための基盤が出来上がったという気持ちです。

Railsチュートリアルの学習方法を調べてみると、2回通して勉強をしたという方をよく見かけますが、
就活が目の間に近づいてきているのでこの後はポートフォリオ用のアプリの開発に移る予定です。

また、Qiitaへの投稿もこの学習記録が初めてでした。
誰かの役に立つ記事とは到底言えないと思っていますが、個人的には勉強のモチベーション維持に繋がったり、
文章を書く経験になったので満足しています。

これから個人開発を進めていくときには、誰かの役に立てるような記事を書ければと思っています。

1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?