Ruby on Rails チュートリアル

プロダクト開発の0→1を学ぼう

第2版 目次

第11章ユーザーをフォローする

この章では、他のユーザーをフォロー (およびフォロー解除) できるソーシャルレイヤーを追加し、各ユーザーのHomeページに、現在フォロー中のユーザーのステータスフィードを表示できるようにして、サンプルアプリケーションのコアを完成させます。また、自分をフォローしているユーザーと、自分がフォローしているユーザーを同時に表示できるようにします。ユーザー間のリレーションをモデリングする方法については11.1で説明します。続いてWebインターフェイスの作成方法について、Ajaxの紹介と合わせて11.2で説明します。最終的に、完全に動作するステータスフィードの開発は11.3で完了します。

この最終章では、本書の中で最も難易度の高い手法をいくつか使用しています。その中には、ステータスフィード作成のためにRuby/SQLを「だます」テクニックも含まれます。この章の例全体にわたって、これまでよりも複雑なデータモデルを使用しています。ここで学んだデータモデルは、今後自分用のWebアプリケーションを開発するときに必ず役に立ちます。本書を卒業して実際の開発に携わるときのために、11.4にサンプルアプリケーションのコア部分を拡張する際のお勧めの方法を記しました。そのための高度な資料への参照も含まれています。

Gitユーザーはこれまで同様新しいトピックブランチを作成してください。

$ git checkout -b following-users

この章で扱っている手法は本書全体の中で最も難易度が高いので、理解を助けるため、コードを書く前にはいったん立ち止まってインターフェイスを探検することにします。これまでの章と同様、最初にモックアップを示します1。ページ操作の全体的なフローは次のようになります。あるユーザー (John Calvin) は自分のプロファイルページを最初に表示し (図11.1)、フォローするユーザーを選択するためにUsersページ (図11.2) に移動します。Calvinは2番目のユーザーThomas Hobbes (図11.3) を表示し、[Follow] ボタンを押してフォローします。これにより、[Follow] ボタンが [Unfollow] に変わり、Hobbes の [followers] カウントが1人増えます (図11.4)。CalvinがHomeページに戻ると、[following] カウントが1人増え、Hobbesのマイクロポストがステータスフィードに表示されるようになっていることがわかります (図11.5)。この節では、以後このフローの実現に専念します。

page_flow_profile_mockup_bootstrap
図11.1現在のユーザーのプロファイル。(拡大)
page_flow_user_index_mockup_bootstrap
図11.2フォローする相手を見つける。(拡大)
page_flow_other_profile_follow_button_mockup_bootstrap
図11.3フォローするユーザーのプロファイルに [Follow] ボタンが表示されている。(拡大)
page_flow_other_profile_unfollow_button_mockup_bootstrap
図11.4プロファイルに [Unfollow] ボタンが表示され、フォロワーのカウントが1つ増えた。(拡大)
page_flow_home_page_feed_mockup_bootstrap
図11.5Homeページにステータスフィードが表示され、フォローのカウントが1増えた。(拡大)

11.1Relationshipモデル

ユーザーをフォローする機能を実装する第一歩は、データモデルを構成することです。ただし、これは見た目ほど単純ではありません。素朴に考えれば、has_many (1対多) リレーションシップはこんな感じでできそうな気がします。1人のユーザーが複数のユーザーをhas_manyとしてフォローし 、1人のユーザーに複数のフォロワーがいることをhas_manyで表す、という具合です。この後で説明しますが、この方法ではたちまち壁に突き当たってしまいます。これを解決するためのhas_many through (多対多の関係を表すのに使用) についてもこの後で説明します。この節で説明するアイディアの多くは、最初なかなか意図が読み取れないこともあると思います。複雑なデータモデルも、腑に落ちるまで時間がかかることでしょう。もし自分が混乱し始めていると感じたら、まずはこの章の最後まで進め、それからもう一度この章全体を読み返してみてください。読み返すことでよりよく理解できると思います。

11.1.1データモデルの問題 (および解決策)

ユーザーをフォローするデータモデル構成のための第一歩として、典型的な場合を検討してみましょう。あるユーザーが、別のユーザーをフォローしているところを考えてみましょう。具体例を挙げると、CalvinはHobbesをフォローしています。これを逆から見れば、HobbesはCalvinからフォローされています。CalvinはHobbesから見ればフォロワー (follower)であり、HobbesはCalvinによってフォローされている (followed) ことになります。Railsにおけるデフォルトの複数形の慣習に従えば、あるユーザーをフォローしているすべてのユーザーの集合はfollowersとなり、user.followersはそれらのユーザーの配列を表すことになります。残念なことに、この名前付けは逆についてはうまくいきません (Railsのというより英語の都合ですが)。フォローされているすべてのユーザーの集合は、このままではfollowedsとなってしまい、英語の文法からも外れるうえに非常に見苦しいものになってしまいます。followedに代えてfollowingとすれば、followingsのように複数形にも対応できるのではないでしょうか。しかし今度は意味が曖昧になってしまいます。この文脈で “following” と書くと、英語ではあなたをフォローする人々の集合、つまりあなたのフォロワーを指します。これでは意味が正反対になってしまいます。“following”は表示で “50 following, 75 followers” のように使用することはありますが、データモデルとしては違う名前を使用することにしましょう。ここでは、フォローしているユーザーたち自体を表すのに “followed users” を採用することにし、これにuser.followed_users配列が対応します2

これにより、図11.6のようにfollowed_usersテーブルと has_many関連付けを使用して、フォローしているユーザー (followed users) のモデリングができます。user.followed_usersはユーザーの配列でなければならないため、followed_usersテーブルのそれぞれの行は、followed_idで識別可能なユーザーである必要があります。この行にはfollower_idもあり、これで関連付けを行います3。さらに、それぞれの行はユーザーなので、これらのユーザーに名前やパスワードなどの属性も追加する必要があるでしょう。

naive_user_has_many_followed_users
図11.6フォローしているユーザーの素朴な実装例。

図11.6のデータモデルの問題点は、非常に無駄が多いことです。各行には、フォローしているユーザーのidのみならず、名前やメールアドレスまであります。これらはいずれも usersテーブルに既にあるものばかりです。さらによくないことに、followersの方をモデリングするときにも、同じぐらい無駄の多いfollowersテーブルを別に作成しなければならなくなってしまいます。結論としては、このデータモデルはメンテナンスの観点から見て悪夢です。ユーザー名を変更するたびに、usersテーブルのそのレコードだけでなく、followed_usersテーブルとfollowersテーブルの両方について、そのユーザーを含むすべての行を更新しなければならなくなります。

この問題の根本は、必要な抽象化を行なっていないことです。正しい抽象化の方法を見つけ出す方法の1つは、Webアプリケーションにおけるfollowingの動作をどのように実装するかをじっくり考えることです。7.1.2において、RESTアーキテクチャは、作成されたり削除されたりするリソースに関連していたことを思い出してください。ここから、2つの疑問が生じます。1. あるユーザーが別のユーザーをフォローするとき、何が作成されるのでしょうか。2. あるユーザーが別のユーザーをフォロー解除するとき、何が削除されるのでしょうか。

この点を踏まえて考えると、この場合アプリケーションによって作成または削除されるのは、つまるところ2人のユーザーの「関係 (リレーションシップ)」であることがわかります。つまり、1人のユーザーは「has_many :relationships」、つまり1対多の関係を持つことができ、さらにユーザーはリレーションシップを経由して多くのfollowed_users (またはfollowers) と関係を持つことができるということです。実際、図11.6には必要なものがほぼあります。それぞれの「フォローしているユーザー」はfollowed_idで一意に特定できるので、followed_usersテーブルをRelationshipsテーブルに変換します。続いて、ユーザーの詳細情報 (名前とメールアドレス) は Relationshipテーブルから削除してしまいます。そしてfollowed_idを使用して「フォローしているユーザー」をusersテーブルから取り出します。今度は逆の関係を考えます。follower_idカラムを使用して、ユーザーのフォロワーの配列を取り出すことができます。

ユーザーのfollowed_usersの配列を作成するには、followed_idの配列を取り出し、それぞれのidごとに対応するユーザーを見つけ出します。皆様の期待どおり、Railsにはこのような手続きを簡単に行うための方法があります。多対多の関係を表現するこの手法はhas_many throughとして知られています。11.1.4でも説明しますが、Railsは、以下のような簡潔なコードを使用することで、1人のユーザーが多数のユーザーとRelationshipテーブル経由でフォローしている/されている関係を記述することができます。

has_many :followed_users, through: :relationships, source: :followed

このコードは自動的に、user.followed_usersを「フォローしているユーザー」の配列を使用してデプロイします。このデータモデルの模式図を図11.7に示します。

user_has_many_followed_users
図11.7ユーザーのリレーションシップで表される、フォローしている/されているユーザーのモデル。

このデータモデルを実装するには、最初に以下のようにRelationshipモデルを生成します。

$ rails generate model Relationship follower_id:integer followed_id:integer

上のコマンドを実行するとRelationshipファクトリーも生成されるので、以下を実行してファクトリーを削除してください。

$ rm -f spec/factories/relationship.rb

このリレーションシップは今後follower_idfollowed_idで頻繁に検索することになるので、リスト11.1に示したように、それぞれのカラムにインデックスを追加します。

リスト11.1 relationshipsテーブルにインデックスを追加する。
db/migrate/[timestamp]_create_relationships.rb
class CreateRelationships < ActiveRecord::Migration
  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

リスト11.1には複合 (composite) インデックスが使用されていることに注目してください。これはfollower_idfollowed_idを組み合わせたときの一意性 (uniquenes: 重複がないこと) を強制するもので、これにより、あるユーザーは別のユーザーを2度以上フォローすることはできなくなります。

add_index :relationships, [:follower_id, :followed_id], unique: true

(リスト6.19のメールアドレスの一意インデックスと比較してみてください)。11.1.4でも説明しますが、このような重複はインターフェイスレベルでも起きることのないようにします。しかし、一意 (ユニーク) インデックスを追加することで、ユーザーが何らかの方法で (curlなどのコマンドラインツールを使用して) リレーションシップを重複させてしまうようなことがあればエラーが発生するようになります。さらに、このRelationshipモデルには今後一意性検証を追加する予定です。しかし、一意インデックスを使用していればリレーションシップが重複したときに必ずエラーになるので、現時点では一意インデックスで十分です。

relationshipsテーブルを作成するために、いつものようにデータベースのマイグレーションを行なってテストデータベースを準備しましょう。

$ bundle exec rake db:migrate
$ bundle exec rake db:test:prepare

作成したRelationshipデータモデルを図11.8に示します。

relationship_model
図11.8Relationshipデータモデル。

11.1.2User/relationshipの関連付け

フォローしているユーザーとフォロワーを実装する前に、ユーザーとリレーションシップの関連付けを行います。1人のユーザーにはhas_many (1対多) リレーションシップがあり、このリレーションシップは2人のユーザーの間の関係なので、フォローしているユーザーとフォロワーの両方に属します (belongs_to)。

10.1.3のマイクロポストのときと同様、以下のようなユーザー関連付けのコードを使用して新しいリレーションシップを作成します。

user.relationships.build(followed_id: ...)

基本的な検証テストから始めることにします。 このテストをリスト11.2に示します。

リスト11.2 リレーションシップの作成と属性をテストする。
spec/models/relationship_spec.rb
require 'spec_helper'

describe Relationship do

  let(:follower) { FactoryGirl.create(:user) }
  let(:followed) { FactoryGirl.create(:user) }
  let(:relationship) { follower.relationships.build(followed_id: followed.id) }

  subject { relationship }

  it { should be_valid }
end

ところで、リスト11.2ではletを使用してインスタンス変数にアクセスしていることに注目してください。UserモデルやMicropostモデルのテストのときは、それぞれ@user@micropostを使用していました。この違いが問題になることはほとんどありません4が、動作をよりはっきりとさせるために、インスタンス変数を使用する際にはletを使用することをお勧めします。これまで通常のインスタンス変数を使用してきたのは、インスタンス変数を早い段階で紹介しておきたかったのと、letがやや高度であるためです。

今度はUserモデルのrelationships属性をリスト11.3のようにテストしましょう。

リスト11.3 user.relationships属性のテスト。
spec/models/user_spec.rb
require 'spec_helper'

describe User do
  .
  .
  .
  it { should respond_to(:feed) }
  it { should respond_to(:relationships) }
  .
  .
  .
end

この時点で、アプリケーションコードは10.1.3のようになるのではないかと予測した方もいるかもしれません。実際似ているのですが、1つ大きな違いがあります。Micropostモデルの場合、以下のように書くことができました。

class Micropost < ActiveRecord::Base
  belongs_to :user
  .
  .
  .
end

および以下です。

class User < ActiveRecord::Base
  has_many :microposts
  .
  .
  .
end

このように書くことができたのは、micropostsテーブルにはユーザーを特定するためのuser_id属性があるからです (10.1.1)。2つのデータベーステーブルを結合するために使用されるidは、外部キーと呼ばれます。Userモデルオブジェクトの外部キーがuser_idの場合、Railsは自動的に関連付けを推測します。つまり、Railsはデフォルトで、<クラス>_idという形式の外部キーがあることを期待します。ここで<クラス>は小文字のクラス名です5。ここではユーザー (users) を扱っていますが、このユーザーは (user_idではなく) 外部キーであるfollower_idによって特定されるので、リスト11.4のようにそのことを明示的にRailsに伝える必要があります6

リスト11.4 ユーザー/リレーションシップのhas_manyの関連付けを実装する。
app/models/user.rb
class User < ActiveRecord::Base
  has_many :microposts, dependent: :destroy
  has_many :relationships, foreign_key: "follower_id", dependent: :destroy
  .
  .
  .
end

(ユーザーを削除したら、ユーザーのリレーションシップも同時に削除される必要があります。そのため、関連付けにdependent: :destroyを追加してあります。この部分のテストは演習に回します (11.5)。)

Micropostモデルの場合と同様、Relationshipモデルはユーザーとbelongs_to (属する) の関係を持ちます。この場合、リレーションシップのオブジェクトはfollowerfollowed userの両方に「属します」。リスト11.5でこれをテストします。

リスト11.5 ユーザー/リレーションシップのbelongs_to関連付けをテストする。
spec/models/relationship_spec.rb
describe Relationship do
  .
  .
  .
  describe "follower methods" do
    it { should respond_to(:follower) }
    it { should respond_to(:followed) }
    its(:follower) { should eq follower }
    its(:followed) { should eq followed }
  end
end

これに対応するアプリケーションコードを作成するには、belongs_toリレーションシップを普段と同様に作成します。Railsは外部キーを、それに対応するシンボルから推測します。たとえば、:followerからfollower_idを推測し、:followedからfollowed_idを推測するという具合です。しかし、FollowedモデルもFollowerモデルも実際にはないので、クラス名Userを提供する必要があります。結果をリスト11.6に示します。デフォルトで作成されるRelationshipモデルとは異なり、followed_idのみアクセス可能となっている点に注意してください。

リスト11.6 belongs_to関連付けを Relationshipモデルに追加する。
app/models/relationship.rb
class Relationship < ActiveRecord::Base
  belongs_to :follower, class_name: "User"
  belongs_to :followed, class_name: "User"
end

実際には、followedの関連付けは11.1.5まで使用しません。しかしfollower/followed構造を両方作り、これらを同時に実装する方が理解しやすくなります。

この時点で、リスト11.2のテストとリスト11.3のテストはパスするはずです。

$ bundle exec rspec spec/

11.1.3検証

先に進む前に、Relationshipモデルの検証を追加して完全なものにしておきましょう。テスト (リスト11.7)とアプリケーションコード (リスト11.8) は素直な作りです。

リスト11.7 Relationshipモデル検証のテスト。
spec/models/relationship_spec.rb
describe Relationship do
  .
  .
  .
  describe "when followed id is not present" do
    before { relationship.followed_id = nil }
    it { should_not be_valid }
  end

  describe "when follower id is not present" do
    before { relationship.follower_id = nil }
    it { should_not be_valid }
  end
end
リスト11.8 Relationshipモデルの検証を追加する。
app/models/relationship.rb
class Relationship < ActiveRecord::Base
  belongs_to :follower, class_name: "User"
  belongs_to :followed, class_name: "User"
  validates :follower_id, presence: true
  validates :followed_id, presence: true
end

11.1.4フォローしているユーザー

いよいよRelationship関連付けの核心、followed_usersfollowersに取りかかります。まずはfollowed_usersから始めます (リスト11.9)。

リスト11.9 user.followed_users属性のテスト。
spec/models/user_spec.rb
require 'spec_helper'

describe User do
  .
  .
  .
  it { should respond_to(:relationships) }
  it { should respond_to(:followed_users) }
  .
  .
  .
end

この実装では、最初からhas_many throughを使用します。図11.7のように、1人のユーザーにはいくつもの「フォローする/される (多対多)」のリレーションシップがあります。デフォルトのhas_many through関連付けでは、Railsは単一バージョンの関連付けに対応する外部キーを探します。つまり以下のコードは

has_many :followeds, through: :relationships

relationshipsテーブルのfollowed_idを使用して配列を作成します。ただし、11.1.1でも指摘したとおり、user.followedsとすると英語の感覚として非常に見苦しくなってしまいますので、"followed"を複数形にするには “followed users” とuserを追加してそれを複数形にする方がずっと自然です。従って、フォローしているユーザーの配列を表すためにuser.followed_usersと書くことにします。Railsではデフォルトをオーバーライドすることができるので、ここでは:sourceパラメーター (リスト11.10) を使用し、followed_users配列の元はfollowed idの集合であることを明示的にRailsに伝えます。

リスト11.10 Userモデルのfollowed_users関連付けを追加する。
app/models/user.rb
class User < ActiveRecord::Base
  has_many :microposts, dependent: :destroy
  has_many :relationships, foreign_key: "follower_id", dependent: :destroy
  has_many :followed_users, through: :relationships, source: :followed
  .
  .
  .
end

ユーザーをフォローするというリレーションシップを作成するために、follow!ユーティリティメソッドを導入し、user.follow!(other_user)と書けるようにしましょう (このfollow!メソッドは常に正常に動作しなければなりません。従って、create!メソッドやsave!メソッドと同様、末尾に感嘆符を置いて、作成に失敗した場合には例外を発生することを示します) 。さらに、これに関連するfollowing?論理値メソッドも追加し、あるユーザーが誰かをフォローしているかどうかを確認できるようにします7。これらのメソッドの実際の使用例をリスト11.11に示します。

リスト11.11 “フォロー用の” ユーティリティメソッドをいくつかテストする。
spec/models/user_spec.rb
require 'spec_helper'

describe User do
  .
  .
  .
  it { should respond_to(:followed_users) }
  it { should respond_to(:following?) }
  it { should respond_to(:follow!) }
  .
  .
  .
  describe "following" do
    let(:other_user) { FactoryGirl.create(:user) }
    before do
      @user.save
      @user.follow!(other_user)
    end

    it { should be_following(other_user) }
    its(:followed_users) { should include(other_user) }
  end
end

このアプリケーションコードでは、following?メソッドはother_userという1人のユーザーを引数にとり、フォローする相手のユーザーがデータベース上に存在するかどうかをチェックします。follow!メソッドは、relationships関連付けを経由してcreate!を呼び出すことで、「フォローする」のリレーションシップを作成します。このコードをリスト11.12に示します。

リスト11.12 following?ユーティリティメソッドfollow! ユーティリティメソッド
app/models/user.rb
class User < ActiveRecord::Base
  .
  .
  .
  def feed
    .
    .
    .
  end

  def following?(other_user)
    relationships.find_by(followed_id: other_user.id)
  end

  def follow!(other_user)
    relationships.create!(followed_id: other_user.id)
  end
  .
  .
  .
end

リスト11.12では、ユーザー自身を明示的には書かず、単に以下のように書いています。

relationships.create!(...)

以下は、上と同等のコードです。

self.relationships.create!(...)

selfを明示的に書くかどうかは好みの問題です。

ユーザーは他のユーザーをフォローできるだけでなく、フォロー解除もできる必要があります。当然、リスト11.13に示したようなunfollow!メソッドが必要になります8

リスト11.13 ユーザーのフォロー解除をテストする。
spec/models/user_spec.rb
require 'spec_helper'

describe User do
  .
  .
  .
  it { should respond_to(:follow!) }
  it { should respond_to(:unfollow!) }
  .
  .
  .
  describe "following" do
    .
    .
    .
    describe "and unfollowing" do
      before { @user.unfollow!(other_user) }

      it { should_not be_following(other_user) }
      its(:followed_users) { should_not include(other_user) }
    end
  end
end

unfollow!のコードは実に簡単です。フォローしているユーザーのidでリレーションシップを検索し、それを削除すればよいのです (リスト11.14)。

リスト11.14 ユーザーのリレーションシップを削除してフォロー解除する。
app/models/user.rb
class User < ActiveRecord::Base
  .
  .
  .
  def following?(other_user)
    relationships.find_by(followed_id: other_user.id)
  end

  def follow!(other_user)
    relationships.create!(followed_id: other_user.id)
  end

  def unfollow!(other_user)
    relationships.find_by(followed_id: other_user.id).destroy
  end
  .
  .
  .
end

11.1.5フォロワー

リレーションシップというパズルの最後の一片は、user.followersメソッドを追加することです。これは上のuser.followed_usersメソッドと対になります。図11.7を見ていて気付いた方もいると思いますが、フォロワーの配列をデプロイするために必要な情報は、relationshipsテーブルに既にあります。実は、follower_idfollowed_idを入れ替えるだけで、フォロワーについてもユーザーのフォローのときとまったく同じ方法が使用できます。ということは、何らかの方法で2つのカラムを入れ替えたreverse_relationshipsテーブルを用意することができれば (図11.9)、user.followersを最小限の手間で実装できることになります。

user_has_many_followers_2nd_ed
図11.9Relationshipモデルのカラムを入れ替えて作った、フォロワーのモデル。

Railsが魔法で何とかしてくれることを期待して、このテストを作成しましょう (リスト11.15)。

リスト11.15 逆リレーションシップをテストする。
spec/models/user_spec.rb
require 'spec_helper'

describe User do
  .
  .
  .
  it { should respond_to(:relationships) }
  it { should respond_to(:followed_users) }
  it { should respond_to(:reverse_relationships) }
  it { should respond_to(:followers) }
  .
  .
  .

  describe "following" do
    .
    .
    .
    it { should be_following(other_user) }
    its(:followed_users) { should include(other_user) }

    describe "followed user" do
      subject { other_user }
      its(:followers) { should include(@user) }
    end
    .
    .
    .
  end
end

subjectメソッドを使用して@userからother_userに対象を切り替えていることで、フォロワーのリレーションシップのテストを自然に実行できていることに注目してください。

subject { other_user }
its(:followers) { should include(@user) }

もちろん、逆リレーションシップのためにわざわざデータベーステーブルを1つ余分に作成するようなことはしません。代わりに、フォロワーとフォローしているユーザーの関係が対称的であることを利用し、単にfollowed_idを主キーとして渡すことでreverse_relationshipsをシミュレートすればよいのです。つまり、このrelationships関連付けでは以下のようにfollower_idを外部キーとして使用し、

has_many :relationships, foreign_key: "follower_id"

reverse_relationshipsでは以下のようにfollowed_idを外部キーとして使用します。

has_many :reverse_relationships, foreign_key: "followed_id"

これにより、リスト11.16に示したように逆リレーションシップを経由してfollowersの関連付けを得ることができます。

リスト11.16 逆リレーションシップを使用してuser.followersを実装する。
app/models/user.rb
class User < ActiveRecord::Base
  .
  .
  .
  has_many :reverse_relationships, foreign_key: "followed_id",
                                   class_name:  "Relationship",
                                   dependent:   :destroy
  has_many :followers, through: :reverse_relationships, source: :follower
  .
  .
  .
end

(リスト11.4のときと同様、dependent :destroyのテストは演習に回します (11.5)。) 実際には、この関連付けでは以下のようにクラス名を明示的に含める必要があることに注意してください。

has_many :reverse_relationships, foreign_key: "followed_id",
                                 class_name: "Relationship"

これを指定しないと、Railsは実在しないReverseRelationshipクラスを探しに行ってしまいます。

ここでは、以下のように:sourceキーを省略してもよいことにも注意してください。

has_many :followers, through: :reverse_relationships

:followers属性の場合、Railsが “followers” を単数形にして自動的に外部キーfollower_idを探してくれるからです (:followedではこうはいきません)。上のコードでは、関連付けがhas_many :followed_usersと同じ形式になることを強調するために、あえて:sourceキーを付けてありますが、もちろん省略しても構いません。

リスト11.16のコードによって、フォローしているユーザーとフォロワーの関連付けの実装は完了しました。テストもすべてパスするはずです。

$ bundle exec rspec spec/

この節は、データモデリングのスキルを向上させるという強い要請に基いて書かれました。時間をかけて身に付けていただければ幸いです。この節で使用されたさまざまな関連付けを理解するのに一番良いのは、次の節で行なっているように実際のWebインターフェイスで使用することです。

11.2フォローしているユーザー用のWebインターフェイス

この章の最初に、フォローしているユーザーのページ表示の流れについて説明しました。この節では、モックアップで示したようにフォロー/フォロー解除の基本的なインターフェイスを実装します。また、フォローしているユーザーと、フォロワーにそれぞれ表示用のページを作成します。11.3では、ユーザーのステータスフィードを追加して、サンプルアプリケーションを完成させます。

11.2.1フォローしているユーザーのサンプルデータ

1つ前の章のときと同じように、サンプルデータを自動作成するRakeタスクを使用してデータベースにサンプルリレーションシップを登録するのがやはり便利です。先にサンプルデータを自動作成しておけば、Webページの見た目のデザインから先にとりかかることができ、バックエンドの機能の実装をこの節の後に回すことができます。

前回リスト10.20,で自動生成したサンプルデータは少々いい加減でしたので、今回はまずユーザーとマイクロポストを作成するためのメソッドをそれぞれ別々に定義し、それから新しくmake_relationshipsメソッドを作成してサンプルリレーションシップを追加することにしましょう。作成したRakeファイルをリスト11.17に示します。

リスト11.17 フォローしている、またはフォローされている関係を表すリレーションシップをサンプルデータに追加する。
lib/tasks/sample_data.rake
namespace :db do
  desc "Fill database with sample data"
  task populate: :environment do
    make_users
    make_microposts
    make_relationships
  end
end

def make_users
  admin = User.create!(name:     "Example User",
                       email:    "example@railstutorial.jp",
                       password: "foobar",
                       password_confirmation: "foobar",
                       admin: true)
  99.times do |n|
    name  = Faker::Name.name
    email = "example-#{n+1}@railstutorial.jp"
    password  = "password"
    User.create!(name:     name,
                 email:    email,
                 password: password,
                 password_confirmation: password)
  end
end

def make_microposts
  users = User.all(limit: 6)
  50.times do
    content = Faker::Lorem.sentence(5)
    users.each { |user| user.microposts.create!(content: content) }
  end
end

def make_relationships
  users = User.all
  user  = users.first
  followed_users = users[2..50]
  followers      = users[3..40]
  followed_users.each { |followed| user.follow!(followed) }
  followers.each      { |follower| follower.follow!(user) }
end

上のコードのうち、サンプルリレーションシップを作成する部分は以下です。

def make_relationships
  users = User.all
  user  = users.first
  followed_users = users[2..50]
  followers      = users[3..40]
  followed_users.each { |followed| user.follow!(followed) }
  followers.each      { |follower| follower.follow!(user) }
end

ここでは、最初のユーザーにユーザー3からユーザー51までをフォローさせ、それから逆にユーザー4からユーザー41に最初のユーザーをフォローさせます。ソースを見るとわかるように、このような設定を自由に行うことができます。こうしてリレーションシップを作成しておけば、アプリケーションのインターフェイスを開発するには十分です。

リスト11.17を実行するために、いつものように以下を実行してデータベース上にデータを生成します。

$ bundle exec rake db:reset
$ bundle exec rake db:populate
$ bundle exec rake db:test:prepare

11.2.2統計とフォロー用フォーム

これでサンプルユーザーに、フォローしているユーザーの配列とフォロワーの配列ができました。ユーザープロファイルページとHomeページを更新してこれを反映しましょう。最初に、プロファイルページとHomeページに、フォローしているユーザーとフォロワーの統計情報を表示するためのパーシャルを作成します。次に、フォロー用とフォロー解除用のフォームを作成します。それから、フォローしているユーザーとフォロワーの一覧を表示する専用のページを作成します。

11.1.1でも述べたとおり、英語の “following” は、属性として使用すると意味が曖昧になります。user.followingは、フォローしているユーザーとも、フォロワーとも受け取れてしまいます。しかし、フォローしているユーザーをページに表示するときの “50 following” というラベルになら問題なく使用できます。実際、これはTwitterでも使用されている表示です。図11.1のモックアップおよび図11.10の拡大図を参照してください。

stats_partial_mockup
図11.10統計情報パーシャルのモックアップ。

図11.10の統計情報には、現在のユーザーがフォローしている人数と、現在のフォロワーの人数が表示されています。それぞれの表示はリンクになっており、専用の表示ページに移動できます。第5章では、これらのリンクはダミーテキスト’#’を使用して無効にしていました。しかしルーティングについての知識もだいぶ増えてきたので、今回は実装することにしましょう。実際のページ作成は11.2.3まで行いませんが、ルーティングは今実装します (リスト11.18)。このコードでは、resourcesブロックの内側で:memberメソッドを使用しています。これは初登場ですので、どんな動作をするのか推測してみてください。 (: リスト11.18のコードは resources :users置き換えます)

リスト11.18 followingおよびfollowersアクションをUsersコントローラに追加する。
config/routes.rb
SampleApp::Application.routes.draw do
  resources :users do
    member do
      get :following, :followers
    end
  end
  .
  .
  .
end

この場合のURLは/users/1/followingや/users/1/followersのようになるのではないかと推測した方もいると思います。そして、リスト11.18のコードはまさにそれを行なっているのです。どちらのページもデータを表示するものなので、(RESTの慣習に基いて) GETリクエストに応答するためにgetを使用してURLを生成します。memberメソッドは、ユーザーidを含むURLにそのルート (route) が応答できるようにするものです。idを指定せずにすべてのメンバーを表示するには、以下のようにcollectionを使用できます。

resources :users do
  collection do
    get :tigers
  end
end

このコードは/users/tigersというURLに応答します (アプリケーションにあるすべてのtigerのリストを表示します)。 Railsにはさまざまなルーティングオプションがありますが、詳細についてはRailsガイドの記事「Rails ルーティング」を参照してください。リスト11.18によって生成されるルーティングテーブルを表11.1に示します。ここにある、フォローしているユーザー用とフォロワー用の名前付きルートをこの後使用します。曖昧さのない「フォローしているユーザー (followed users)」と、Twitter式の「フォローしている (following)」表示を両方採用しましたが、このルーティングでは仕組み上残念ながら曖昧な方の "following" を使用せざるを得ません。曖昧でない "followed users" のままだと、ルートはfollowed_users_user_pathという英語として見苦しいものになるため、表11.1following_user_pathが生成されるように調整しました。

HTTPリクエストURLアクション名前付きルート
GET/users/1/followingfollowingfollowing_user_path(1)
GET/users/1/followersfollowersfollowers_user_path(1)
表11.1リスト11.18のリソースのカスタムルールで提供されるRESTfulなルート

ルーティングが定義されたことで、統計情報パーシャルをテストできる状態になりました (最初にテストを書いてもよかったのですが、ルーティングを更新しておかないと名前付きルートの説明がしにくかったので、テストを後にしました) 。この統計情報パーシャルはプロファイルページとHomeページの両方に表示されます。リスト11.19では、後者のHomeページの方でテストしています。

リスト11.19 Homeページ上の、フォローしているユーザー/フォロワーの統計情報をテストする。
spec/requests/static_pages_spec.rb
require 'spec_helper'

describe "Static pages" do
  .
  .
  .
  describe "Home page" do
    .
    .
    .
    describe "for signed-in users" do
      let(:user) { FactoryGirl.create(:user) }
      before do
        FactoryGirl.create(:micropost, user: user, content: "Lorem")
        FactoryGirl.create(:micropost, user: user, content: "Ipsum")
        sign_in user
        visit root_path
      end

      it "should render the user's feed" do
        user.feed.each do |item|
          expect(page).to have_selector("li##{item.id}", text: item.content)
        end
      end

      describe "follower/following counts" do
        let(:other_user) { FactoryGirl.create(:user) }
        before do
          other_user.follow!(user)
          visit root_path
        end

        it { should have_link("0 following", href: following_user_path(user)) }
        it { should have_link("1 followers", href: followers_user_path(user)) }
      end
    end
  end
  .
  .
  .
end

このテストの中心となるのは、フォローしているユーザーとフォロワーのカウントがページに表示され、それぞれに正しいURLが設定されていることを確認することです。

it { should have_link("0 following", href: following_user_path(user)) }
it { should have_link("1 followers", href: followers_user_path(user)) }

ここでは、表11.1の名前付きルートを使用して、正しいアドレスへのリンクを確認していることに注目してください。また、ここでは “followers” という語はラベルとして使用しているので、フォロワーが1人の場合にも複数形のままとします。

統計情報パーシャルのアプリケーションコードは、リスト11.20に示したとおり、単なる divタグ内のリンクです。

リスト11.20 フォローしているユーザーとフォロワーの統計情報を表示するパーシャル。
app/views/shared/_stats.html.erb
<% @user ||= current_user %>
<div class="stats">
  <a href="<%= following_user_path(@user) %>">
    <strong id="following" class="stat">
      <%= @user.followed_users.count %>
    </strong>
    following
  </a>
  <a href="<%= followers_user_path(@user) %>">
    <strong id="followers" class="stat">
      <%= @user.followers.count %>
    </strong>
    followers
  </a>
</div>

このパーシャルはユーザー表示ページとHomeページの両方に表示されるので、リスト11.20の最初の行では、以下のコードを使用して適切な方を選択しています。

<% @user ||= current_user %>

コラム 8.2でも説明したとおり、@usernilでない場合 (つまりプロファイルページの場合) は何もせず、nilの場合 (つまりHomeページの場合) には@userにカレントユーザーを設定します。

フォローしているユーザーの人数と、フォロワーの人数は、以下の関連付けを使用して計算されます。

@user.followed_users.count

上と、以下を使用します。

@user.followers.count

リスト10.17のマイクロポストのコードと比較してみましょう。あのときは以下のように書きました。

@user.microposts.count

このコードを使用してマイクロポストをカウントします。

一部の要素で、以下のようにCSS idを指定していることにもぜひ注目してください。

<strong id="following" class="stat">
...
</strong>

こうしておくと、11.2.5でAjaxを実装するときに便利です。そこでは、一意のidを指定してページ要素にアクセスしています。

統計情報パーシャルができあがりました。Homeページにこの統計情報を表示するのは、リスト11.21のように簡単にできます (これにより、リスト11.19のテストにもパスするようになります)。

リスト11.21 フォロワーの統計情報をHomeページに追加する。
app/views/static_pages/home.html.erb
<% if signed_in? %>
      .
      .
      .
      <section>
        <%= render 'shared/user_info' %>
      </section>
      <section>
        <%= render 'shared/stats' %>
      </section>
      <section>
        <%= render 'shared/micropost_form' %>
      </section>
      .
      .
      .
<% else %>
  .
  .
  .
<% end %>

統計情報にスタイルを与えるために、リスト11.22のようにSCSSを追加しましょう (なお、このSCSSにはこの章で使用するスタイルがすべて含まれています)。スタイル追加の結果を図11.11に示します。

リスト11.22 Homeページのサイドバー用SCSS。
app/assets/stylesheets/custom.css.scss
.
.
.

/* sidebar */
.
.
.
.stats {
  overflow: auto;
  a {
    float: left;
    padding: 0 10px;
    border-left: 1px solid $grayLighter;
    color: gray;
    &:first-child {
      padding-left: 0;
      border: 0;
    }
    &:hover {
      text-decoration: none;
      color: $blue;
    }
  }
  strong {
    display: block;
  }
}

.user_avatars {
  overflow: auto;
  margin-top: 10px;
  .gravatar {
    margin: 1px 1px;
  }
}
.
.
.
home_page_follow_stats_bootstrap
図11.11Homeページ (/) にフォロー関連の統計情報を表示する。(拡大)

この後すぐ、プロファイルにも統計情報パーシャルを表示しますが、今のうちにリスト11.23のようにフォロー/フォロー解除ボタン用のパーシャルも作成しましょう。

リスト11.23 フォロー/フォロー解除ボタン用のパーシャル。
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 %>

このコードは、followunfollowのパーシャルに作業を振っているだけです。パーシャルでは、Relationshipsリソース用のルールを含む新しいルートが必要です。これを、リスト10.22のMicropostsリソースの例に従って作成しましょう (リスト11.24)。

リスト11.24 ユーザーのリレーションシップ用のルートを追加する。
config/routes.rb
SampleApp::Application.routes.draw do
  .
  .
  .
  resources :sessions,      only: [:new, :create, :destroy]
  resources :microposts,    only: [:create, :destroy]
  resources :relationships, only: [:create, :destroy]
  .
  .
  .
end

follow/unfollowパーシャル自体は、リスト11.25リスト11.26に示します。

リスト11.25 ユーザーをフォローするフォーム。
app/views/users/_follow.html.erb
<%= form_for(current_user.relationships.build(followed_id: @user.id)) do |f| %>
  <div><%= f.hidden_field :followed_id %></div>
  <%= f.submit "Follow", class: "btn btn-large btn-primary" %>
<% end %>
リスト11.26 ユーザーをフォロー解除するフォーム。
app/views/users/_unfollow.html.erb
<%= form_for(current_user.relationships.find_by(followed_id: @user.id),
             html: { method: :delete }) do |f| %>
  <%= f.submit "Unfollow", class: "btn btn-large" %>
<% end %>

これらの2つのフォームでは、いずれもform_forを使用してRelationshipモデルオブジェクトを操作しています。これらの2つのフォームの主な違いは、リスト11.25新しいリレーションシップを作成するのに対し、リスト11.26は既存のリレーションシップを見つけ出すという点です。すなわち、前者はPOSTリクエストを Relationshipsコントローラに送信してリレーションシップをcreate (作成) し、後者はDELETEリクエストを送信してリレーションシップをdestroy (削除) するということです (これらのアクションは11.2.4で実装します)。最終的に、このfollow/unfollowフォームにはボタンしかないことを理解していただけたと思います。しかし、それでもこのフォームはfollowed_idをコントローラに送信する必要があります。これを行うために、リスト11.25hidden_fieldメソッドを使用します。このメソッドは 以下のフォームのHTMLを生成します。

<input id="followed_relationship_followed_id"
name="followed_relationship[followed_id]"
type="hidden" value="3" />

この「隠れた」inputタグは、関連する情報をページに置きながら、それらをブラウザ上で非表示にします。

これで、11.27のようにフォロー用のフォームをユーザープロファイルページにインクルードしてパーシャルを出力できるようになりました。 プロファイルには、図11.12および図11.13のようにそれぞれ [Follow]、[Unfollow] ボタンが表示されます。

リスト11.27 ユーザープロファイルページにフォロー用のフォームとフォロワーの統計情報を追加する。
app/views/users/show.html.erb
<% provide(:title, @user.name) %>
<div class="row">
  <aside class="span4">
    <section>
      <h1>
        <%= gravatar_for @user %>
        <%= @user.name %>
      </h1>
    </section>
    <section>
      <%= render 'shared/stats' %>
    </section>
  </aside>
  <div class="span8">
    <%= render 'follow_form' if signed_in? %>
    .
    .
    .
  </div>
</div>
profile_follow_button_bootstrap
図11.12あるユーザープロファイル (/users/2) に [Follow] ボタンが表示されている。 (拡大)
profile_unfollow_button_bootstrap
図11.13あるユーザープロファイル (/users/6) に [Unfollow] ボタンが表示されている。(拡大)

これらのボタンはもうすぐ動作するようになります。実はこのボタンの実装には2とおりの方法があります。1つは標準的な方法 (11.2.4)、もう1つはAjaxを使用する方法 (11.2.5) です。でもその前に、フォローしているユーザーとフォロワーを表示するページをそれぞれ作成してHTMLインターフェイスを完成させてしまいましょう。

11.2.3「フォローしているユーザー」ページと「フォロワー」ページ

フォローしているユーザーを表示するページと、フォロワーを表示するページは、いずれもユーザープロファイルページとユーザーインデックスページ (9.3.1) を合わせたような作りになるという点で似ています。どちらにもフォローの統計情報などのユーザー情報を表示するサイドバーと、ユーザーのリストがあります。さらに、サイドバーにはユーザープロファイル画像のリンクを格子状に並べて表示する予定です。この要求に合うモックアップを図11.14 (フォローしているユーザー用) および 図 11.15 (フォロワー用) に示します。

following_mockup_bootstrap
図11.14フォローしているユーザー用ページのモックアップ。(拡大)
followers_mockup_bootstrap
図11.15ユーザーのフォロワー用ページのモックアップ。(拡大)

ここでの最初の作業は、フォローしているユーザーのリンクとフォロワーのリンクを動くようにすることです。Twitterにならい、どちらのページでもユーザーのサインインを要求します。 このテストは11.28のように行います。ユーザーがサインインしたら、どちらのページにもフォローしているユーザー用リンクとフォロワー用リンクを表示します。このテストはリスト11.29のように行います。

リスト11.28 フォローしているユーザー用ページとフォロワー用ページでの認可をテストする。
spec/requests/authentication_pages_spec.rb
require 'spec_helper'

describe "Authentication" do
  .
  .
  .
  describe "authorization" do

    describe "for non-signed-in users" do
      let(:user) { FactoryGirl.create(:user) }

      describe "in the Users controller" do
        .
        .
        .
        describe "visiting the following page" do
          before { visit following_user_path(user) }
          it { should have_title('Sign in') }
        end

        describe "visiting the followers page" do
          before { visit followers_user_path(user) }
          it { should have_title('Sign in') }
        end
      end
      .
      .
      .
    end
    .
    .
    .
  end
  .
  .
  .
end
リスト11.29 followed_usersページとfollowersページをテストする。
spec/requests/user_pages_spec.rb
require 'spec_helper'

describe "User pages" do
  .
  .
  .
  describe "following/followers" do
    let(:user) { FactoryGirl.create(:user) }
    let(:other_user) { FactoryGirl.create(:user) }
    before { user.follow!(other_user) }

    describe "followed users" do
      before do
        sign_in user
        visit following_user_path(user)
      end

      it { should have_title(full_title('Following')) }
      it { should have_selector('h3', text: 'Following') }
      it { should have_link(other_user.name, href: user_path(other_user)) }
    end

    describe "followers" do
      before do
        sign_in other_user
        visit followers_user_path(other_user)
      end

      it { should have_title(full_title('Followers')) }
      it { should have_selector('h3', text: 'Followers') }
      it { should have_link(user.name, href: user_path(user)) }
    end
  end
end

この実装には1つだけトリッキーな部分があります。Usersコントローラに2つの新しいアクションを追加する必要があるのですが、これはリスト11.18で定義した2つのルートにもとづいており、これらはそれぞれfollowingおよびfollowersと呼ぶ必要があります。それぞれのアクションでは、タイトルを設定し、ユーザーを検索し、@user.followed_usersまたは@user.followersからデータを取り出し、ページネーションを行なって、ページを出力する必要があります。作成したアクションをリスト11.30に示します。

リスト11.30 followingアクションとfollowersアクション。
app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :signed_in_user,
                only: [:index, :edit, :update, :destroy, :following, :followers]
  .
  .
  .
  def following
    @title = "Following"
    @user = User.find(params[:id])
    @users = @user.followed_users.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

これらのアクションでは、render明示的に呼び出していることに注意してください。ここでは、作成の必要なshow_followというビューを出力しています。renderで呼び出しているビューが同じである理由は、このERbはどちらの場合でもほぼ同じであり、リスト11.31で両方の場合をカバーできるためです。

リスト11.31 フォローしているユーザーの表示とフォロワーの表示の両方に使用するshow_followビュー。
app/views/users/show_follow.html.erb
<% provide(:title, @title) %>
<div class="row">
  <aside class="span4">
    <section>
      <%= 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>
      <%= 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="span8">
    <h3><%= @title %></h3>
    <% if @users.any? %>
      <ul class="users">
        <%= render @users %>
      </ul>
      <%= will_paginate %>
    <% end %>
  </div>
</div>

これでテストにパスするはずです。画面も、図11.16 (フォローしているユーザー) および 図11.17 (フォロワー) のようになるはずです。

user_following_bootstrap
図11.16現在のユーザーにフォローされているユーザーを表示する。(拡大)
user_followers_bootstrap
図11.17現在のユーザーのフォロワーを表示する。(拡大)

11.2.4[フォローする] ボタン (標準的な方法)

ビューが整ってきました。いよいよ [フォローする] [フォロー解除する] ボタンを動作させましょう。これらのボタンのテストには、本書で扱ったさまざまなテスティングの技法が集約されています。このテストコードを読むのはよい練習になります。リスト11.32の内容をじっくり勉強し、テスト内容とその目的をすべて理解できるようにしてください (リスト11.32のコードには実は若干のセキュリティ上の抜けがあります。皆さんは見つけることができるでしょうか。この点についてはこの後でカバーします)。このコードにおけるhave_xpathメソッドの使用法に注目してください。 これは、XPathを使用してHTML5を含むXMLドキュメントを自在にナビゲートすることのできる、極めて高度かつパワフルなテクニックです。Web検索でXPathを使用する方法の詳細についてはXPath構文 (英語) を参照してください。

リスト11.32 Follow/Unfollowボタンをテストする。
spec/requests/user_pages_spec.rb
require 'spec_helper'

describe "User pages" do
  .
  .
  .
  describe "profile page" do
    let(:user) { FactoryGirl.create(:user) }
    .
    .
    .
    describe "follow/unfollow buttons" do
      let(:other_user) { FactoryGirl.create(:user) }
      before { sign_in user }

      describe "following a user" do
        before { visit user_path(other_user) }

        it "should increment the followed user count" do
          expect do
            click_button "Follow"
          end.to change(user.followed_users, :count).by(1)
        end

        it "should increment the other user's followers count" do
          expect do
            click_button "Follow"
          end.to change(other_user.followers, :count).by(1)
        end

        describe "toggling the button" do
          before { click_button "Follow" }
          it { should have_xpath("//input[@value='Unfollow']") }
        end
      end

      describe "unfollowing a user" do
        before do
          user.follow!(other_user)
          visit user_path(other_user)
        end

        it "should decrement the followed user count" do
          expect do
            click_button "Unfollow"
          end.to change(user.followed_users, :count).by(-1)
        end

        it "should decrement the other user's followers count" do
          expect do
            click_button "Unfollow"
          end.to change(other_user.followers, :count).by(-1)
        end

        describe "toggling the button" do
          before { click_button "Unfollow" }
          it { should have_xpath("//input[@value='Follow']") }
        end
      end
    end
  end
  .
  .
  .
end

リスト11.32は、これらのボタンをクリックし、そのときの正しい動作を指定することによりテストを行います。この実装を書くには、正しい動作をより深く理解する必要があります。フォローすることとフォロー解除することは、それぞれリレーションシップを作成することと削除することです。これはつまり、Relationshipsコントローラでcreateアクションとdestroyを定義するということであり、このコントローラを作成する必要があります。これらのボタンはユーザーがサインインしていないと表示されないので一見セキュリティを満たしているように見えますが、リスト11.32には、ある低レベルの問題が抜けています。つまり、createアクションとdestroyアクションはサインインしているユーザーのみがアクセスできることを確認するテストがありません (これが上で述べたセキュリティホールです) 。リスト11.33では、postメソッドとdeleteメソッドを使用してこれらのアクションに直接アクセスすることによって、この要求を満たすようにしました。

リスト11.33 Relationshipsコントローラの認可をテストする。
spec/requests/authentication_pages_spec.rb
require 'spec_helper'

describe "Authentication" do
  .
  .
  .
  describe "authorization" do

    describe "for non-signed-in users" do
      let(:user) { FactoryGirl.create(:user) }
      .
      .
      .
      describe "in the Relationships controller" do
        describe "submitting to the create action" do
          before { post relationships_path }
          specify { expect(response).to redirect_to(signin_path) }
        end

        describe "submitting to the destroy action" do
          before { delete relationship_path(1) }
          specify { expect(response).to redirect_to(signin_path) }
        end
      end
      .
      .
      .
    end
  end
end

すぐ削除されるので事実上意味のないRelationshipオブジェクトをわざわざ作成することによるオーバーヘッドを回避するために、deleteテストでは名前付きルートにid 1をハードコードしてあります。

before { delete relationship_path(1) }

ユーザーがリダイレクトされた後で、アプリケーションがこのidでリレーションシップを取り出すので、このコードは動作します。

上のテストにパスするための以下のコントローラのコードは、驚くほど簡潔です。単に、フォローしているユーザーまたはこれからフォローするユーザーを取り出し、関連するユーティリティメソッドを使用してそれらをフォローまたはフォロー解除しているだけです。すべての実装をリスト11.34に示します。

リスト11.34 Relationshipsコントローラ。
app/controllers/relationships_controller.rb
class RelationshipsController < ApplicationController
  before_action :signed_in_user

  def create
    @user = User.find(params[:relationship][: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

リスト11.34を見てみれば、先ほどのセキュリティ問題が実はそれほど重大なものではないことを理解いただけると思います。もしサインインしていないユーザーが (curlなどのコマンドラインツールなどを使用して) これらのアクションに直接アクセスするようなことがあれば、current_usernilになり、どちらのメソッドでも2行目で例外が発生します。エラーにはなりますが、アプリケーションやデータに影響は生じません。このままでも支障はありませんが、やはりこのような例外には頼らない方がよいので、上ではひと手間かけてセキュリティのためのレイヤーを追加しました。

これで、フォロー/フォロー解除の機能が完成しました。どのユーザーも、他のユーザーをフォローしたり、フォロー解除したりできます。サンプルアプリケーションを実際に動かしてみたり、以下のようにテストスイートを実行することで動作を確認できます。

$ bundle exec rspec spec/

11.2.5[フォローする] ボタン (Ajax)

フォロー関連の機能の実装はこのとおり完了しましたが、ステータスフィードに取りかかる前にもう一つだけ機能を洗練させてみたいと思います。11.2.4では、Relationshipsコントローラのcreateアクションと destroyアクションを単に元のプロファイルにリダイレクトしていました。つまり、ユーザーはプロファイルページを最初に表示し、それからユーザーをフォローし、その後すぐ元のページにリダイレクトされるという流れになります。ユーザーをフォローした後、本当にそのページから離れて元のページに戻らないといけないのでしょうか。この点を考えなおしてみましょう。

これはAjaxを使用することで解決できます。Ajaxを使用すれば、Webページからサーバーに「非同期」で、ページを移動することなくリクエストを送信することができます9。WebフォームにAjaxを採用するのは今や当たり前になりつつあるので、RailsでもAjaxを簡単に実装できるようになっています。フォロー用とフォロー解除用のフォームパーシャルをこれに沿って更新するのは簡単です。以下のコードがあるとします。

form_for

上を以下のように変更します。

form_for ..., remote: true

たったこれだけで、Railsは自動的にAjaxを使用します10。更新の結果をリスト11.35リスト11.36に示します。

リスト11.35 フォロー用のフォームでAjaxを使用する。
app/views/users/_follow.html.erb
<%= form_for(current_user.relationships.build(followed_id: @user.id),
             remote: true) do |f| %>
  <div><%= f.hidden_field :followed_id %></div>
  <%= f.submit "Follow", class: "btn btn-large btn-primary" %>
<% end %>
リスト11.36 フォロー解除用のフォームでAjaxを使用する。
app/views/users/_unfollow.html.erb
<%= form_for(current_user.relationships.find_by(followed_id: @user.id),
             html: { method: :delete },
             remote: true) do |f| %>
  <%= f.submit "Unfollow", class: "btn btn-large" %>
<% end %>

ERbによって実際に生成されるHTMLはそれほど重要ではありませんが、興味がある方のために、以下の核心部分をお見せします。

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

ここでは、formタグの内部でdata-remote="true"変数を設定しています。これは、JavaScriptによるフォーム操作を許可することをRailsに知らせるためのものです。以前のRailsでは完全なJavaScriptコードを挿入していましたが、Rails 3からは、このようにHTMLプロパティを使用して簡単にJavaScriptを使用できます。これは、JavaScriptを前面に出すべからずという哲学に従っています。

フォームの更新が終わったので、今度はこれに対応するRelationshipsコントローラを改造して、Ajaxリクエストに応答できるようにしましょう。実は、Ajaxを使用するとテスティングがかなりトリッキーになってしまいます。Ajaxのテストはそれだけで大きなテーマなので、本格的に説明しようとすると本書の範囲を超えてしまいます。ここではリスト11.37のテストを使用しましょう。ここではxhrメソッド (“XmlHttpRequest” の略です) を使用してAjaxリクエストを発行しています。以前の、getpostpatchdeleteメソッドを使用したテストと比べてみてください。それから、Ajaxリクエストを受信したときにcreateアクションとdestroyアクションが正常に動作することを確認します (Ajaxを多用するアプリケーションを徹底的にテストしたい方は、SeleniumWatirを参照してみてください)。

リスト11.37 RelationshipsコントローラがAjaxリクエストに応答することをテストする。
spec/controllers/relationships_controller_spec.rb
require 'spec_helper'

describe RelationshipsController do

  let(:user) { FactoryGirl.create(:user) }
  let(:other_user) { FactoryGirl.create(:user) }

  before { sign_in user, no_capybara: true }

  describe "creating a relationship with Ajax" do

    it "should increment the Relationship count" do
      expect do
        xhr :post, :create, relationship: { followed_id: other_user.id }
      end.to change(Relationship, :count).by(1)
    end

    it "should respond with success" do
      xhr :post, :create, relationship: { followed_id: other_user.id }
      expect(response).to be_success
    end
  end

  describe "destroying a relationship with Ajax" do

    before { user.follow!(other_user) }
    let(:relationship) do
      user.relationships.find_by(followed_id: other_user.id)
    end

    it "should decrement the Relationship count" do
      expect do
        xhr :delete, :destroy, id: relationship.id
      end.to change(Relationship, :count).by(-1)
    end

    it "should respond with success" do
      xhr :delete, :destroy, id: relationship.id
      expect(response).to be_success
    end
  end
end

リスト11.37のコードは、実はコントローラ用テストとしては初めてのものです。本書の以前の版ではコントローラ用のテストを多用していましたが、現在は結合テストの観点からコントローラ向けのテストは控えています。ただし、どういうわけかこの場合xhrメソッドを結合テストで使用することができないために、このコントローラでのテストを行なっています。xhrは先ほど登場したばかりですが、本書ではひとまずコードの文脈から以下のコードの動作を推測していただくようお願いします。

xhr :post, :create, relationship: { followed_id: other_user.id }

xhrが取る引数は、関連するHTTPメソッドを指すシンボル、アクションを指すシンボル、またはコントローラ自身にあるparamsの内容を表すハッシュのいずれかです。これまでの例と同様、expectを使用してブロック内の操作をまとめ、関連するカウントを1増やしたり減らしたりするテストを行なっています。

このテストが暗に示しているように、実はこのアプリケーションコードがAjaxリクエストへの応答に使用するcreateアクションとdestroyアクションは、通常のHTTP POSTリクエストとDELETEリクエストに応答するのに使用されるのと同じものです。これらのアクションは、11.2.4のようにリダイレクトを伴う通常のHTTPリクエストと、JavaScriptを使用するAjaxリクエストの両方に応答できればよいのです。実際のコントローラのコードをリスト11.38に示します。(練習のため、11.5のコードも見てみてください。このコードは動作は同じですが、よりコンパクトになっています。)

リスト11.38 RelationshipsコントローラでAjaxリクエストに応答する。
app/controllers/relationships_controller.rb
class RelationshipsController < ApplicationController
  before_action :signed_in_user

  def create
    @user = User.find(params[:relationship][: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

このコードでは、リクエストの種類に応じたアクションを実行するためにrespond_toを使用しています。(注意: ここで使用しているrespond_toは、RSpecの例で使用しているrespond_toとは別物です)。この文法は少々変わっていて、混乱を招く可能性があるため、以下のコードの動作を理解するようにしてください。

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

上のコードでは、リクエストの種類に応じて、続く行の中から1つだけが実行されることに注意してください。

Ajaxリクエストを受信した場合は、Railsが自動的にアクションと同じ名前を持つJavaScript組み込みRuby (.js.erb) ファイル (create.js.erbdestroy.js.erbなど) を呼び出します。ご想像のとおり、これらのファイルではJavaScriptと組み込みRuby (ERb) をミックスして現在のページに対するアクションを実行することができます。ユーザーをフォローしたときやフォロー解除したときにユーザープロファイルページを更新するために、私たちがこれから作成および編集しなければならないのは、まさにこれらのファイルです。

JS-ERbファイルの内部では、Railsが自動的にjQuery JavaScriptヘルパーを提供します。これにより、DOM (Document Object Model) を使用してページを操作できます。jQueryライブラリにはDOM操作用の膨大なメソッドが提供されていますが、ここで使用するのはわずか2つです。それにはまず、ドル記号 ($) を使用してDOM要素に一意のCSS idでアクセスする文法について知る必要があります。たとえば、follow_form要素を操作するには、以下の文法を使用します。

$("#follow_form")

(リスト11.23では、これはフォームを囲むdivタグであり、フォームそのものではなかったことを思い出してください。) jQueryの文法はCSSの記法から影響を受けており、#シンボルを使用してCSSのidを指定します。ご想像のとおり、jQueryはCSSと同様、ドット.を使用してCSSクラスを操作できます。

次に必要なメソッドはhtmlです。これは、引数の中で指定された要素の内側にあるHTMLを更新します。たとえば、フォロー用フォーム全体を"foobar"という文字列で置き換えるには、以下を使用します。

$("#follow_form").html("foobar")

純粋なJavaScriptと異なり、JS-ERbファイルでは組み込みRuby (ERb) を使用できます。create.js.erbファイルでは、フォロー用のフォームをunfollowパーシャルで更新し、フォロワーのカウントを更新するのにERbを使用しています (もちろんこれは、フォローに成功した場合の動作です)。変更の結果をリスト11.39に示します。このコードではescape_javascript関数を使用していることに注目してください。この関数は、JavaScriptファイル内にHTMLを挿入するときに実行結果をエスケープする (画面に表示しない) ために必要です。

リスト11.39 JavaScript組み込みRubyを使用してフォローのリレーションシップを作成する。
app/views/relationships/create.js.erb
$("#follow_form").html("<%= escape_javascript(render('users/unfollow')) %>")
$("#followers").html('<%= @user.followers.count %>')

destroy.js.erbファイルの方も同様です (リスト11.40)。

リスト11.40 Ruby JavaScript (RJS) を使用してフォローのリレーションシップを削除する。
app/views/relationships/destroy.js.erb
$("#follow_form").html("<%= escape_javascript(render('users/follow')) %>")
$("#followers").html('<%= @user.followers.count %>')

これらのコードにより、ユーザープロファイルを表示して、ページを更新せずにフォローまたはフォロー解除ができるようになったはずです。テストスイートもパスするはずです。

$ bundle exec rspec spec/

RailsでAjaxを使用するというテーマは奥が深く、かつ進歩が早いので、本書ではほんの表面をなぞったに過ぎません。しかし、本書の他の題材と同様、今後より高度な資料にあたる際に必要な基礎となるはずです。

11.3ステータスフィード

ついに、サンプルアプリケーションの山頂が目の前に現れました。最後の難関、ステータスフィードの実装に取りかかりましょう。この節で扱われている内容は、本書の中でも最も高度なものです。完全なステータスフィードは、10.3.3で扱ったプロトフィードをベースにします。現在のユーザーにフォローされているユーザーのマイクロポストの配列を作成し、現在のユーザー自身のマイクロポストと合わせて表示します。この機能を実現するには、RailsとRubyの高度な機能の他に、SQLプログラミングの技術も必要です。

手強い課題に挑むのですから、ここで実装すべき内容を慎重に見直すことが重要です。図11.5でお見せしたステータスフィードの最終形を図11.18に再度掲載します。

page_flow_home_page_feed_mockup_bootstrap
図11.18ステータスフィードが表示されたHomeページのモックアップ。(拡大)

11.3.1動機と計画

ステータスフィードの基本的なアイディアはシンプルです。図11.19に、サンプルのmicropostsデータベーステーブルと、それをフィードした結果を示します。図の矢印で示されているように、この目的は、現在のユーザーによってフォローされているユーザーに対応するユーザーidを持つマイクロポストを取り出し、同時に現在のユーザー自身のマイクロポストも一緒に取り出すことです。

user_feed
図11.19: id 1のユーザーがid 2、7、8、10をフォローしているときのフィード。

あるユーザーによってフォローされているユーザーのマイクロポストをすべて見つけ出すために、from_users_followed_byというメソッドを実装することを考えてみましょう。このメソッドは、以下のように使用します。

Micropost.from_users_followed_by(user)

この時点ではもちろん実装はありませんが、機能を確認するためのテストは作成できます。このテストで重要なことは、フィードに必要な3つの条件を満たすことです。1) フォローしているユーザーのマイクロポストがフィードに含まれていること。2) 自分自身のマイクロポストもフィードに含まれていること。3) フォローしていないユーザーのマイクロポストがフィードに含まれていないこと。このうち2つについては、リスト10.35のテストで既に確認できるようになっています。このテストでは、ユーザー自身のマイクロポストがフィードに含まれ、フォローしていないユーザーのマイクロポストがフィードに含まれていないことを確認します。既にユーザーをフォローできるようになっているので、3番目のテスト、つまりフォローしているユーザーのマイクロポストがフィードに含まれていることを確認するテストをリスト11.41のように追加できます。

リスト11.41 ステータスフィードの最終テスト。
spec/models/user_spec.rb
require 'spec_helper'

describe User do
  .
  .
  .
  describe "micropost associations" do
    before { @user.save }
    let!(:older_micropost) do
      FactoryGirl.create(:micropost, user: @user, created_at: 1.day.ago)
    end
    let!(:newer_micropost) do
      FactoryGirl.create(:micropost, user: @user, created_at: 1.hour.ago)
    end
    .
    .
    .
    describe "status" do
      let(:unfollowed_post) do
        FactoryGirl.create(:micropost, user: FactoryGirl.create(:user))
      end
      let(:followed_user) { FactoryGirl.create(:user) }

      before do
        @user.follow!(followed_user)
        3.times { followed_user.microposts.create!(content: "Lorem ipsum") }
      end

      its(:feed) { should include(newer_micropost) }
      its(:feed) { should include(older_micropost) }
      its(:feed) { should_not include(unfollowed_post) }
      its(:feed) do
        followed_user.microposts.each do |micropost|
          should include(micropost)
        end
      end
    end
  end
  .
  .
  .
end

ステータスフィードのモデルのコードは、リスト11.42に示したように、面倒な部分をMicropost.from_users_followed_byに任せてしまっています。この内容は後で実装しなければなりません。

リスト11.42 Userモデルに完全なフィードを追加する。
app/models/user.rb
class User < ActiveRecord::Base
  .
  .
  .
  def feed
    Micropost.from_users_followed_by(self)
  end
  .
  .
  .
end

11.3.2フィードを初めて実装する

では、その面倒なMicropost.from_users_followed_byを実装しましょう。簡単のため、以後このメソッドを単に “フィード” と呼びます。最終的な表示がやや込み入っているため、欲張らずに細かい部品を1つずつ確かめながら導入することで最終的なフィードを実装します。

最初に、このフィードで必要なクエリについて考えましょう。ここで必要なのは、micropostsテーブルから、あるユーザー (つまり自分自身) がフォローしているユーザーに対応するidを持つマイクロポストをすべて選択 (select) することです。このクエリを模式的に書くと以下のようになります。

SELECT * FROM microposts
WHERE user_id IN (<list of ids>) OR user_id = <user id>

上のコードを書く際に、SQLがINというキーワードをサポートしていることを前提にしています (大丈夫、実際にサポートされています)。このキーワードを使用することで、idの集合の内包 (set inclusion) に対してテストを行えます。

10.3.3のプロトフィードでは、上のような選択を行うためにActive Recordでリスト10.36のようにwhereメソッドを使用していたことを思い出してください。このときは、選択する対象はシンプルでした。現在のユーザーに対応するユーザーidを持つマイクロポストをすべて選択すればよかったのでした。

Micropost.where("user_id = ?", id)

ここで行いたい選択は、上よりももう少し複雑で、たとえば以下のような感じになります。

where("user_id in (?) OR user_id = ?", following_ids, user)

(上のコードでは、条件部分にuser.idの代わりにRailsの習慣であるuserを使用していることに注意してください。Railsはこのような場合に自動的にidを使用します。また、冒頭のMicropost.が省略されていることにも注意してください。このコードはMicropostモデル自身の中に置かれることを前提としています。)

これらの条件から、フォローされているユーザーに対応するidの配列が必要であることがわかってきました。これを行う方法の1つは、Rubyのmapメソッドを使用することです。このメソッドはすべての "列挙可能 (enumerable)" オブジェクト (配列やハッシュなど、要素の集合で構成されるあらゆるオブジェクト11) で使用できます。このメソッドの使用法については4.3.2でも説明しました。以下のように使用します。

$ rails console
>> [1, 2, 3, 4].map { |i| i.to_s }
=> ["1", "2", "3", "4"]

上に示したような状況では、各要素に対して同じメソッド (この場合to_s) が実行されます。これは非常によく使われる方法であり、以下のようにアンパサンド &と、メソッドに対応するシンボルを使用した短縮表記も可能です12。この短縮表記なら変数iを使用せずに済みます。

>> [1, 2, 3, 4].map(&:to_s)
=> ["1", "2", "3", "4"]

joinメソッド (4.3.1) を使用すれば、idを集めた文字列を以下のようにカンマ区切りでつなげることもできます。

>> [1, 2, 3, 4].map(&:to_s).join(', ')
=> "1, 2, 3, 4"

上のメソッドを使用すれば、user.followed_usersにある各要素のidを呼び出し、フォローしているユーザーのidの配列を構成することができます。たとえば、データベースの最初のユーザーの場合は、以下の配列になります。

>> User.first.followed_users.map(&:id)
=> [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42,
43, 44, 45, 46, 47, 48, 49, 50, 51]

実際、この手法は実に便利なので、Active Recordは以下でもデフォルトで同じ結果を返します。

>> User.first.followed_user_ids
=> [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42,
43, 44, 45, 46, 47, 48, 49, 50, 51]

このfollowed_user_idsメソッドは、実はActive Recordによってhas_many :followed_users関連付けから自動生成されたものです (リスト11.10)。これにより、user.followed_usersコレクションに対応するidを得るための_idsを、関連付けの名前に追加するだけで済みます。フォローしているユーザーidの文字列は以下のようになります。

>> User.first.followed_user_ids.join(', ')
=> "4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42,
43, 44, 45, 46, 47, 48, 49, 50, 51

なお、以上は説明のためのコードであり、実際にSQL文字列に挿入するときは、このように記述する必要はありません。実は、?を内挿すると自動的にこの辺りの面倒を見てくれます。さらに、データベースに依存する一部の非互換性まで解消してくれます。つまり、ここでは以下をそのまま使えばよいだけなのです。

user.followed_user_ids

それ以外の追加は不要です。

ここで、以下のコードを見てみましょう。

Micropost.from_users_followed_by(user)

このコードはMicropostクラスのクラスメソッド (4.4.1で簡単に説明したコンストラクタ) に関連するのではないかと推測した方もいると思います。 これに沿って実装した推奨コードをリスト11.43に示します。

リスト11.43 from_users_followed_byの最初の実装。
app/models/micropost.rb
class Micropost < ActiveRecord::Base
  .
  .
  .
  def self.from_users_followed_by(user)
    followed_user_ids = user.followed_user_ids
    where("user_id IN (?) OR user_id = ?", followed_user_ids, user)
  end
end

上のリスト11.43まで述べてきたことは仮説に過ぎませんでしたが、それを実装したコードは確かに動きます。テストスイートを実行して確認することもできます。このテストはパスするはずです。

$ bundle exec rspec spec/

簡単なアプリケーションであれば、この最初の実装だけでほとんどの目的を達成できるでしょう。しかし、私たちのサンプルアプリケーションの実装にはまだ足りないものがあります。それが何なのか、次の節に進む前に考えてみてください (ヒント:フォローしているユーザーが5000人もいたらどうなるでしょうか)。

11.3.3サブセレクト

前節のヒントでおわかりのように、11.3.2のフィードの実装は、投稿されたマイクロポストの数が膨大になったときにうまくスケールアップできません。フォローしているユーザーが5000人程度になるとこういうことが起きる可能性があります。この節では、フォローしているユーザー数に応じてスケーリングできるように、ステータスフィードを再度実装します。

11.3.2で問題となるのは、以下のコードです。

followed_user_ids = user.followed_user_ids

このコードは、フォローしているすべてのユーザーをメモリーから一気に取り出し、フォローしているユーザーの完全な配列を作り出します。リスト11.43の条件では、集合に内包されているかどうかだけしかチェックされていないため、この部分をもっと効率的なコードにできるはずです。そして、SQLは本来このような集合の操作に最適化されています。これを解決する方法は、フォローしているユーザーのidの検索をデータベースに保存するときにサブセレクト (subselect) を使用することです。

リスト11.44でコードを若干修正し、フィードをリファクタリングすることから始めましょう。

リスト11.44 from_users_followed_byを改良する。
app/models/micropost.rb
class Micropost < ActiveRecord::Base
  .
  .
  .
  # 与えられたユーザーがフォローしているユーザー達のマイクロポストを返す。
  def self.from_users_followed_by(user)
    followed_user_ids = user.followed_user_ids
    where("user_id IN (:followed_user_ids) OR user_id = :user_id",
          followed_user_ids: followed_user_ids, user_id: user)
  end
end

次の段階の準備として、以下のコードを

where("user_id IN (?) OR user_id = ?", followed_user_ids, user)

以下の同等のコードに置き換えました。

where("user_id IN (:followed_user_ids) OR user_id = :user_id",
      followed_user_ids: followed_user_ids, user_id: user)

前者の疑問符を使用した文法も便利ですが、同じ変数を複数の場所に挿入したい場合は、後者の置き換え後の文法を使用するのがより便利です。

上の説明が暗に示すように、これからSQLクエリにもう1つuser_idを追加します。特に、以下のRubyコードは、

followed_user_ids = user.followed_user_ids

以下のSQLスニペットと置き換えることができます。

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

このコードではSQLサブセレクトが使用されています。ユーザー1についてすべてを選択することは、内部的には以下のような感じになります。

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

このサブセレクトは、集合のロジックを (Railsではなく) データベースに保存するので、より効率が高まります13

これで基礎を固めることができましたので、リスト11.45のように効率のよいフィードを実装する準備ができました。ここに記述されているのはナマのSQLなので、followed_user_idsはエスケープではなく内挿 (interpolate) されることに注意してください (実際はどちらでも動作しますが、この文脈では内挿と考える方が筋が通っています)。

リスト11.45 from_users_followed_byの最終的な実装。
app/models/micropost.rb
class Micropost < ActiveRecord::Base
  belongs_to :user
  default_scope -> { order('created_at DESC') }
  validates :content, presence: true, length: { maximum: 140 }
  validates :user_id, presence: true

  # 与えられたユーザーがフォローしているユーザー達のマイクロポストを返す。
  def self.from_users_followed_by(user)
    followed_user_ids = "SELECT followed_id FROM relationships
                         WHERE follower_id = :user_id"
    where("user_id IN (#{followed_user_ids}) OR user_id = :user_id",
          user_id: user.id)
  end
end

このコードはRailsとRubyとSQLが複雑に絡み合っていて厄介ですが、ちゃんと動作します。(もちろん、サブセレクトを使用すればいくらでもスケールアップできるなどということはありません。大規模なWebサイトでは、バックグラウンドジョブを使用して、フィードを非同期で生成するなどの対策が必要でしょう。Webサイトのスケーリングのようなデリケートな問題は本書の範疇を超えます)。

11.3.4新しいステータスフィード

ステータスフィードのコードはリスト11.45で完成しました。確認のため、Homeページ用のコードもリスト11.46に示します。このコードは、図11.20に示したように、ビューで使用するマイクロポストに関連するフィードをページネーションして作成します14。このpaginateメソッドは、データベースから一度に30ずつマイクロポストを取り出しますが、実はこのメソッドがリスト11.45のMicropostモデルのメソッドにまではるばる到達して動作することに注目してください (開発サーバーのログ・ファイルに出力されたSQL文を調べることで、このことを確認できます)。

リスト11.46 homeアクションのページネーション付きフィード (再掲)。
app/controllers/static_pages_controller.rb
class StaticPagesController < ApplicationController

  def home
    if signed_in?
      @micropost  = current_user.microposts.build
      @feed_items = current_user.feed.paginate(page: params[:page])
    end
  end
  .
  .
  .
end
home_page_with_feed_bootstrap
図11.20Homeページで動作するステータスフィード。(拡大)

11.4最後に

ステータスフィードが追加され、Ruby on Railsチュートリアルの中心となるサンプルアプリケーションがとうとう完成しました。このサンプルアプリケーションには、Railsの主要な機能 (モデル、ビュー、コントローラ、テンプレート、パーシャル、フィルタ、検証、コールバック、has_many/belongs_to/has_many through関連付け、セキュリティ、テスティング、デプロイ) が多数含まれています。これだけでもかなりの量ですが、Railsについて学ぶべきことはまだまだたくさんあります。今後の学習の手始めとするために、この節ではサンプルアプリケーションのコア部分のさまざまな拡張方法を提案し、それぞれに必要な学習内容についても示します。

アプリケーションの拡張に取りかかる前に、まずは現状の変更をマージしておきましょう。

$ git add .
$ git commit -m "Add user following"
$ git checkout master
$ git merge following-users

必要であれば、いつものようにコードをプッシュしてデプロイします。

$ git push
$ git push heroku
$ heroku pg:reset DATABASE
$ heroku run rake db:migrate
$ heroku run rake db:populate

11.4.1サンプルアプリケーションの機能を拡張する

この節で提案するさまざまな拡張 (パスワードリマインダ、メールによる確認、サンプルアプリケーション向けには検索、返信、メッセージングなど) は、ほとんどがWebアプリケーションで一般的な機能です。これらの拡張を1つか2つでも実装してみることで、本書から巣立って自分のアプリケーションを書くときにきっと役に立つことでしょう。

いざ実装し始めてみると思ったよりずっと難しく感じるかもしれませんが、それも当然です。新しい機能という真っ白なキャンバスを目の前にすれば、気後れしてしまうのも無理はありません。皆さんが拡張を始めるにあたり、ささやかながら私から2つほど一般的なアドバイスをしてみたいと思います。1) Railsアプリケーションに何らかの機能を追加するときには、ぜひRailsCastsアーカイブをチェックしてみてください。今自分がやろうとしていることは、既にRyan Batesが取り上げたトピックにあるかもしれません15。もし手頃なトピックがあれば、関連するRailsCastをウォッチすることで、時間を大幅に節約できることでしょう。2) できるだけ念入りにGoogleで検索し、自分が調べようとしているトピックに言及しているブログやチュートリアルがないかどうか、よく探すことです。Webアプリケーションの開発には常に困難がつきまといます。他人の経験と失敗から学ぶことも重要です。

以下の機能はどれも難易度がそれなりに高いので、実装に必要となるかもしれないツールについてのヒントも書いておきました。たとえヒントがあったとしても、以下の機能が本書の最終章の演習よりもずっと難易度が高いことは変わりません。相当頑張ったにもかかわらず挫折することも当然あると思いますので、どうかそんなときには落ち込まないでください。私の時間にも限りがありますので、皆さんに個別のお手伝いをすることはできそうにありません。しかし、これらの拡張のいくつかについてなら、今後単発の記事やスクリーンキャストバンドルを公開することがあると思います。「Railsチュートリアル」http://railstutorial.jp/のメインページを参照し、ニュースフィードを購読して最新情報をチェックしてみてください。

返信機能

Twitterには、マイクロポスト入力中に@記号に続けてユーザーのログイン名を入力するとそのユーザーに返信できる機能があります。このポストは、宛先のユーザーのフィードと、自分をフォローしているユーザーにのみ表示されます。この返信機能の簡単なバージョンを実装してみましょう。具体的には、@replyは受信者のフィードと送信者のフィードにのみ表示されるようにします。これを行うには、micropostsテーブルのin_reply_toカラムと、追加のincluding_repliesスコープをMicropostモデルに追加する必要があるとおもいます

このサンプルアプリケーションには独自のユーザーログインがないので、ユーザーを一意に表す方法も考えなければならないでしょう。1つの方法は、idと名前を組み合わせて@1-michael-hartlのようにすることです。もう1つの方法は、ユーザー登録の項目に一意のユーザー名を追加し、@repliesで使えるようにすることです。

メッセージ機能

Twitterでは、マイクロポストの入力時に最初に “d” キーを押すとダイレクト (プライベート) メッセージを行える機能がサポートされています。この機能をサンプルアプリケーションに実装してみましょう。ヒントは、Messageモデルと、新規マイクロポストにマッチする正規表現です。

フォロワーの通知

ユーザーに新しくフォロワーが増えたときにメールで通知する機能を実装してみましょう。続いて、メールでの通知機能をオプションとして選択可能にし、不要な場合は通知をオフにできるようにしてみましょう。この機能を追加するには、Railsからメールを送信する機能を追加する必要があります。最初にRailsCast「Rails 3のAction Mailer」を参照してください。

パスワードリマインダー

現状のサンプルアプリケーションには、ユーザーがパスワードを忘れてしまったときの復旧手段がありません。第6章で一方向セキュアパスワードハッシュを実装したため、パスワードをメールで送信することは不可能ですが、パスワードをリセットするリンクをメールで送信することなら可能です。必要な知識については、RailsCast「パスワード保存機能とパスワードリセット機能について (英語)」を参照してください。

ユーザー登録の確認

現在のサンプルアプリケーションには、正規表現による最小限の確認以外に、メールアドレスを検証する手段がありません。ユーザー登録時にメールアドレスを検証する手順を追加してください。この新機能では、ユーザー作成時に「仮のユーザーアカウント」を作成し、アクティベーション用のURLをメールで送信し、URLにユーザーがアクセスしたらユーザーアカウントを有効にするという手順が必要です。ユーザーアカウントを有効/無効にする方法については、「Rails ステートマシン」でネットを検索してみてください。

RSSフィード

ユーザーごとのマイクロポストをRSSフィードする機能を実装してください。次にステータスフィードをRSSフィードする機能も実装し、余裕があればフィードに認証スキームも追加してアクセスを制限してみてください。ヒントについてはRailsCast「RSSフィードの生成 (英語)」を参照してください。

REST API

多くのWebサイトはAPI (Application Programmer Interface) を公開しており、第三者のアプリケーションからリソースのget/post/put/deleteが行えるようになっています。サンプルアプリケーションにもこのようなREST APIを実装してください。解決のヒントは、respond_toブロック (11.2.5) をアプリケーションのコントローラの多くのアクションに追加することです。このブロックはXMLをリクエストされたときに応答します。セキュリティには十分注意してください。認可されたユーザーにのみAPIアクセスを許可する必要があります。

検索機能

現在のサンプルアプリケーションには、ユーザーインデックスページを端から探すか、他のユーザーのフィードを表示する以外に、他のユーザーを検索する手段がありません。この点を強化するために、検索機能を実装してください。続いて、マイクロポストを検索する機能も追加してください。あらかじめRailsCast「簡単な検索フォーム (英語)」を参照しておくとよいでしょう。このアプリケーションを共有ホストか専用のサーバーにデプロイするのであれば、Thinking Sphinxの導入をお勧めします (RailsCast「Thinking Sphinx (英語)」も参照してください)。Herokuでデプロイするのであれば、「Heroku全文検索機能」マニュアル (英語) に従う必要があります。(訳注: @budougumi0617 さんがRails 4.0版における簡単な検索フォームの実装例を公開してくれました。Thx!)

11.4.2読み物ガイド

読むに値するRails関連の書籍やドキュメントは書店やWebでいくらでも見つけられます。正直、あまりの多さに閉口するほどです。幸い、それらのほとんどが現在でも入手/アクセス可能です。より高度な技術を身に付けるためのお勧めリソースをいくつかリストアップします (訳注: 日本語の読み物ガイドは、本書のWebサイトのヘルプページにリストアップしてあります)。

  • Ruby on Railsチュートリアル スクリーンキャスト。本書に合わせて、完全版のスクリーンキャストを用意してあります。このスクリーンキャストでは、本書の話題をすべてカバーしているだけでなく、さまざまなコツや秘訣も満載されており、スクリーンショットだけでは捉えにくい実際の動作を動画で視聴することもできます。スクリーンキャストはRuby on RailsチュートリアルWebサイトから購入できます。
  • RailsCasts (英語)。強く推奨します。このRailsCastsの素晴らしさについては、どれほど言葉を尽くしても足りません。最初にRailsCastsエピソードアーカイブを開いて、目についたトピックを適当に開くところから始めてみるとよいでしょう。
  • RubyとRailsのお勧め書籍 (英語)。「Beginning Ruby」(Peter Cooper 著)、「The Well-Grounded Rubyist」(David A. Black著)、「Eloquent Ruby」(Russ Olsen著)、Rubyをさらに深く学ぶのであれば 「The Ruby Way」(Hal Fulton著) がお勧めです。Railsをさらに深く学ぶのであれば、「The Rails 3 Way」(Obie Fernandez著)と「Rails 3 in Action (第2版待ち)」(Ryan Bigg、Yehuda Katz著) がお勧めです。
  • PluralsightCode School (どちらも英語)。PluralsightのスクリーンキャストとCode Schoolのインタラクティブコースは品質が高いことで知られており、強くお勧めいたします。
  • (訳注) Rails ガイド: トピック毎に分類された最新のRailsリファレンスで、Railsチュートリアルの完走者や、Railsエンジニアを対象にして書かれています。本書で紹介しきれなかったActive ModelやAction View/Controllerの機能を解説する記事や、アセットパイプラインの仕組みやデバッグ手法を解説する記事などがあり、Railsについてより深く、体系的に学びたいときに便利です。合計 1,400 ページを超える大型書籍で (参考: 本書は合計700ページです)、本書と同様にWeb版は全て無料で読めます。
  • (訳注) 地域Rubyの会: 読み物ではありませんが、日本全国にRuby/Railsのコミュニティがあり、定期的に勉強会やミートアップ (集まってコードを書いたりコードの相談をしたりするイベント) を開催しています。Railsチュートリアルをここまで読み終えた方であれば十分なスキルは揃っているはずなので、地域Rubyの会の中にあるお近くのコミュニティまで遊びに行ってはいかがでしょうか?

11.5演習

  1. 特定のユーザーに関連付けられたrelationshipの削除をテストするコードを追加してください (対応する実装はリスト11.4およびリスト11.16dependent :destroyです)。ヒント: リスト10.12の例に従ってみましょう。
  2. リスト11.38respond_toメソッドは、そのアクションから出発してRelationshipsコントローラ自身に到達します。そして、respond_toブロックは、respond_withというRailsのメソッドと置き換え可能です。リスト11.47はこの置き換えを行ったものですが、このコードもテストスイートにパスすることを確認することで、このコードが正しく動作することを証明してください (このメソッドの詳細については、Googleで “rails respond_with” と検索してください)。
  3. リスト11.31をリファクタリングしてください。具体的には、フォローしているユーザーページ、フォロワーページ、Homeページ、ユーザー表示ページで共通して使用されているコードをパーシャルとして追加します。
  4. リスト11.19のモデルに従って、プロファイルページの統計情報のテストを作成してください。
リスト11.47 リスト11.38をコンパクトにリファクタリングしたもの。
class RelationshipsController < ApplicationController
  before_action :signed_in_user

  respond_to :html, :js

  def create
    @user = User.find(params[:relationship][:followed_id])
    current_user.follow!(@user)
    respond_with @user
  end

  def destroy
    @user = Relationship.find(params[:id]).followed
    current_user.unfollow!(@user)
    respond_with @user
  end
end
  1. モックアップツアーの写真は、それぞれ https://www.flickr.com/photos/john_lustig/2518452221/http://www.flickr.com/photos/30775272@N05/2884963755/より引用しました。
  2. 本書の最初の版ではuser.followingとしていましたが、結局これも混乱を招くことに気付きました。私にこの用語を変更する決心をさせてくれ、よりわかりやすいアドバイスを提供してくれた読者、Cosmo Leeに感謝いたします (なお、彼のアドバイスをそのまま採用したわけではありませんので、もし本書にまだわかりにくい部分があったとしても、彼には一切の責任がないことをここに申し伝えておきます) 。
  3. 簡単のため、図11.6では followed_usersテーブルのidカラムを省略してあります。
  4. 詳細については、Stack Overflowの「どんなときにletを使用すべきか (英語)」を参照してください。
  5. 技術的には、Railsはunderscoreメソッドを使用してクラス名をidに変換しています。たとえば、"FooBar".underscoreを実行すると"foo_bar"に変換されます。従って、 FooBarオブジェクトの外部キーはfoo_bar_idになるでしょう (なお、underscoreと逆の働きをするcamelizeというメソッドもあります。これは"camel_case""CamelCase"のように変換します) 。
  6. followed_idでもユーザーを特定できることに気付き、フォローしているユーザーとフォロワーの扱いが対称的でないことをよく考えてみれば、もうゲームに勝ったようなものです。これについては11.1.5でも扱います。
  7. 特定の分野でモデリングの経験を多く積めば、このようなユーティリティメソッドが必要になることを事前に思い付けるようになるでしょう。たとえ思い付けないことがあったとしても、明確なテストを書こうとするときに、いつの間にかこういうメソッドを自分が作成していることに気付くことでしょう。だからというわけではありませんが、今はこのようなメソッドが必要であるということに気付けなくても問題ありません。ソフトウェアの開発は、繰りかえしに次ぐ繰り返しです。読みづらくなるまでコードを書き足し、そのコードをリファクタリングする、その繰り返しです。そして、より簡潔なコードを書くために、本書が少しでもお役に立てばと思います。
  8. unfollow!は、失敗したときに例外を発生しないことに注意してください。正直に書くと、削除に失敗したことをRailsがどうやって検出しているのか、著者にもわかりません。follow!に感嘆符が付いているので、それに合わせてこのメソッドにも感嘆符を追加しているに過ぎません。
  9. Ajaxは、名目上asynchronous JavaScript and XMLの略ということになっています。最も初期のAjaxに関する記事でも "Ajax" で統一されていますが、ときどき “AJAX” と誤ったスペルが使用されていることがあります。
  10. Ajaxが動作するためには、ブラウザでJavaScriptが有効になっている必要がありますが、実際にはJavaScriptが無効になっている場合でも11.2.4のようにまったく同じ動作になるようにしてあります。
  11. 列挙可能 (enumerable) オブジェクトであることの主な条件は、eachメソッドを実装していることです。このメソッドはコレクションを列挙します。
  12. この記法は、実際にはRailsによるコアRuby言語の拡張として行われたのが始まりです。この記法があまりに便利なので、後にRuby自身にまで取り入れられたほどです。実にクールだと思いますが、いかがでしょうか。
  13. 必要なサブセレクトを作成するための、より高度な方法については、「ActiveRecordのサブセレクトをハックする (英語)」というブログ記事を参照してください。 
  14. 実際は、図11.20の見栄えを良くするために、Railsコンソールを使用してマイクロポストをいくつか手動で投稿しています。
  15. RailsCastsではテストを省略していることが多いので、その点には注意してください。1回のエピソードを短くまとめるためにテストを省略しているのですが、それに釣られてテスティングの重要性を軽く考えることのないようにしてください。RailsCastでアイディアやヒントを得たら、新機能の実装はぜひともテスト駆動開発で進めることをお勧めいたします。(その意味でも、RailsCast「テスティングの方法 (英語)」をぜひ一度参照してください。Ryan Bates自身も、現実にはテスト駆動開発を採用していることが多くありますし、彼のテスティングスタイルは本書のものと基本的に同じです。)
第11章 ユーザーをフォローする Rails 4.0 (第2版)