Ruby on Rails チュートリアル
-
第2版 目次
- 第1章ゼロからデプロイまで
- 第2章デモアプリケーション
- 第3章ほぼ静的なページの作成
- 第4章Rails風味のRuby
- 第5章レイアウトを作成する
- 第6章ユーザーのモデルを作成する
- 第7章ユーザー登録
- 第8章サインイン、サインアウト
- 第9章ユーザーの更新・表示・削除
- 第10章ユーザーのマイクロポスト
- 第11章ユーザーをフォローする
|
||
第2版 目次
|
||
最新版を読む |
Ruby on Rails チュートリアル
プロダクト開発の0→1を学ぼう
下記フォームからメールアドレスを入力していただくと、招待リンクが記載されたメールが届きます。リンクをクリックし、アカウントを有効化した時点から『30分間』解説動画のお試し視聴ができます。
メール内のリンクから視聴を開始できます。
第2版 目次
- 第1章ゼロからデプロイまで
- 第2章デモアプリケーション
- 第3章ほぼ静的なページの作成
- 第4章Rails風味のRuby
- 第5章レイアウトを作成する
- 第6章ユーザーのモデルを作成する
- 第7章ユーザー登録
- 第8章サインイン、サインアウト
- 第9章ユーザーの更新・表示・削除
- 第10章ユーザーのマイクロポスト
- 第11章ユーザーをフォローする
第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)。この節では、以後このフローの実現に専念します。
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。さらに、それぞれの行はユーザーなので、これらのユーザーに名前やパスワードなどの属性も追加する必要があるでしょう。
図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に示します。
このデータモデルを実装するには、最初に以下のようにRelationshipモデルを生成します。
$ rails generate model Relationship follower_id:integer followed_id:integer
上のコマンドを実行するとRelationshipファクトリーも生成されるので、以下を実行してファクトリーを削除してください。
$ rm -f spec/factories/relationship.rb
このリレーションシップは今後follower_id
とfollowed_id
で頻繁に検索することになるので、リスト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_id
とfollowed_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に示します。
11.1.2User/relationshipの関連付け
フォローしているユーザーとフォロワーを実装する前に、ユーザーとリレーションシップの関連付けを行います。1人のユーザーにはhas_many
(1対多) リレーションシップがあり、このリレーションシップは2人のユーザーの間の関係なので、フォローしているユーザーとフォロワーの両方に属します (belongs_to
)。
10.1.3のマイクロポストのときと同様、以下のようなユーザー関連付けのコードを使用して新しいリレーションシップを作成します。
user.relationships.build(followed_id: ...)
基本的な検証テストから始めることにします。 このテストをリスト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のようにテストしましょう。
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。
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
(属する) の関係を持ちます。この場合、リレーションシップのオブジェクトはfollower
とfollowed user
の両方に「属します」。リスト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
のみアクセス可能となっている点に注意してください。
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) は素直な作りです。
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
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_users
とfollowers
に取りかかります。まずはfollowed_users
から始めます (リスト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に伝えます。
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に示します。
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に示します。
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。
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)。
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_id
とfollowed_id
を入れ替えるだけで、フォロワーについてもユーザーのフォローのときとまったく同じ方法が使用できます。ということは、何らかの方法で2つのカラムを入れ替えたreverse_relationships
テーブルを用意することができれば (図11.9)、user.followers
を最小限の手間で実装できることになります。
Railsが魔法で何とかしてくれることを期待して、このテストを作成しましょう (リスト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
の関連付けを得ることができます。
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に示します。
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の拡大図を参照してください。
図11.10の統計情報には、現在のユーザーがフォローしている人数と、現在のフォロワーの人数が表示されています。それぞれの表示はリンクになっており、専用の表示ページに移動できます。第5章では、これらのリンクはダミーテキスト’#’
を使用して無効にしていました。しかしルーティングについての知識もだいぶ増えてきたので、今回は実装することにしましょう。実際のページ作成は11.2.3まで行いませんが、ルーティングは今実装します (リスト11.18)。このコードでは、resources
ブロックの内側で:member
メソッドを使用しています。これは初登場ですので、どんな動作をするのか推測してみてください。 (注: リスト11.18のコードは resources :users
を置き換えます)
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.1でfollowing_user_path
が生成されるように調整しました。
HTTPリクエスト | URL | アクション | 名前付きルート |
---|---|---|---|
GET | /users/1/following | following | following_user_path(1) |
GET | /users/1/followers | followers | followers_user_path(1) |
ルーティングが定義されたことで、統計情報パーシャルをテストできる状態になりました (最初にテストを書いてもよかったのですが、ルーティングを更新しておかないと名前付きルートの説明がしにくかったので、テストを後にしました) 。この統計情報パーシャルはプロファイルページと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タグ内のリンクです。
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でも説明したとおり、@user
が nil
でない場合 (つまりプロファイルページの場合) は何もせず、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のテストにもパスするようになります)。
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に示します。
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;
}
}
.
.
.
この後すぐ、プロファイルにも統計情報パーシャルを表示しますが、今のうちにリスト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 %>
このコードは、follow
とunfollow
のパーシャルに作業を振っているだけです。パーシャルでは、Relationshipsリソース用のルールを含む新しいルートが必要です。これを、リスト10.22のMicropostsリソースの例に従って作成しましょう (リスト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に示します。
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 %>
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.25のhidden_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] ボタンが表示されます。
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>
これらのボタンはもうすぐ動作するようになります。実はこのボタンの実装には2とおりの方法があります。1つは標準的な方法 (11.2.4)、もう1つはAjaxを使用する方法 (11.2.5) です。でもその前に、フォローしているユーザーとフォロワーを表示するページをそれぞれ作成してHTMLインターフェイスを完成させてしまいましょう。
11.2.3「フォローしているユーザー」ページと「フォロワー」ページ
フォローしているユーザーを表示するページと、フォロワーを表示するページは、いずれもユーザープロファイルページとユーザーインデックスページ (9.3.1) を合わせたような作りになるという点で似ています。どちらにもフォローの統計情報などのユーザー情報を表示するサイドバーと、ユーザーのリストがあります。さらに、サイドバーにはユーザープロファイル画像のリンクを格子状に並べて表示する予定です。この要求に合うモックアップを図11.14 (フォローしているユーザー用) および 図 11.15 (フォロワー用) に示します。
ここでの最初の作業は、フォローしているユーザーのリンクとフォロワーのリンクを動くようにすることです。Twitterにならい、どちらのページでもユーザーのサインインを要求します。 このテストは11.28のように行います。ユーザーがサインインしたら、どちらのページにもフォローしているユーザー用リンクとフォロワー用リンクを表示します。このテストはリスト11.29のように行います。
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
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に示します。
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で両方の場合をカバーできるためです。
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 (フォロワー) のようになるはずです。
11.2.4[フォローする] ボタン (標準的な方法)
ビューが整ってきました。いよいよ [フォローする] [フォロー解除する] ボタンを動作させましょう。これらのボタンのテストには、本書で扱ったさまざまなテスティングの技法が集約されています。このテストコードを読むのはよい練習になります。リスト11.32の内容をじっくり勉強し、テスト内容とその目的をすべて理解できるようにしてください (リスト11.32のコードには実は若干のセキュリティ上の抜けがあります。皆さんは見つけることができるでしょうか。この点についてはこの後でカバーします)。このコードにおけるhave_xpath
メソッドの使用法に注目してください。 これは、XPathを使用してHTML5を含むXMLドキュメントを自在にナビゲートすることのできる、極めて高度かつパワフルなテクニックです。Web検索でXPathを使用する方法の詳細についてはXPath構文 (英語) を参照してください。
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
メソッドを使用してこれらのアクションに直接アクセスすることによって、この要求を満たすようにしました。
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に示します。
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_user
はnil
になり、どちらのメソッドでも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に示します。
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 %>
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リクエストを発行しています。以前の、get
、post
、patch
、delete
メソッドを使用したテストと比べてみてください。それから、Ajaxリクエストを受信したときにcreate
アクションとdestroy
アクションが正常に動作することを確認します (Ajaxを多用するアプリケーションを徹底的にテストしたい方は、SeleniumとWatirを参照してみてください)。
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のコードも見てみてください。このコードは動作は同じですが、よりコンパクトになっています。)
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.erb
やdestroy.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を挿入するときに実行結果をエスケープする (画面に表示しない) ために必要です。
app/views/relationships/create.js.erb
$("#follow_form").html("<%= escape_javascript(render('users/unfollow')) %>")
$("#followers").html('<%= @user.followers.count %>')
destroy.js.erb
ファイルの方も同様です (リスト11.40)。
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に再度掲載します。
11.3.1動機と計画
ステータスフィードの基本的なアイディアはシンプルです。図11.19に、サンプルのmicroposts
データベーステーブルと、それをフィードした結果を示します。図の矢印で示されているように、この目的は、現在のユーザーによってフォローされているユーザーに対応するユーザーidを持つマイクロポストを取り出し、同時に現在のユーザー自身のマイクロポストも一緒に取り出すことです。
あるユーザーによってフォローされているユーザーのマイクロポストをすべて見つけ出すために、from_users_followed_by
というメソッドを実装することを考えてみましょう。このメソッドは、以下のように使用します。
Micropost.from_users_followed_by(user)
この時点ではもちろん実装はありませんが、機能を確認するためのテストは作成できます。このテストで重要なことは、フィードに必要な3つの条件を満たすことです。1) フォローしているユーザーのマイクロポストがフィードに含まれていること。2) 自分自身のマイクロポストもフィードに含まれていること。3) フォローしていないユーザーのマイクロポストがフィードに含まれていないこと。このうち2つについては、リスト10.35のテストで既に確認できるようになっています。このテストでは、ユーザー自身のマイクロポストがフィードに含まれ、フォローしていないユーザーのマイクロポストがフィードに含まれていないことを確認します。既にユーザーをフォローできるようになっているので、3番目のテスト、つまりフォローしているユーザーのマイクロポストがフィードに含まれていることを確認するテストをリスト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
に任せてしまっています。この内容は後で実装しなければなりません。
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に示します。
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でコードを若干修正し、フィードをリファクタリングすることから始めましょう。
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) されることに注意してください (実際はどちらでも動作しますが、この文脈では内挿と考える方が筋が通っています)。
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文を調べることで、このことを確認できます)。
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
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著) がお勧めです。
- PluralsightとCode School (どちらも英語)。PluralsightのスクリーンキャストとCode Schoolのインタラクティブコースは品質が高いことで知られており、強くお勧めいたします。
- (訳注) Rails ガイド: トピック毎に分類された最新のRailsリファレンスで、Railsチュートリアルの完走者や、Railsエンジニアを対象にして書かれています。本書で紹介しきれなかったActive ModelやAction View/Controllerの機能を解説する記事や、アセットパイプラインの仕組みやデバッグ手法を解説する記事などがあり、Railsについてより深く、体系的に学びたいときに便利です。合計 1,600 ページを超える大型書籍で (参考: 本書は合計700ページです)、本書と同様にWeb版は全て無料で読めます。
- (訳注) 地域Rubyの会: 読み物ではありませんが、日本全国にRuby/Railsのコミュニティがあり、定期的に勉強会やミートアップ (集まってコードを書いたりコードの相談をしたりするイベント) を開催しています。Railsチュートリアルをここまで読み終えた方であれば十分なスキルは揃っているはずなので、地域Rubyの会の中にあるお近くのコミュニティまで遊びに行ってはいかがでしょうか?
11.5演習
- 特定のユーザーに関連付けられたrelationshipの削除をテストするコードを追加してください (対応する実装はリスト11.4およびリスト11.16の
dependent :destroy
です)。ヒント: リスト10.12の例に従ってみましょう。 - リスト11.38の
respond_to
メソッドは、そのアクションから出発してRelationshipsコントローラ自身に到達します。そして、respond_to
ブロックは、respond_with
というRailsのメソッドと置き換え可能です。リスト11.47はこの置き換えを行ったものですが、このコードもテストスイートにパスすることを確認することで、このコードが正しく動作することを証明してください (このメソッドの詳細については、Googleで “rails respond_with” と検索してください)。 - リスト11.31をリファクタリングしてください。具体的には、フォローしているユーザーページ、フォロワーページ、Homeページ、ユーザー表示ページで共通して使用されているコードをパーシャルとして追加します。
- リスト11.19のモデルに従って、プロファイルページの統計情報のテストを作成してください。
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
- モックアップツアーの写真は、それぞれ https://www.flickr.com/photos/john_lustig/2518452221/と http://www.flickr.com/photos/30775272@N05/2884963755/より引用しました。↑
- 本書の最初の版では
user.following
としていましたが、結局これも混乱を招くことに気付きました。私にこの用語を変更する決心をさせてくれ、よりわかりやすいアドバイスを提供してくれた読者、Cosmo Leeに感謝いたします (なお、彼のアドバイスをそのまま採用したわけではありませんので、もし本書にまだわかりにくい部分があったとしても、彼には一切の責任がないことをここに申し伝えておきます) 。↑ - 簡単のため、図11.6では
followed_users
テーブルのid
カラムを省略してあります。↑ - 詳細については、Stack Overflowの「どんなときにletを使用すべきか (英語)」を参照してください。↑
- 技術的には、Railsは
underscore
メソッドを使用してクラス名をidに変換しています。たとえば、"FooBar".underscore
を実行すると"foo_bar"
に変換されます。従って、FooBar
オブジェクトの外部キーはfoo_bar_id
になるでしょう (なお、underscore
と逆の働きをするcamelize
というメソッドもあります。これは"camel_case"
を"CamelCase"
のように変換します) 。↑ followed_id
でもユーザーを特定できることに気付き、フォローしているユーザーとフォロワーの扱いが対称的でないことをよく考えてみれば、もうゲームに勝ったようなものです。これについては11.1.5でも扱います。↑- 特定の分野でモデリングの経験を多く積めば、このようなユーティリティメソッドが必要になることを事前に思い付けるようになるでしょう。たとえ思い付けないことがあったとしても、明確なテストを書こうとするときに、いつの間にかこういうメソッドを自分が作成していることに気付くことでしょう。だからというわけではありませんが、今はこのようなメソッドが必要であるということに気付けなくても問題ありません。ソフトウェアの開発は、繰りかえしに次ぐ繰り返しです。読みづらくなるまでコードを書き足し、そのコードをリファクタリングする、その繰り返しです。そして、より簡潔なコードを書くために、本書が少しでもお役に立てばと思います。↑
unfollow!
は、失敗したときに例外を発生しないことに注意してください。正直に書くと、削除に失敗したことをRailsがどうやって検出しているのか、著者にもわかりません。follow!
に感嘆符が付いているので、それに合わせてこのメソッドにも感嘆符を追加しているに過ぎません。↑- Ajaxは、名目上asynchronous JavaScript and XMLの略ということになっています。最も初期のAjaxに関する記事でも "Ajax" で統一されていますが、ときどき “AJAX” と誤ったスペルが使用されていることがあります。↑
- Ajaxが動作するためには、ブラウザでJavaScriptが有効になっている必要がありますが、実際にはJavaScriptが無効になっている場合でも11.2.4のようにまったく同じ動作になるようにしてあります。↑
- 列挙可能 (enumerable) オブジェクトであることの主な条件は、
each
メソッドを実装していることです。このメソッドはコレクションを列挙します。↑ - この記法は、実際にはRailsによるコアRuby言語の拡張として行われたのが始まりです。この記法があまりに便利なので、後にRuby自身にまで取り入れられたほどです。実にクールだと思いますが、いかがでしょうか。↑
- 必要なサブセレクトを作成するための、より高度な方法については、「ActiveRecordのサブセレクトをハックする (英語)」というブログ記事を参照してください。 ↑
- 実際は、図11.20の見栄えを良くするために、Railsコンソールを使用してマイクロポストをいくつか手動で投稿しています。↑
- RailsCastsではテストを省略していることが多いので、その点には注意してください。1回のエピソードを短くまとめるためにテストを省略しているのですが、それに釣られてテスティングの重要性を軽く考えることのないようにしてください。RailsCastでアイディアやヒントを得たら、新機能の実装はぜひともテスト駆動開発で進めることをお勧めいたします。(その意味でも、RailsCast「テスティングの方法 (英語)」をぜひ一度参照してください。Ryan Bates自身も、現実にはテスト駆動開発を採用していることが多くありますし、彼のテスティングスタイルは本書のものと基本的に同じです。)↑
Railsチュートリアルは YassLab 社によって運営されています。
コンテンツを継続的に提供するため、書籍・動画・質問対応サービスなどもご検討していただけると嬉しいです。
研修支援や教材連携にも対応しています。note マガジンや YouTube チャンネルも始めたので、よければぜひ遊びに来てください!