#ユーザーをフォローする
■第14章
他のユーザーをフォロしたりフォロー解除したり、フォローしているユーザーの投稿をステータスフィードに表示する機能を追加する。
ここで学んだデータモデルは、今後自分用のWebアプリケーションを開発するときに必ず役に立つらしいので、理解できるよう頑張ります。。
##14.1 Relationshipモデル
has_many
を使えば実現できるようなものではない。問題は生じてしまう。
それを解決するためにhas_many through
について説明する。
###14.1.1 データモデルの問題 (および解決策)
いい加減なモデルを作ってしまうと、とにかく面倒で悲惨なことになる。
データの関係についてしっかり考えて作成する。
userとuserを繋ぐテーブルactive_relationshipsを作成する。
マイグレーション生成。
$ rails generate model Relationship follower_id:integer followed_id:integer
このリレーションシップは今後follower_id
とfollowed_id
で頻繁に検索することになるので、それぞれのカラムにインデックスを追加。
class CreateRelationships < ActiveRecord::Migration[5.1]
def change
create_table :relationships do |t|
t.integer :follower_id
t.integer :followed_id
t.timestamps
end
add_index :relationships, :follower_id
add_index :relationships, :followed_id
add_index :relationships, [:follower_id, :followed_id], unique: true
end
end
一番下のadd_indexは、uniqueと書かれている。これは必ずユニークであることを保証する仕組み。
relationships
テーブルを作成するためにマイグレーション。
rails db:migrate
###14.1.2 User/Relationshipの関連付け
UserとRelationshipの関連付けを行う。
能動的関係に対して1対多 (has_many
) の関連付けを実装。
class User < ApplicationRecord
has_many :microposts, dependent: :destroy
has_many :active_relationships, class_name: "Relationship",
foreign_key: "follower_id",
dependent: :destroy
.
.
.
end
リレーションシップ/フォロワーに対してbelongs_to
の関連付けを追加。
class Relationship < ApplicationRecord
belongs_to :follower, class_name: "User"
belongs_to :followed, class_name: "User"
end
###14.1.3 Relationshipのバリデーション
まずはテストを作成する。fixtureはいったん空にしておく。
Relationshipモデルのバリデーションをテスト。
require 'test_helper'
class RelationshipTest < ActiveSupport::TestCase
def setup
@relationship = Relationship.new(follower_id: users(:michael).id,
followed_id: users(:archer).id)
end
test "should be valid" do
assert @relationship.valid?
end
test "should require a follower_id" do
@relationship.follower_id = nil
assert_not @relationship.valid?
end
test "should require a followed_id" do
@relationship.followed_id = nil
assert_not @relationship.valid?
end
end
Relationshipモデルに対してバリデーションを追加。
class Relationship < ApplicationRecord
belongs_to :follower, class_name: "User"
belongs_to :followed, class_name: "User"
validates :follower_id, presence: true
validates :followed_id, presence: true
end
無事テストも成功。
###14.1.4 フォローしているユーザー
followingがフォロー先ユーザー、followersはフォロー元ユーザー。
Userモデルにfollowing
の関連付けを追加。
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
これにより、フォローしているユーザーを配列の様に扱えるようになった。
follow
やunfollow
といった便利メソッドを追加する。
テストから先に書く。
require 'test_helper'
class UserTest < ActiveSupport::TestCase
.
.
.
test "should follow and unfollow a user" do
michael = users(:michael)
archer = users(:archer)
assert_not michael.following?(archer)
michael.follow(archer)
assert michael.following?(archer)
michael.unfollow(archer)
assert_not michael.following?(archer)
end
end
実際にフォローするメソッド、フォロー解除するメソッド、
現在のユーザーがフォローしてたらtrueを返すメソッドを追加する。
# ユーザーをフォローする
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
テストも通過しました。
###14.1.5 フォロワー
user.followers
メソッドを追加する。フォローと似たようなことをしていく。
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
テストを書いていく。
require 'test_helper'
class UserTest < ActiveSupport::TestCase
.
.
.
test "should follow and unfollow a user" do
michael = users(:michael)
archer = users(:archer)
assert_not michael.following?(archer)
michael.follow(archer)
assert michael.following?(archer)
assert archer.followers.include?(michael)
michael.unfollow(archer)
assert_not michael.following?(archer)
end
end
テストもGREENに。今回は順調です。
##14.2 [Follow] のWebインターフェイス
フォロー/フォロー解除の基本的なインターフェイスを実装していく。また、フォローしているユーザーと、フォロワーにそれぞれ表示用のページを作成する。
###14.2.1 フォローのサンプルデータ
先にサンプルデータを自動作成できるようにしておけば、Webページの見た目のデザインから先にとりかかることができ、バックエンド機能の実装を後に回すことができるらしい。
サンプルデータにfollowing/followerの関係性を追加する。以下のコードを追加。
# リレーションシップ
users = User.all
user = users.first
following = users[2..50]
followers = users[3..40]
following.each { |followed| user.follow(followed) }
followers.each { |follower| follower.follow(user) }
###14.2.2 統計と [Follow] フォーム
最初に、プロフィールページとHomeページに、フォローしているユーザーとフォロワーの統計情報を表示するためのパーシャルを作成する。
次に、フォロー用とフォロー解除用のフォームを作成する。それから、フォローしているユーザーの一覧 ("following") とフォロワーの一覧 ("followers") を表示する専用のページを作成する。
Usersコントローラにfollowing
アクションとfollowers
アクションを追加する。
Rails.application.routes.draw do
root 'static_pages#home'
get '/help', to: 'static_pages#help'
get '/about', to: 'static_pages#about'
get '/contact', to: 'static_pages#contact'
get '/signup', to: 'users#new'
get '/login', to: 'sessions#new'
post '/login', to: 'sessions#create'
delete '/logout', to: 'sessions#destroy'
resources :users do
member do
get :following, :followers
end
end
resources :account_activations, only: [:edit]
resources :password_resets, only: [:new, :create, :edit, :update]
resources :microposts, only: [:create, :destroy]
end
ルーティングを定義したので、統計情報のパーシャルを実装する準備が整った。divタグの中に2つのリンクを含めるようにする。
<% @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にも追加する。
<% if logged_in? %>
<div class="row">
<aside class="col-md-4">
<section class="user_info">
<%= render 'shared/user_info' %>
</section>
<section class="stats">
<%= render 'shared/stats' %>
</section>
<section class="micropost_form">
<%= render 'shared/micropost_form' %>
</section>
</aside>
<div class="col-md-8">
<h3>Micropost Feed</h3>
<%= render 'shared/feed' %>
</div>
</div>
<% else %>
.
.
.
<% end %>
見た目を整えるためにCSSも弄る。
[Follow] / [Unfollow] ボタン用のパーシャルも作成しておく。
<% unless current_user?(@user) %>
<div id="follow_form">
<% if current_user.following?(@user) %>
<%= render 'unfollow' %>
<% else %>
<%= render 'follow' %>
<% end %>
</div>
<% end %>
follow
とunfollow
のパーシャルに作業を振っているだけ。パーシャルでは、Relationshipsリソース用の新しいルーティングが必要。
Rails.application.routes.draw do
root 'static_pages#home'
get 'help' => 'static_pages#help'
get 'about' => 'static_pages#about'
get 'contact' => 'static_pages#contact'
get 'signup' => 'users#new'
get 'login' => 'sessions#new'
post 'login' => 'sessions#create'
delete 'logout' => 'sessions#destroy'
resources :users do
member do
get :following, :followers
end
end
resources :account_activations, only: [:edit]
resources :password_resets, only: [:new, :create, :edit, :update]
resources :microposts, only: [:create, :destroy]
resources :relationships, only: [:create, :destroy]
end
フォローとフォロー解除用のパーシャルは以下。
<%= 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 %>
<%= form_for(current_user.active_relationships.find_by(followed_id: @user.id),
html: { method: :delete }) do |f| %>
<%= f.submit "Unfollow", class: "btn btn-default" %>
<% end %>
れらの2つのフォームの主な違いは、上は新しいリレーションシップを作成するのに対し、下は既存のリレーションシップを見つけ出すという点。
プロフィールにフォローとフォロー解除のボタンをそれぞれ表示するようにする。
<% provide(:title, @user.name) %>
<div class="row">
<aside class="col-md-4">
<section class="user_info">
<h1>
<%= gravatar_for @user %>
<%= @user.name %>
</h1>
</section>
<section class="stats">
<%= render 'shared/stats' %>
</section>
</aside>
<div class="col-md-8">
<%= render 'follow_form' if logged_in? %>
<% if @user.microposts.any? %>
<h3>Microposts (<%= @user.microposts.count %>)</h3>
<ol class="microposts">
<%= render @microposts %>
</ol>
<%= will_paginate @microposts %>
<% end %>
</div>
</div>
###14.2.3 [Following] と [Followers] ページ
フォロー全員を表示するページと、フォロワー全員を表示するページを作っていく。
ログインを要求するようにする。そのためのテストをまずは書く。
require 'test_helper'
class UsersControllerTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
@other_user = users(:archer)
end
.
.
.
test "should redirect following when not logged in" do
get following_user_path(@user)
assert_redirected_to login_url
end
test "should redirect followers when not logged in" do
get followers_user_path(@user)
assert_redirected_to login_url
end
end
following
アクションとfollowers
アクションを追加。
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
private
.
.
.
end
Railsは慣習に従って、アクションに対応するビューを暗黙的に呼び出す。
フォローしているユーザーとフォロワーの両方を表示するshow_followビューを作成。
<% 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>
無事テストもGREEENに。
show_follow
の描画結果を確認するため、統合テストを書いていく。
$ rails generate integration_test following
fixtureも書いていく。
one:
follower: michael
followed: lana
two:
follower: michael
followed: malory
three:
follower: lana
followed: michael
four:
follower: archer
followed: michael
さらに、正しいURLかどうかをテストするコードも加える。
require 'test_helper'
class FollowingTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
log_in_as(@user)
end
test "following page" do
get following_user_path(@user)
assert_not @user.following.empty?
assert_match @user.following.count.to_s, response.body
@user.following.each do |user|
assert_select "a[href=?]", user_path(user)
end
end
test "followers page" do
get followers_user_path(@user)
assert_not @user.followers.empty?
assert_match @user.followers.count.to_s, response.body
@user.followers.each do |user|
assert_select "a[href=?]", user_path(user)
end
end
end
テストも無事GREENに。
###14.2.4 [Follow] ボタン (基本編)
ボタンが動作するようにしていく。Relationshipsコントローラが必要なので生成する。
$ rails generate controller Relationships
ログイン済みでなければRelationshipのカウントが変わっていないことを確認する。テストを書く。
require 'test_helper'
class RelationshipsControllerTest < ActionDispatch::IntegrationTest
test "create should require logged-in user" do
assert_no_difference 'Relationship.count' do
post relationships_path
end
assert_redirected_to login_url
end
test "destroy should require logged-in user" do
assert_no_difference 'Relationship.count' do
delete relationship_path(relationships(:one))
end
assert_redirected_to login_url
end
end
Relationshipsコントローラのアクションに対してbeforeを追加する。
class RelationshipsController < ApplicationController
before_action :logged_in_user
def create
end
def destroy
end
end
ボタンを正常動作させるために、対応するユーザーを見つけてこなければならない。
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
これで、フォロー/フォロー解除の機能が完成。
###14.2.5 [Follow] ボタン (Ajax編)
Ajaxを使えば、Webページからサーバーに「非同期」で、ページを移動することなくリクエストを送信することができる。WebフォームにAjaxを採用するのは今や当たり前になりつつあるので、RailsでもAjaxを簡単に実装できるようになっているらしい。
form_for
これを
form_for ..., remote: true
こう置き換えるだけで、Railsは自動的にAjaxを使うようになる。該当箇所を修正。
Ajaxに対応させるために、respond_to
メソッドをcreate
アクションとdestroy
アクションにそれぞれ追加する。
class RelationshipsController < ApplicationController
before_action :logged_in_user
def create
@user = User.find(params[:followed_id])
current_user.follow(@user)
respond_to do |format|
format.html { redirect_to @user }
format.js
end
end
def destroy
@user = Relationship.find(params[:id]).followed
current_user.unfollow(@user)
respond_to do |format|
format.html { redirect_to @user }
format.js
end
end
end
ブラウザ側でJavaScriptが無効になっていた場合でもうまく動くようにする。
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
javascriptを動かせるようにするためには以下も必要。
JavaScriptと埋め込みRubyを使ってフォローの関係性を作成する。
$("#follow_form").html("<%= escape_javascript(render('users/unfollow')) %>");
$("#followers").html('<%= @user.followers.count %>');
Ruby JavaScript (RJS) を使ってフォローの関係性を削除する。
$("#follow_form").html("<%= escape_javascript(render('users/follow')) %>");
$("#followers").html('<%= @user.followers.count %>');
これで、プロフィールページを更新させずにフォローとフォロー解除ができるようになった。
###14.2.6 フォローをテストする
フォローのテストを書いていく。
require 'test_helper'
class FollowingTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
@other = users(:archer)
log_in_as(@user)
end
.
.
.
test "should follow a user the standard way" do
assert_difference '@user.following.count', 1 do
post relationships_path, params: { followed_id: @other.id }
end
end
test "should follow a user with Ajax" do
assert_difference '@user.following.count', 1 do
post relationships_path, xhr: true, params: { followed_id: @other.id }
end
end
test "should unfollow a user the standard way" do
@user.follow(@other)
relationship = @user.active_relationships.find_by(followed_id: @other.id)
assert_difference '@user.following.count', -1 do
delete relationship_path(relationship)
end
end
test "should unfollow a user with Ajax" do
@user.follow(@other)
relationship = @user.active_relationships.find_by(followed_id: @other.id)
assert_difference '@user.following.count', -1 do
delete relationship_path(relationship), xhr: true
end
end
end
これでテストも無事完了。
##14.3 ステータスフィード
ステータスフィードの実装に取りかかる。
###14.3.1 動機と計画
テストを書いていく中で重要なのは、フィードに必要な以下の3つの条件を満たすこと。
-
フォローしているユーザーのマイクロポストがフィードに含まれていること。
-
自分自身のマイクロポストもフィードに含まれていること。
-
フォローしていないユーザーのマイクロポストがフィードに含まれていないこと
require 'test_helper'
class UserTest < ActiveSupport::TestCase
.
.
.
test "feed should have the right posts" do
michael = users(:michael)
archer = users(:archer)
lana = users(:lana)
# フォローしているユーザーの投稿を確認
lana.microposts.each do |post_following|
assert michael.feed.include?(post_following)
end
# 自分自身の投稿を確認
michael.microposts.each do |post_self|
assert michael.feed.include?(post_self)
end
# フォローしていないユーザーの投稿を確認
archer.microposts.each do |post_unfollowed|
assert_not michael.feed.include?(post_unfollowed)
end
end
end
今のままだとまだテストはRED。
###14.3.2 フィードを初めて実装する
ユーザーのステータスフィードを返す。
とりあえず動くフィードの実装。
class User < ApplicationRecord
.
.
.
# パスワード再設定の期限が切れている場合はtrueを返す
def password_reset_expired?
reset_sent_at < 2.hours.ago
end
# ユーザーのステータスフィードを返す
def feed
Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)
end
# ユーザーをフォローする
def follow(other_user)
following << other_user
end
.
.
.
end
テストは成功に。
###14.3.3 サブセレクト
フィードの実装は、投稿されたマイクロポストの数が膨大になったときにうまくスケールしないらしい。
フォローしているユーザーが5,000人程度になるとWebサービス全体が遅くなる可能性があるから、フォローしているユーザー数に応じてスケールできるように、ステータスフィードを改善していく。
効率的にコードを置き換えるために、SQLのサブセレクトを使う。
class User < ApplicationRecord
.
.
.
# ユーザーのステータスフィードを返す
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
.
.
.
end
テストも無事完了!
##まとめ
できたアプリがこちら
無事Railsチュートリアルを完走することができました。エラーが出た時はかなりしんどかったですが、力技でなんとか押し切れました。
Webアプリケーションがどのように出来ているか、なんとなくですが掴めた気がします。
思ったことがいくつかあるのですが、うまくまとめらないので、箇条書きにして綴っていきます。
####所感
・Railsチュートリアルは多くの人が取り組んでいるので、エラーや詰まった時に参考になる記事が多い。
・プログラミングを学んでいくなら、成果物を作りながらの方が性に合う。手法を学んでいくだけだと作業感が強い。
・自分で調べながら全部できるようになるのまでは、時間がかかりそう。
・パソコンと向き合う時間が長いので、視力低下や疲れの対策を早めに取っておきたい。
・Rubyの書籍の目次などに目を通してみると、ほとんどチュートリアルでやったことだった。
・一周でも得るものは多かったが、二周目、三周目とやって理解を深めた方が良さそう。
####疑問
・人やタスク、チームの規模によりけりな気はするが、大体テストを作るだとか、何か一つタスクを終えるのにはどのくらいの時間がかかるのか
・チームでやっていく中で分担して作業していくとどのような感じになるのか
・フロント、バックエンドと分けて考えることが基本だが、どちらにも必須な知識はどのようなことなのか
####やりたいこと
・上の疑問を解消していきたい。
・人とコミュニケーションを取りながら作業することの経験を積みたい。
・将来的に、自分の興味のある組み込み系のことが少しでもできたらありがたい。
・エンジニアをジャンル分けした時に、Web系と組み込み系は分かれるらしいが、どちらにも活きる知識を身につけたい。
・自分でもWebアプリケーションが開発できるようになりたい。
拙い文章ですが、以上です。