Ruby on Rails チュートリアル

実例を使ってRailsを学ぼう

Michael Hartl (マイケル・ハートル)

第3版 目次

前書き

私が前にいた会社 (CD Baby) は、かなり早い段階でRuby on Railsに乗り換えたのですが、またPHPに戻ってしまいました (詳細は私の名前をGoogleで検索してみてください)。そんな私ですが、Michael Hartl 氏の本を強く勧められたので、その本を使ってもう一度試してみた結果、今度は無事に Rails に乗り換えることができました。それがこの Ruby on Rails チュートリアルという本です。

私は多くの Rails 関連の本を参考にしてきましたが、真の決定版と呼べるものは本書をおいて他にありません。本書では、あらゆる手順が Rails 流で行われています。最初のうちは慣れるまでに時間がかかりましたが、この本を終えた今、ついにこれこそが自然な方式だと感じられるまでになりました。また、本書は Rails 関連の本の中で唯一、多くのプロが推奨するテスト駆動開発 (TDD: Test Driven Development) を、全編を通して実践しています。実例を使ってここまで分かりやすく解説された本は、本書が初めてでしょう。極めつけは、Git や GitHub、Heroku の実例に含めている点です。このような、実際の開発現場で使わているツールもチュートリアルに含まれているため、読者は、まるで実際のプロジェクトの開発プロセスを体験しているかのような感覚が得られるはずです。それでいて、それぞれの実例が独立したセクションになっているのではなく、そのどれもがチュートリアルの内容と見事に一体化しています。

本書は、筋道だった一本道の物語のようになっています。私自身、章の終わりにある練習問題もやりながら、この Rails チュートリアルを3日間かけて一気に読破しました1。最初から最後まで、途中を飛ばさずにやるのが一番効果的で有益な読み方です。ぜひやってみてください。

それでは、楽しんでお読みください!

Derek Sivers (sivers.org) CD Baby 創業者

謝辞

Ruby on Rails チュートリアルは、私の以前の著書「RailsSpace」と、その時の共著者の Aurelius Prochazka から多くのことを参考にさせてもらっています。Aure には、RailsSpace での協力と本書への支援も含め、感謝したいと思います。また、RailsSpaceRails チュートリアルの両方の編集を担当して頂いた Debra Williams Cauley 氏にも謝意を表したく思います。彼女が野球の試合に連れて行ってくれる限り、私は本を書き続けるでしょう。

私にインスピレーションと知識を与えてくれた Rubyist の方々にも感謝したいと思います: David Heinemeier Hansson、Yehuda Katz、Carl Lerche、Jeremy Kemper、Xavier Noria、Ryan Bat、Geoffrey Grosenbach、Peter Cooper、Matt Aimonetti、Mark Bates、Gregg Pollack、Wayne E. Seguin、Amy Hoy, Dave Chelimsky、Pat Maddox、Tom Preston-Werner、Chris Wanstrath、Chad Fowler、Josh Susser、Obie Fernandez、Ian McFarland、Steven Bristol、Pratik Naik、Sarah Mei、Sarah Allen、Wolfram Arnold、Alex Chaffee、Giles Bowkett、Evan Dorn、Long Nguyen、James Lindenbaum、Adam Wiggins、Tikhon Bernstam、Ron Evans、Wyatt Greene、Miles Forrest、Pivotal Labs の方々、Heroku の方々、thoughtbot の方々、そして GitHub の方々、ありがとうございました。最後に、ここに書ききれないほど多くの読者からバグ報告や提案を頂きました。ご協力いただいた皆様のおかげで、本書の完成度をとことんまで高めることができました。

著者

マイケルハートル (Michael Hartl) は「Ruby on Rails チュートリアル」という Web 開発を始めるときに最もよく参考にされる本の著者です。また、Softcover という自費出版プラットフォームの共同創業者でもあります。以前は、(今ではすっかり古くなってしまいましたが)「RailsSpace」という本の執筆および開発に携わったり、また、 一時人気を博した Ruby on Rails ベースのソーシャルネットワーキングプラットフォーム「Insoshi」の開発にも携わっていました。なお、2011年には、Rails コミュニティへの高い貢献が認められて、Ruby Hero Award を受賞しました。ハーバード大学卒業後、カリフォルニア工科大学物理学博士号を取得し、起業プログラム Y Combinator の卒業生でもあります。

  1. 3日間で読破するのは異常です! 実際にはもっと時間をかけて読むのが一般的です。

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

この章では、他のユーザーをフォロー (およびフォロー解除) できるソーシャルレイヤーを追加し、各ユーザーのHomeページに、現在フォロー中のユーザーのステータスフィードを表示できるようにして、サンプルアプリケーションのコアを完成させます。まずは、ユーザー間の関係性をどうモデリングするかについて学びます (12.1)。その後、 モデリング結果に対応するWebインターフェースを実装していきます (12.2)。このとき、Webインターフェースの例としてAjaxについても紹介します。最後に、ステータスフィードの完成版を実装します (12.3)。

この最終章では、本書の中で最も難易度の高い手法をいくつか使用しています。その中には、ステータスフィード作成のためにRuby/SQLを「だます」テクニックも含まれます。この章の例全体にわたって、これまでよりも複雑なデータモデルを使用しています。ここで学んだデータモデルは、今後自分用のWebアプリケーションを開発するときに必ず役に立ちます。また、本書を卒業して実際の開発に携わるときのために、12.4で役立つリソース集 (読み物ガイド) についても紹介します。

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

images/figures/page_flow_profile_mockup_3rd_edition
図12.1: 現在のプロフィールページ
images/figures/page_flow_user_index_mockup_bootstrap
図12.2 フォローする相手を見つける
images/figures/page_flow_other_profile_follow_button_mockup_3rd_edition
図12.3 ユーザーのプロフィール画面に [Follow] ボタンが表示されている
images/figures/page_flow_other_profile_unfollow_button_mockup_3rd_edition
図12.4 プロフィールに [Unfollow] ボタンが表示され、フォロワーのカウントが1つ増えた
images/figures/page_flow_home_page_feed_mockup_3rd_edition
図12.5 Homeページにステータスフィードが表示され、フォローのカウントが1増えた

12.1 Relationshipモデル

ユーザーをフォローする機能を実装する第一歩は、データモデルを構成することです。ただし、これは見た目ほど単純ではありません。素朴に考えれば、has_many (1対多) リレーションシップはこんな感じでできそうな気がします。1人のユーザーが複数のユーザーをhas_manyとしてフォローし 、1人のユーザーに複数のフォロワーがいることをhas_manyで表す、という具合です。この後で説明しますが、この方法ではたちまち壁に突き当たってしまいます。これを解決するためのhas_many through (多対多の関係を表すのに使用) についてもこの後で説明します。

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

$ git checkout master
$ git checkout -b following-users

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

ユーザーをフォローするデータモデル構成のための第一歩として、典型的な場合を検討してみましょう。あるユーザーが、別のユーザーをフォローしているところを考えてみましょう。具体例を挙げると、CalvinはHobbesをフォローしています。これを逆から見れば、HobbesはCalvinからフォローされています。CalvinはHobbesから見ればフォロワー (follower)であり、HobbesはCalvinによってフォローされている (followed) ことになります。Railsにおけるデフォルトの複数形の慣習に従えば、あるユーザーをフォローしているすべてのユーザーの集合はfollowersとなり、user.followersはそれらのユーザーの配列を表すことになります。残念なことに、この名前付けは逆についてはうまくいきません (Railsのというより英語の都合ですが)。フォローされているすべてのユーザーの集合は、このままではfollowedsとなってしまい、英語の文法からも外れるうえに非常に見苦しいものになってしまいます。そこで、今Twitterの慣習にならい、本チュートリアルではfollowingという呼称を採用します (例: “50 following, 75 followers”)。したがって、あるユーザーがフォローしているすべてのユーザーの集合はcalvin.followingとなります。

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

images/figures/naive_user_has_many_following
図12.6 フォローしているユーザーの素朴な実装例

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

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

このデータモデルには他にも解決しなくてはいけない問題があります。Facebookのような友好関係 (Friendships) では本質的に左右対称のデータモデルが成り立ちますが、Twitterのようなフォロー関係では左右非対称の性質があります。すなわち、CalvinはHobbesをフォローしていても、HobbesはCalvinをフォローしていないといった関係性が成り立つのです。このような左右非対称な関係性を見分けるために、それぞれを能動的関係 (Active Relationship)受動的関係 (Passive Relationship)と呼ぶことにします。たとえば先ほどの事例のような、CalvinがHobbesをフォローしているが、HobbesはCalvinをフォローしていない場合では、CalvinはHobbesに対して「能動的関係」を持っていることになります。逆に、HobbesはCalvinに対して「受動的関係」を持っていることになります3

まずは、フォローしているユーザーを生成するために、能動的関係に焦点を当てていきます (受動的関係については12.1.5で考えていきます)。先ほどの12.6は実装のヒントになりました。フォローしているユーザーはfollowed_idがあれば識別することができるので、先ほどのfollowingテーブルをactive_relationshipsテーブルと見立ててみましょう。ただしユーザー情報は無駄なので、ユーザーid以外の情報は削除します。そして、followed_idをとおして、usersテーブルのフォローされているユーザーを見つけるようにします。このデータモデルの模式図にすると、12.7のようになります。

images/figures/user_has_many_following_3rd_edition
図12.7 能動的関係をとおしてフォローしているユーザーを取得する模式図

能動的関係も受動的関係も、最終的にはデータベースの同じテーブルを使うことになります。したがって、テーブル名にはこの「関係」を表す「relationship」を使いましょう。モデル名も同様にして、Relationshipモデルとします。作成したRelationshipデータモデルを12.8に示します。1つのrelationshipテーブルを使って2つのモデル (能動的関係と受動的関係) をシミュレートする方法については、12.1.4で説明します。

images/figures/relationship_model
図12.8 Relationshipデータモデル

このデータモデルを実装するために、まずは次のように12.8に対応したマイグレーションを生成します。

$ rails generate model Relationship follower_id:integer followed_id:integer

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

リスト12.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 null: false
    end
    add_index :relationships, :follower_id
    add_index :relationships, :followed_id
    add_index :relationships, [:follower_id, :followed_id], unique: true
  end
end

リスト12.1では複合キーインデックスという行もあることに注目してください。これは、follower_idfollowed_idの組み合わせが必ずユニークであることを保証する仕組みです。これにより、あるユーザーが同じユーザーを2回以上フォローすることを防ぎます。(リスト6.28でメールアドレスの一意性を保証したり、リスト11.1で使った複合キーインデックスと比較してみてください。) もちろん、このような重複 (2回以上フォローすること) が起きないよう、インターフェイス側の実装でも注意を払います(12.1.4)。しかし、ユーザーが何らかの方法で (たとえばcurlなどのコマンドラインツールを使用して) Relationshipのデータを操作するようなことも起こり得ます。そのような場合でも、一意なインデックスを追加していれば、エラーを発生させて重複を防ぐことができます。

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

$ bundle exec rake db:migrate

12.1.2 User/Relationshipの関連付け

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

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

user.active_relationships.build(followed_id: ...)

この時点で、アプリケーションコードは11.1.3のようになるのではないかと予測した方もいるかもしれません。実際似ているのですが、2つの大きな違いがあります。

まずは1つ目の違いについてです。以前、ユーザーとマイクロポストの関連付けをしたときは、次のように書きました。

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

引数の:micropostsシンボルから、Railsはこれに対応するMicropostモデルを探し出し、見つけることができました4。しかし今回のケースで同じように書くと、

has_many :active_relationships

となってしまい、(ActiveRelationshipモデルを探してしまい) Relationshipモデルを見つけることができません。このため、今回のケースでは、Railsに探して欲しいモデルのクラス名を明示的に伝える必要があります。

2つ目の違いは、先ほどの逆のケースについてです。以前はMicropostモデルで

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

このように書きました。micropostsテーブルにはuser_id属性があるので、これを辿って対応する所有者 (ユーザー) を特定することができました (11.1.1)。データベースの2つのテーブルを繋ぐとき、このようなidは外部キー (foreign key)と呼びます。すなわち、Userモデルに繋げる外部キーが、Micropostモデルのuser_id属性ということです。この外部キーの名前を使って、Railsは関連付けの推測をしています。具体的には、Railsはデフォルトでは外部キーの名前を<class>_idといったパターンとして理解し、 <class>に当たる部分からクラス名 (正確には小文字に変換されたクラス名) を推測します5。ただし、先ほどはユーザーを例として扱いましたが、今回のケースではフォローしているユーザーをfollower_idという外部キーを使って特定しなくてはなりません。また、followerというクラス名は存在しないので、ここでもRailsに正しいクラス名を伝える必要が発生します。

先ほどの説明をコードにまとめると、UserとRelationshipの関連付けはリスト12.2リスト12.3のようになります。

リスト12.2: 能動的関係に対して1対多 (has_many) の関連付けを実装する app/models/user.rb
class User < ActiveRecord::Base
  has_many :microposts, dependent: :destroy
  has_many :active_relationships, class_name:  "Relationship",
                                  foreign_key: "follower_id",
                                  dependent:   :destroy
  .
  .
  .
end

(ユーザーを削除したら、ユーザーのリレーションシップも同時に削除される必要があります。そのため、関連付けにdependent: :destroyも追加しています。)

リスト12.3: リレーションシップ/フォロワーに対してbelongs_toの関連付けを追加する app/models/relationship.rb
class Relationship < ActiveRecord::Base
  belongs_to :follower, class_name: "User"
  belongs_to :followed, class_name: "User"
end

なお、followerの関連付けについては、12.1.5に入るまでは使いません。しかしfollowerとfollowedを対称的に実装しておくことで、構造に対する理解は容易になるはずです。

リスト12.2リスト12.3で定義した関連付けにより、11.1で以前紹介したような多くのメソッドが使えるようになりました。今回使えるようになったメソッドを12.1に示します。

メソッド 用途
active_relationship.follower フォロワーを返します
active_relationship.followed フォローしているユーザーを返します
user.active_relationships.create(followed_id: user.id) userを紐付けて能動的関係を作成/登録する
user.active_relationships.create!(followed_id: user.id) userを紐付けて能動的関係を作成/登録する (失敗時にエラーを出力)
user.active_relationships.build(followed_id: user.id) userと紐付けた新しいRelationshipオブジェクトを返す
表12.1 ユーザーと能動的関係の関連付けによって使えるようになったメソッドのまとめ (

12.1.3 Relationshipのバリデーション

先に進む前に、Relationshipモデルの検証を追加して完全なものにしておきましょう。テスト (リスト12.4)とアプリケーションコード (リスト12.5) は素直な作りです。ただし、ユーザー用のfixtureファイル (リスト6.29) と同じように、生成されたリレーションシップ用のfixtureでは、マイグレーション (リスト12.1) で制約させた一意性を満たすことができません。ということで、ユーザーのときと同じで (リスト6.30でfixtureの内容を削除したように)、今の時点では生成されたリレーションシップ用のfixtureファイルも空にしておきましょう (リスト12.6)。

リスト12.4: Relationshipモデルのバリデーションをテストする test/models/relationship_test.rb
require 'test_helper'

class RelationshipTest < ActiveSupport::TestCase

  def setup
    @relationship = Relationship.new(follower_id: 1, followed_id: 2)
  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
リスト12.5: 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
リスト12.6: Relationship用のfixtureを空にする test/fixtures/relationships.yml
# empty

これにより、テストはGREENになるはずです。

リスト12.7: GREEN
$ bundle exec rake test

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

いよいよRelationshipの関連付けの核心、followingfollowersに取りかかります。今回はhas_many throughを使用します。11.7のように、1人のユーザーにはいくつもの「フォローする/される (多対多)」のリレーションシップがあります。デフォルトのhas_many throughという関連付けでは、Railsはモデル名(単数形)に対応する外部キーを探します。つまり、次のコードでは、

has_many :followeds, through: :active_relationships

Railsは“followeds”というシンボル名を見て、これを“followed”という単数形に変え、 relationshipsテーブルのfollowed_idを使って対象のユーザーを取得してきます。しかし、12.1.1で指摘したように、user.followedsという名前は英語として不適切です。代わりに、user.followingという名前を使いましょう。そのためには、Railsのデフォルトを上書きする必要があります。ここでは:sourceパラメーター (リスト12.10) を使用し、「following配列の元はfollowed idの集合である」ということを明示的にRailsに伝えます。

リスト12.8: Userモデルにfollowingの関連付けを追加する app/models/user.rb
class User < ActiveRecord::Base
  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

リスト12.8で定義した関連付けにより、フォローしているユーザーを配列の様に扱えるようになりました。たとえば、include?メソッド (4.3.1) を使ってフォローしているユーザーの集合を調べてみたり、関連付けを通してオブジェクトを探しだせるようになります。

user.following.include?(other_user)
user.following.find(other_user)

followingメソッドで配列のように扱えるだけでも便利ですが、Railsは単純な配列ではなく、もっと賢くこの集合を扱っています。たとえば、次のようなコードでは

following.include?(other_user)

フォローしている全てのユーザーをデータベースから取得し、その集合に対してinclude?メソッドを実行しているように見えますが、しかし実際にはデータベースの中で直接比較をするように配慮しています。(11.2.1でも説明したように、次のようなコードは

user.microposts.count

データベースの中で合計を計算したほうが高速になることを思い出してください。)

次に、followingで取得した集合をより簡単に取り扱うために、followunfollowといった便利メソッドを追加しましょう。これらのメソッドは、たとえばuser.follow(other_user)といった具合に使います。さらに、これに関連するfollowing?論理値メソッドも追加し、あるユーザーが誰かをフォローしているかどうかを確認できるようにします6

今回は、こういったメソッドはテストから先に書いていきます。というのも、Webインターフェイスなどで便利メソッドを使うのはまだ先なので、すぐに使える場面がなく、実装した手応えを得にくいからです。一方で、Userモデルに対するテストを書くのは簡単かつ今すぐできます。そのテストの中で、これらのメソッドを使っていきます。具体的には、following?メソッドであるユーザーをまだフォローしていないことを確認、followメソッドを使ってそのユーザーをフォロー、 following?メソッドを使ってフォロー中になったことを確認、 最後にunfollowメソッドでフォロー解除できたことを確認、といった具合でテストをしてきます。このテストをコードにすると、リスト12.9のようになります。

リスト12.9: “following” 関連のメソッドをテストする RED test/models/user_test.rb
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

12.1のメソッドを参考にしながら、followingによる関連付けを使ってfollowunfollowfollowing?メソッドを実装していきましょう (リスト12.10)。このとき、可能な限りself (user自身を表すオブジェクト) を省略している点に注目してください。

リスト12.10: "following" 関連のメソッド GREEN app/models/user.rb
class User < ActiveRecord::Base
  .
  .
  .
  def feed
    .
    .
    .
  end

  # ユーザーをフォローする
  def follow(other_user)
    active_relationships.create(followed_id: other_user.id)
  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

  private
  .
  .
  .
end

リスト12.10のコードを追加することで、テストスイートはGREENになるはずです。

リスト12.11: GREEN
$ bundle exec rake test

12.1.5 フォロワー

リレーションシップというパズルの最後の一片は、user.followersメソッドを追加することです。これは上のuser.followingメソッドと対になります。12.7を見ていて気付いた方もいると思いますが、フォロワーの配列を展開するために必要な情報は、relationshipsテーブルに既にあります (リスト12.2コードをとおしてactive_relationshipsテーブルを扱っています)。実は、follower_idfollowed_idを入れ替えるだけで、フォロワーについてもユーザーのフォローのときとまったく同じ方法が使用できます。これはpassive_relationshipsactive_relationshipsについても同じです。したがって、データモデルは12.9のようになります。

images/figures/user_has_many_followers_3rd_edition
図12.9 Relationshipモデルのカラムを入れ替えて作った、フォロワーのモデル

12.9を参考にしたデータモデルの実装をリスト12.12に示しますが、この実装はリスト12.8とまさに類似しています。

リスト12.12: 受動的関係を使ってuser.followersを実装する app/models/user.rb
class User < ActiveRecord::Base
  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

一点、リスト12.12で注意すべき箇所は、次のように参照先 (followers) を指定するための:sourceキーを省略してもよかったという点です。

has_many :followers, through: :passive_relationships

これは:followers属性の場合、Railsが “followers” を単数形にして自動的に外部キーfollower_idを探してくれるからです。リスト12.8と違って必要のない:sourceキーをそのまま残しているのは、has_many :followingとの類似性を強調させるためです。

次に、followers.include?メソッドを使って先ほどのデータモデルをテストしていきます。テストコードはリスト12.13のとおりです。(リスト12.13では、following?と対照的なfollowed_by?メソッドを使ってもよかったのですが、サンプルアプリケーションで実際に使う場面がなかったので省略しました。)

リスト12.13: followersに対するテスト GREEN test/models/user_test.rb
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

リスト12.13ではリスト12.9に1行だけ追加していますが、実際には多くの処理が正しく動いていなければパスしません。つまり、リスト12.12の実装に対するテストは、実装の影響を受けやすいテストだといえます。

この時点で、全てのテストがGREENになるはずです。

$ bundle exec rake test

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

12.1ではやや複雑なデータモデリングの技術を説明しました。理解するのに時間がかかってしまっても大丈夫なので、安心してください。実際、使用されたさまざまな関連付けを理解するのに一番良いのは、Webインターフェイスで使用してみることです。

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

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

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

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

リスト12.14: サンプルデータにfollowing/followerの関係性を追加する db/seeds.rb
# ユーザー
User.create!(name:  "Example User",
             email: "example@railstutorial.org",
             password:              "foobar",
             password_confirmation: "foobar",
             admin:     true,
             activated: true,
             activated_at: Time.zone.now)

99.times do |n|
  name  = Faker::Name.name
  email = "example-#{n+1}@railstutorial.org"
  password = "password"
  User.create!(name: name,
              email: email,
              password:              password,
              password_confirmation: password,
              activated: true,
              activated_at: Time.zone.now)
end

# マイクロポスト
users = User.order(:created_at).take(6)
50.times do
  content = Faker::Lorem.sentence(5)
  users.each { |user| user.microposts.create!(content: content) }
end

# リレーションシップ
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) }

リスト12.14を実行してデータベース上のサンプルデータを作り直すために、いつものコマンドを実行しましょう。

$ bundle exec rake db:migrate:reset
$ bundle exec rake db:seed

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

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

12.1.1で指摘したように、Twitterの慣習にしたがってフォロー数の単位には“following”を使い、たとえば“50 following”といった具合に表示します。この単位は12.1のモックアップの一部でも既に使われていました。該当箇所を拡大して12.10に再掲します。

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

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

リスト12.15: Usersコントローラにfollowingアクションとfollowersアクションを追加する config/routes.rb
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]
end

この場合のURLは/users/1/followingや/users/1/followersのようになるのではないかと推測した方もいると思います。そして、リスト12.15のコードはまさにそれを行なっているのです。また、どちらもデータを表示するページなので、適切なHTTPメソッドはGETリクエストになります。したがって、getメソッドを使って適切なレスポンスを返すようにします。ちなみに、memberメソッドを使うとユーザーidが含まれているURLを扱うようになりますが、idを指定せずにすべてのメンバーを表示するには、以下のようにcollectionメソッドを使用します。

resources :users do
  collection do
    get :tigers
  end
end

このコードは/users/tigersというURLに応答します (アプリケーションにあるすべてのtigerのリストを表示します)7

リスト12.15によって生成されるルーティングテーブルを12.2に示します。ここにある、フォローしているユーザー用とフォロワー用の名前付きルートをこの後使用します。

HTTPリクエスト URL アクション 名前付きルート
GET /users/1/following following following_user_path(1)
GET /users/1/followers followers followers_user_path(1)
表12.2 カスタムルールで提供するリスト12.15のRESTfulルート

ルーティングを定義したので、統計情報のパーシャルを実装する準備が整いました。このパーシャルでは、divタグの中に2つのリンクを含めるようにします (リスト12.16)。

リスト12.16: フォロワーの統計情報を表示するパーシャル 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ページの両方に表示されるので、リスト12.16の最初の行では、以下のコードを使用して適切な方を選択しています。

<% @user ||= current_user %>

コラム8.1でも説明したとおり、@usernilでない場合 (つまりプロファイルページの場合) は何もせず、nilの場合 (つまりHomeページの場合) には@userにカレントユーザーを設定します。フォローしているユーザーの人数と、フォロワーの人数は、以下の関連付けを使用して計算されます。

@user.following.count

さらに、以下のコードは

@user.followers.count

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

@user.microposts.count

このコードを使用してマイクロポストの合計数を表示します。(以前同様、高速化のためにRailsはデータベースの中で合計を計算するようにしています。)

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

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

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

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

リスト12.17: Homeページにフォロワーの統計情報を追加する app/views/static_pages/home.html.erb
<% 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 %>

統計情報にスタイルを与えるために、リスト12.18のようにSCSSを追加しましょう (なお、このSCSSにはこの章で使用するスタイルがすべて含まれています)。変更の結果、Homeページは12.11のようになります。

リスト12.18: Homeページのサイドバー用のSCSS app/assets/stylesheets/custom.css.scss
.
.
.
/* sidebar */
.
.
.
.gravatar {
  float: left;
  margin-right: 10px;
}

.gravatar_edit {
  margin-top: 15px;
}

.stats {
  overflow: auto;
  margin-top: 0;
  padding: 0;
  a {
    float: left;
    padding: 0 10px;
    border-left: 1px solid $gray-lighter;
    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;
  }
  a {
    padding: 0;
  }
}

.users.follow {
  padding: 0;
}

/* forms */
.
.
.
images/figures/home_page_follow_stats_3rd_edition
図12.11 Homeページにフォロー関連の統計情報を表示する

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

リスト12.19: 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 %>

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

リスト12.20: Relationshipリソース用のルーティングを追加する config/routes.rb
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

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

リスト12.21: ユーザーをフォローするためのフォーム app/views/users/_follow.html.erb
<%= form_for(current_user.active_relationships.build) do |f| %>
  <div><%= hidden_field_tag :followed_id, @user.id %></div>
  <%= f.submit "Follow", class: "btn btn-primary" %>
<% end %>
リスト12.22: ユーザーのアンフォローするフォーム app/views/users/_unfollow.html.erb
<%= form_for(current_user.active_relationships.find_by(followed_id: @user.id),
             html: { method: :delete }) do |f| %>
  <%= f.submit "Unfollow", class: "btn" %>
<% end %>

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

<input id="followed_id" name="followed_id" type="hidden" value="3" />

10.2.4リスト10.50で見たように、隠しフィールドのinputタグを使うことで、ブラウザ上に表示させずに適切な情報を含めることができます。

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

リスト12.23: プロフィールページにフォロー用フォームとフォロワーの統計情報を追加する app/views/users/show.html.erb
<% provide(:title, @user.name) %>
<div class="row">
  <aside class="col-md-4">
    <section>
      <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>
images/figures/profile_follow_button_3rd_edition
図12.12: プロフィール画面 (/users/2) に [Follow] ボタンが表示されている
images/figures/profile_unfollow_button_3rd_edition
図12.13 プロフィール画面 (/users/5) に [Unfollow] ボタンが表示されている

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

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

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

images/figures/following_mockup_bootstrap
図12.14 フォローしているユーザー用ページのモックアップ
images/figures/followers_mockup_bootstrap
図12.15 ユーザーのフォロワー用ページのモックアップ

ここでの最初の作業は、フォローしているユーザーのリンクとフォロワーのリンクを動くようにすることです。Twitterにならい、どちらのページでもユーザーのログインを要求します。前回のアクセス制御と同様に、まずはテストから書いていきます。今回使うテストはリスト12.24のとおりです。

リスト12.24: フォロー/フォロワーページの認可をテストする RED test/controllers/users_controller_test.rb
require 'test_helper'

class UsersControllerTest < ActionController::TestCase

  def setup
    @user = users(:michael)
    @other_user = users(:archer)
  end
  .
  .
  .
  test "should redirect following when not logged in" do
    get :following, id: @user
    assert_redirected_to login_url
  end

  test "should redirect followers when not logged in" do
    get :followers, id: @user
    assert_redirected_to login_url
  end
end

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

リスト12.25: followingアクションとfollowersアクション 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

  private
  .
  .
  .
end

本チュートリアルのいたるところで見てきたように、Railsは慣習に従って、アクションに対応するビューを暗黙的に呼び出します。たとえば、showアクションの最後でshow.html.erbを呼び出すといった具合です。一方で、リスト12.25のいずれのアクションも、render明示的に呼び出し、show_followという同じビューを出力しています。したがって、作成が必要なビューはこれ1つです。renderで呼び出しているビューが同じである理由は、このERbはどちらの場合でもほぼ同じであり、リスト12.26で両方の場合をカバーできるためです。

リスト12.26: フォローしているユーザーとフォロワーの両方を表示するshow_followビュー 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>

リスト12.25にあるアクションは、2通りの方法でリスト12.26のビューを呼び出します。“following”をとおって描画したビューを12.16に、“followers”をとおって描画したビューを12.17に示します。このとき、上のコードではカレントユーザーを一切使っていない点に注目してください。したがって、他のユーザーのフォロワー一覧ページもうまく動きます (12.18)。

images/figures/user_following_3rd_edition
図12.16 現在のユーザーにフォローされているユーザーを表示する
images/figures/user_followers_3rd_edition
図12.17 ユーザーのフォロワーを表示する
images/figures/diferent_user_followers_3rd_edition
図12.18 別のユーザーのフォロワーを表示する

フォロー一覧もフォロワー一覧も動くようになったので、この振る舞いを検証するための2つの統合テストを書いていきましょう。これらの統合テストを基本的なテストに留め、網羅的なテストではありません。5.3.4でも指摘したように、HTML構造を網羅的にチェックするテストは壊れやすく、生産性を逆に落としかねないからです。したがって今回は、正しい数が表示されているかどうかと、正しいURLが表示されているかどうかの2つのテストを書きます。

いつものように、統合テストを生成するところから始めます。

$ rails generate integration_test following
      invoke  test_unit
      create    test/integration/following_test.rb

次に、テストデータをいくつか揃えます。リレーションシップ用のfixtureにデータを追加しましょう。11.2.3では、次のように書くことで

orange:
  content: "I just ate an orange!"
  created_at: <%= 10.minutes.ago %>
  user: michael

ユーザーとマイクロポストを関連付けできたことを思い出してください。また、上のコードではユーザー名を書いていましたが、

user: michael

次のようにユーザーidでも関連付けできます。

user_id: 1

この例を参考にしてリレーションシップ用のfixtureにテストデータを追加すると、リスト12.27のようになります。

リスト12.27: following/followerをテストするためのリレーションシップ用fixture test/fixtures/relationships.yml
one:
  follower: michael
  followed: lana

two:
  follower: michael
  followed: mallory

three:
  follower: lana
  followed: michael

four:
  follower: archer
  followed: michael

リスト12.27のfixtureでは、前半の2つでMichaelがLanaとMalloryをフォローし、後半の2つでLanaとArcherがMichaelをフォローしています。あとは、正しい数かどうかを確認するために、assert_matchメソッド (リスト11.27) を使ってプロフィール画面のマイクロポスト数をテストします。さらに、正しいURLかどうかをテストするコードも加えると、リスト12.28のようになります。

リスト12.28: following/followerページのテスト GREEN test/integration/following_test.rb
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

なお、リスト12.28では、次のコードを加えていますが

assert_not @user.following.empty?

このコードは次のコードを確かめるためのテストなので、

@user.following.each do |user|
  assert_select "a[href=?]", user_path(user)
end

無意味なテストではないことに注意してください (followersについても同様です)。

これで、テストがGREENになるはずです。

リスト12.29: GREEN
$ bundle exec rake test

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

ビューが整ってきました。いよいよ [フォローする] [フォロー解除する] ボタンを動作させましょう。フォローとフォロー解除はそれぞれリレーションシップの作成と削除に対応しているため、まずはRelationshipsコントローラが必要です。いつものようにコントローラを生成しましょう。

$ rails generate controller Relationships

リスト12.31でも説明しますが、Relationshipsコントローラのアクションでアクセス制御することはそこまで難しくありません。しかし、前回のアクセス制御のときと同様に最初にテストを書き、それをパスするように実装することでセキュリティモデルを確立させていきましょう。今回はまず、コントローラのアクションにアクセスするとき、ログイン済みのユーザーであるかどうかをチェックします。 もしログインしていなければ、ログインページにリダイレクトさせ、Relationshipのカウントが変わっていないことを確認します (リスト12.30)。

リスト12.30: リレーションシップの基本的なアクセス制御に対するテスト RED test/controllers/relationships_controller_test.rb
require 'test_helper'

class RelationshipsControllerTest < ActionController::TestCase

  test "create should require logged-in user" do
    assert_no_difference 'Relationship.count' do
      post :create
    end
    assert_redirected_to login_url
  end

  test "destroy should require logged-in user" do
    assert_no_difference 'Relationship.count' do
      delete :destroy, id: relationships(:one)
    end
    assert_redirected_to login_url
  end
end

次に、リスト12.30のテストをパスさせるために、logged_in_userフィルターをRelationshipsコントローラのアクションに対して追加します (リスト12.31)。

リスト12.31: リレーションシップのアクセス制御 GREEN app/controllers/relationships_controller.rb
class RelationshipsController < ApplicationController
  before_action :logged_in_user

  def create
  end

  def destroy
  end
end

フォロー/フォロー解除ボタンを動かすためには、フォーム (リスト12.21/リスト12.22) から送信されたパラメータを使って、followed_idに対応するユーザーを見つけてくる必要があります 。その後、見つけてきたユーザーに対して適切にfollow/unfollowメソッド (リスト12.10) を使います。このすべてを実装した結果を、リスト12.32に示します。

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

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

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

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

これで、フォロー/フォロー解除の機能が完成しました。どのユーザーも、他のユーザーをフォローしたり、フォロー解除したりできます。ブラウザ上でボタンをクリックして、確かめてみてください。(振る舞いを検証する統合テストは12.2.6で実装します。) 2番目のユーザーをフォローする前の状態を12.19に、フォローした結果を12.20にそれぞれ示します。

images/figures/unfollowed_user
図12.19: フォローしていないユーザーの画面
images/figures/followed_user
図12.20: ユーザーをフォローした結果

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

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

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

form_for

上を以下のように置き換えます。

form_for ..., remote: true

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

リスト12.33: Ajaxを使ったフォローフォーム app/views/users/_follow.html.erb
<%= form_for(current_user.active_relationships.build, remote: true) do |f| %>
  <div><%= hidden_field_tag :followed_id, @user.id %></div>
  <%= f.submit "Follow", class: "btn btn-primary" %>
<% end %>
リスト12.34: Ajaxを使ったフォロー解除フォーム app/views/users/_unfollow.html.erb
<%= form_for(current_user.active_relationships.find_by(followed_id: @user.id),
             html: { method: :delete },
             remote: true) do |f| %>
  <%= f.submit "Unfollow", class: "btn" %>
<% 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リクエストに応答できるようにしましょう。こういったリクエストの種類によって応答を場合分けするときは、respond_toというメソッドを使います。一般的な使い方は、次のような具合です。

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

この文法は少々変わっていて混乱を招く可能性がありますが、上の (ブロック内の) コードのうち、いずれかの1行が実行されるという点が重要です (このためrespond_toメソッドは、上から順に実行する逐次処理というより、if文を使った分岐処理に近いイメージです)。RelationshipsコントローラでAjaxに対応させるために、respond_toメソッドをcreateアクションとdestroyアクション (リスト12.32) にそれぞれ追加してみましょう。変更の結果をリスト12.35に示します。このとき、ユーザーのローカル変数 (user) をインスタンス変数 (@user) に変更した点に注目してください。これは、リスト12.32のときはインスタンス変数は必要なかったのですが、リスト12.33リスト12.34を実装したことにより、インスタンス変数が必要になったためです。

リスト12.35: RelationshipsコントローラでAjaxリクエストに対応する 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)
    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

リスト12.35でAjaxリクエストに対応したので、今度はブラウザ側でJavaScriptが無効になっていた場合 (Ajaxリクエストが送れない場合) でもうまく動くようにします (リスト12.36)。

リスト12.36: JavaScriptが無効になっていたときのための設定 config/application.rb
require File.expand_path('../boot', __FILE__)
.
.
.
module SampleApp
  class Application < Rails::Application
    .
    .
    .
    # 認証トークンをremoteフォームに埋め込む
    config.action_view.embed_authenticity_token_in_remote_forms = true
  end
end

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

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

$("#follow_form")

リスト12.19では、これはフォームを囲む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を使用しています (もちろんこれは、フォローに成功した場合の動作です)。変更の結果をリスト12.37に示します。このコードではescape_javascriptメソッドを使用していることに注目してください。この関数は、JavaScriptファイル内にHTMLを挿入するときに実行結果をエスケープする (画面に表示しない) ために必要です。

リスト12.37: JavaScriptと埋め込みRubyを使ってフォローの関係性を作成する app/views/relationships/create.js.erb
$("#follow_form").html("<%= escape_javascript(render('users/unfollow')) %>");
$("#followers").html('<%= @user.followers.count %>');

各行の末尾にセミコロン ; があることに注目してください。 これはプログラミング言語によくある文法で、古くは1950年代中ごろに開発されたALGOLまで遡ります。

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

リスト12.38: Ruby JavaScript (RJS) を使ってフォローの関係性を削除する app/views/relationships/destroy.js.erb
$("#follow_form").html("<%= escape_javascript(render('users/follow')) %>");
$("#followers").html('<%= @user.followers.count %>');

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

12.2.6 フォローをテストする

フォローボタンが動くようになったので、バグを検知するためのシンプルなテストを書いていきましょう。ユーザーのフォローに対するテストでは、 /relationshipsに対してPOSTリクエストを送り、フォローされたユーザーが1人増えたことをチェックします。具体的なコードは次のとおりです。

assert_difference '@user.following.count', 1 do
  post relationships_path, followed_id: @other.id
end

これは標準的なフォローに対するテストではありますが、Ajax版もやり方は大体同じです。Ajaxのテストでは、postの代わりにxhr :postを使うだけです。

assert_difference '@user.following.count', 1 do
  xhr :post, relationships_path, followed_id: @other.id
end

ここで使っているxhr (XmlHttpRequest) というメソッドは、Ajaxでリクエストを発行するします。したがって、リスト12.35respond_toでは、JavaScriptに対応した行が実行されるようになります。

また、ユーザーをアンフォローするときも構造はほとんど同じで、postメソッドをdeleteメソッドに置き換えてテストします。つまり、そのユーザーのidとリレーションシップのidを使ってDELETEリクエストを送信し、フォローしている数が1つ減ることを確認します。

assert_difference '@user.following.count', -1 do
  delete relationship_path(relationship),
         relationship: relationship.id
end

Ajaxの場合は次のとおりです。

assert_difference '@user.following.count', -1 do
  xhr :delete, relationship_path(relationship),
               relationship: relationship.id
end

これらのテストをまとめた結果を、リスト12.39に示します。

リスト12.39: Follow/Unfollowボタンをテストする GREEN test/integration/following_test.rb
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, followed_id: @other.id
    end
  end

  test "should follow a user with Ajax" do
    assert_difference '@user.following.count', 1 do
      xhr :post, relationships_path, 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
      xhr :delete, relationship_path(relationship)
    end
  end
end

この時点では、テストはGREENになるはずです。

リスト12.40: GREEN
$ bundle exec rake test

12.3 ステータスフィード

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

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

images/figures/page_flow_home_page_feed_mockup_bootstrap
図12.21 ステータスフィード付きのHomeページのモックアップ

12.3.1 動機と計画

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

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

どのようにフィードを実装するのかはまだ明確ではありませんが、テストについてはやや明確そうなので、(コラム3.3のガイドラインに従って) まずはテストから書いていきます。このテストで重要なことは、フィードに必要な3つの条件を満たすことです。1) フォローしているユーザーのマイクロポストがフィードに含まれていること。2) 自分自身のマイクロポストもフィードに含まれていること。3) フォローしていないユーザーのマイクロポストがフィードに含まれていないこと。そしてリスト9.43リスト11.51のfixtureファイルから、MichaelのフィードではLanaと自分自身の投稿が見えていて、Archerの投稿は見えないことがわかります。先ほどの3つの条件をアサーションに変換して、Userモデル (リスト11.44) feedメソッドがあることに注意しながら、更新したUserモデルに対するテストを書いてみましょう。結果をリスト12.41に示します。

リスト12.41: ステータスフィードのテスト RED test/models/user_test.rb
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になるはずです。

リスト12.42: RED
$ bundle exec rake test

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

ステータスフィードで要求される設計はリスト12.41のテストで明確になったので (このテストにパスすれば良いので)、早速フィードの実装に着手してみましょう。最終的なフィードの実装はやや込み入っているため、細かい部品を1つずつ確かめながら導入していきます。最初に、このフィードで必要なクエリについて考えましょう。ここで必要なのは、micropostsテーブルから、あるユーザー (つまり自分自身) がフォローしているユーザーに対応するidを持つマイクロポストをすべて選択 (select) することです。このクエリを模式的に書くと以下のようになります。

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

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

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

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

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

Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)

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

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

上に示したような状況では、各要素に対して同じメソッド が実行されます。これは非常によく使われる方法であり、次のようにアンパサンド &と、メソッドに対応するシンボルを使用した短縮表記 (4.3.2) も可能です。この短縮表記なら変数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.followingにある各要素のidを呼び出し、フォローしているユーザーのidの配列を構成することができます。たとえば、データベースの最初のユーザーの場合は、以下の配列になります。

>> User.first.following.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.following_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]

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

>> User.first.following_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文字列に挿入するときは、このように記述する必要はありません。実は、?を内挿すると自動的にこの辺りの面倒を見てくれます。さらに、データベースに依存する一部の非互換性まで解消してくれます。つまり、ここではfollowing_idsメソッドをそのまま使えばよいだけなのです。

結果、最初に想像していたとおり

Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)

が無事に動きました! 変更の結果をリスト12.43に示します。

リスト12.43: とりあえず動くフィードの実装 GREEN app/models/user.rb
class User < ActiveRecord::Base
  .
  .
  .
  # パスワードリセットが期限切れなら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)
    active_relationships.create(followed_id: other_user.id)
  end
  .
  .
  .
end

これでテストがGREENになるはずです。

リスト12.44: GREEN
$ bundle exec rake test

いくつかのアプリケーションにおいては、この初期実装だけで目的が達成され、十分に思えるかもしれません。しかしリスト12.43にはまだ足りないものがあります。それが何なのか、次の節に進む前に考えてみてください(ヒント:フォローしているユーザーが5000人もいたらどうなるでしょうか)。

12.3.3 サブセレクト

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

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

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

リスト12.45: whereメソッド内の変数に、キーと値のペアを使う GREEN app/models/user.rb
class User < ActiveRecord::Base
  .
  .
  .
  # ユーザーのステータスフィードを返す
  def feed
    Micropost.where("user_id IN (:following_ids) OR user_id = :user_id",
                    following_ids: following_ids, user_id: id)
  end
  .
  .
  .
end

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

Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)

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

Micropost.where("user_id IN (:following_ids) OR user_id = :user_id",
                following_ids: following_ids, user_id: id)

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

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

following_ids

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

following_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ではなく) データベースに保存するので、より効率が高まります。

これで基礎を固めることができましたので、リスト12.46のようにもっと効率なフィードを実装する準備ができました。ここに記述されているのは生のSQLなので、following_idsという文字列はエスケープされているのではなく、式展開されることに注意してください

リスト12.46: フィードの最終的な実装 GREEN app/models/user.rb
class User < ActiveRecord::Base
  .
  .
  .
  # ユーザーのステータスフィードを返す
  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とRubyとSQLが複雑に絡み合っていて厄介ですが、ちゃんと動作します。

リスト12.47: GREEN
$ bundle exec rake test

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

リスト12.46をもって、ステータスフィードの実装は完了です。11.3.3でHomeページには既にフィードを追加していたことを思い出してください。 思い出すキッカケとして、homeアクションはリスト12.48に再掲します。第11章ではただのプロトタイプでしたが (11.14)、リスト12.46の実装によって、Homeページで完全なフィードが表示できていることがわかります (12.23)。

リスト12.48: homeアクション内で、フィードにもページネーションを適用する app/controllers/static_pages_controller.rb
class StaticPagesController < ApplicationController

  def home
    if logged_in?
      @micropost  = current_user.microposts.build
      @feed_items = current_user.feed.paginate(page: params[:page])
    end
  end
  .
  .
  .
end
images/figures/home_page_with_feed_3rd_edition
図12.23: Homeページで動作するステータスフィード

この時点で、masterブランチに変更を取り込む準備ができました。

$ bundle exec rake test
$ git add -A
$ git commit -m "Add user following"
$ git checkout master
$ git merge following-users

コードをリポジトリにpushして、本番環境にデプロイしてみましょう。

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

本番環境で動作するステータスフィードは以下のようになります (12.24)。

images/figures/live_status_feed
図12.24: 本番環境で動作するステータスフィード

12.4 最後に

ステータスフィードが追加され、Ruby on Railsチュートリアルのサンプルアプリケーションがとうとう完成しました。このサンプルアプリケーションには、Railsの主要な機能 (モデル、ビュー、コントローラ、テンプレート、パーシャル、フィルタ、検証、コールバック、has_many/belongs_to/has_many through関連付け、セキュリティ、テスティング、デプロイ) が多数含まれています。

これだけでもかなりの量ですが、 Web開発ついて学ぶべきことはまだまだたくさんあります。今後の学習の手始めとするために、この節では、より踏み込んだ学習をするための方法を紹介します。

12.4.2 読み物ガイド

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

(訳注: 日本語の読み物ガイドとしては、@zyunnosukeさんの記事「Ruby on Railsを仕事にしていくための第一歩」がよくまとまっています。「Railsチュートリアルを読み終わった後はどうすれば良い?」とお悩みの方は、是非ご参考にしてみてください。)

  • Railsスクリーンキャスト: 本書に合わせて、完全版のスクリーンキャスト (現在は英語版のみ) を用意してあります。このスクリーンキャストでは、本書の話題をすべてカバーしているだけでなく、さまざまなコツや秘訣も満載されており、スクリーンショットだけでは捉えにくい実際の動作を動画で視聴することもできます。スクリーンキャスト (英語) は Ruby on RailsチュートリアルWebサイト(英語版)で購入できます。
  • RailsCasts: 最初にRailsCastsエピソードアーカイブを開いて、目についたトピックを適当に開くところから始めてみるとよいでしょう。
  • Tealeaf Academy: 開発者自身による新人向けトレーニング講座が最近増えてきました。身の回りのそういった人がいればよいのですが、そうでない場合はオンラインでどこからでも受講できるTealeaf Academyがあります。もし体系化されたカリキュラムやインストラクターによるフィードバックが欲しければ、Tealeafは良い選択肢となり得るでしょう。
  • Turing School of Software & Design: コロラド州デンバーで27週間 (約半年)、フルタイムでRuby/Rails/JavaScriptを学ぶプログラムです (訳注: プログラミング留学ってイメージですね)。ほとんどの生徒はプログラミング経験が乏しい時点からスタートしていますが、強い意志と高いモチベーションをもっています (上達にはこれらが必要です)。Turingスクールでは、生徒が卒業後に職を見つけることを保証していて、見つからなければ授業料を返還しています。Railsチュートリアルの読者は$500の割引クーポンが使えます。興味ある方はRAILSTUTORIAL500というクーポンコードを使ってください。
  • Bloc: 体系化されたカリキュラムを使った、オンライン上のブートキャンプで、参加者に応じて適切なメンターを配置したり、プロジェクトを通した学習に特化している点が特長です。RAILSTUTORIAL500というクーポンコードを使うと、入会費を500ドル割引できます。
  • Thinkful: プロのエンジニアとペアを組んで、プロジェクト実践型のカリキュラムで進んでいくオンライン講座です。対応している科目はRuby on Rails、フロントエンド開発、Webデザイン、データサイエンスです。
  • Pragmatic Studio: Mike ClarkとNicole Clarkが教鞭を執っているオンラインのRailsクラスです。
  • RailsApps: 教育目的の、Railsアプリケーションのサンプル集です。
  • Code School: 非常に多種多様なプログラミングを対話的に学習できるコース
  • Bala Paranj’s Test Driven Development in Ruby: Rubyを使ってテスト駆動開発を学ぶ、上級者向けのオンライン講座です。
  • RubyやRailsのお勧め書籍: 「Beginning Ruby」(Peter Cooper 著)、「The Well-Grounded Rubyist」(David A. Black著)、「Eloquent Ruby」(Russ Olsen著)、Rubyをさらに深く学ぶのであれば 「The Ruby Way」(Hal Fulton著) がお勧めです。Railsをさらに深く学ぶのであれば「Agile Web Development with Rails」(Sam Ruby / Dave Thomas / David Heinemeier Hansson著)、「The Rails 4 Way」(Obie Fernandez / Kevin Faustino著)、「Rails 4 in Action」 (Ryan Bigg / Yehuda Katz著)がお勧めです。
  • Railsガイド: トピック毎に分類された最新のRailsリファレンスです (訳注: RailsGuidesの日本語版を「Railsガイド」と呼んでいます)。

12.4.2 本章のまとめ

  • has_many :throughを使うと、複雑なデータ関係をモデリングできる
  • has_manyメソッドには、クラス名や外部キーなど、いくつものオプションを渡すことができる
  • 適切なクラス名と外部キーと一緒にhas_many/has_many :throughを使うことで、能動的関係 (フォローする) や受動的関係 (フォローされる) がモデリングできた
  • ルーティングは、ネストさせて使うことができる
  • whereメソッドを使うと、柔軟で強力なデータベースへの問い合わせが作成できる
  • Railsは (必要に応じて) 低級なSQLクエリを呼び出すことができる
  • 本書で学んだすべてを駆使することで、フォローしているユーザーのマイクロポスト一覧をステータスフィードに表示させることができた

12.5 演習

: 『演習の解答マニュアル (英語)』にはRuby on Railsチュートリアルのすべての演習の解答が掲載されており、www.railstutorial.orgで原著を購入いただいた方には無料で配布しています (訳注: 解答は英語です)。

なお、演習とチュートリアル本編の食い違いを避ける方法については、演習用のトピックブランチに追加したメモ (3.6) を参考にしてください。

  1. HomeページとProfileページにある統計情報のテストを書いてみてください。ヒント: リスト11.27のテストに追加してください。(Homeページの統計情報は別のテストにしてみませんか。)
  2. Homeページに表示されている1ページ目のフィードをテストしてください。リスト12.49はそのテンプレートです。CGI.escapeHTMLでHTMLのエスケープ処理を使っている点に注目して、なぜこれが必要なのか考えてみてください。(試しにエスケープ処理を外して、HTMLのソースコードを注意深く調べてください。マイクロポストの内容がおかしいはずです。)
リスト12.49: フィードのHTMLをテストする GREEN test/integration/following_test.rb
require 'test_helper'

class FollowingTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
    log_in_as(@user)
  end
  .
  .
  .
  test "feed on Home page" do
    get root_path
    @user.feed.paginate(page: 1).each do |micropost|
      assert_match CGI.escapeHTML(FILL_IN), FILL_IN
    end
  end
end
  1. モックアップツアーの写真は、それぞれ http://www.flickr.com/photos/john_lustig/2518452221/ と https://www.flickr.com/photos/renemensen/9187111340 から引用しました。 
  2. 見えやすくするため、12.6からfollowingテーブルのidカラムを省略しました。
  3. 読者のPaul Fioravantiがこの用語を提案してくれました。ありがとうございます。
  4. 技術的には、Railsはhas_manyに与えられた引数を、classifyメソッドを使ってクラス名に変換しています。このメソッドは、たとえば"foo_bars"であれば"FooBar"に変換します。
  5. 技術的には、Railsはunderscoreメソッドを使用してクラス名をidに変換しています。たとえば、"FooBar".underscoreを実行すると"foo_bar"に変換されます。従って、 FooBarオブジェクトの外部キーはfoo_bar_idになるでしょう
  6. 特定の分野でモデリングの経験を多く積めば、このようなユーティリティメソッドが必要になることを事前に思い付けるようになるでしょう。たとえ思い付けないことがあったとしても、明確なテストを書こうとするときに、いつの間にかこういうメソッドを自分が作成していることに気付くことでしょう。だからというわけではありませんが、今はこのようなメソッドが必要であるということに気付けなくても問題ありません。ソフトウェアの開発は、繰りかえしに次ぐ繰り返しです。読みづらくなるまでコードを書き足し、そのコードをリファクタリングする、その繰り返しです。そして、より簡潔なコードを書くために、本書が少しでもお役に立てばと思います。
  7. Railsにはさまざまなルーティングオプションがありますが、詳細についてはRailsガイドの記事「Railsルーティング」を参照してください。
  8. Asynchronous (非同期の) JavaScript And XMLの それぞれの頭文字をとっています。Ajaxはしばしば “AJAX” と大文字で書かれますが、Ajaxの起源となる記事では一貫して “Ajax” となっています。
  9. 列挙可能 (enumerable) オブジェクトであることの主な条件は、eachメソッドを実装していることです。このメソッドはコレクションを列挙します。
Railsチュートリアルは,Ruby/Rails のアジャイル開発を得意とする YassLab によって運営・保守されております.
継続的に良いコンテンツを提供する為に,電子書籍解説動画のご購入を検討して頂けると幸いです m(_ _)m
スポンサーシップや商用利用などに関するご相談がありましたら,お問い合わせページよりお気軽にご連絡ください :)