Ruby on Rails チュートリアル
-
第4版 目次
- 第1章ゼロからデプロイまで
- 第2章Toyアプリケーション
- 第3章ほぼ静的なページの作成
- 第4章Rails風味のRuby
- 第5章レイアウトを作成する
- 第6章ユーザーのモデルを作成する
- 第7章ユーザー登録
- 第8章基本的なログイン機構
- 第9章発展的なログイン機構
- 第10章ユーザーの更新・表示・削除
- 第11章アカウントの有効化
- 第12章パスワードの再設定
- 第13章ユーザーのマイクロポスト
- 第14章ユーザーをフォローする
|
||
第4版 目次
|
||
最新版を読む |
Ruby on Rails チュートリアル
プロダクト開発の0→1を学ぼう
下記フォームからメールアドレスを入力していただくと、招待リンクが記載されたメールが届きます。リンクをクリックし、アカウントを有効化した時点から『30分間』解説動画のお試し視聴ができます。
メール内のリンクから視聴を開始できます。
第4版 目次
- 第1章ゼロからデプロイまで
- 第2章Toyアプリケーション
- 第3章ほぼ静的なページの作成
- 第4章Rails風味のRuby
- 第5章レイアウトを作成する
- 第6章ユーザーのモデルを作成する
- 第7章ユーザー登録
- 第8章基本的なログイン機構
- 第9章発展的なログイン機構
- 第10章ユーザーの更新・表示・削除
- 第11章アカウントの有効化
- 第12章パスワードの再設定
- 第13章ユーザーのマイクロポスト
- 第14章ユーザーをフォローする
第14章ユーザーをフォローする
この章では、サンプルアプリケーションのコアとなる部分を完成させます。具体的には、他のユーザーをフォロー (およびフォロー解除) できるソーシャルな仕組みの追加と、フォローしているユーザーの投稿をステータスフィードに表示する機能を追加します。まずは、ユーザー間の関係性をどうモデリングするかについて学びます (14.1)。その後、 モデリング結果に対応するWebインターフェースを実装していきます (14.2)。このとき、Webインターフェースの例としてAjaxについても紹介します。最後に、ステータスフィードの完成版を実装します (14.3)。
この最終章では、本書の中で最も難易度の高い手法をいくつか使っています。その中には、ステータスフィード作成のためにRuby/SQLを「だます」テクニックも含まれます。この章の例全体にわたって、これまでよりも複雑なデータモデルを使います。ここで学んだデータモデルは、今後自分用のWebアプリケーションを開発するときに必ず役に立ちます。また、本書を卒業して実際の開発に携わるときのために、14.4で役立つリソース集 (読み物ガイド) についても紹介します。
この章で扱っている手法は本書全体の中で最も難易度が高いので、理解を助けるため、コードを書く前にはいったん立ち止まってインターフェースを探検することにします。これまでの章と同様、最初にモックアップを示します1。ページ操作の全体的なフローは次のようになります。あるユーザー (John Calvin) は自分のプロフィールページを最初に表示し (図 14.1)、フォローするユーザーを選択するためにUsersページ (図 14.2) に移動します。Calvinは2番目のユーザーThomas Hobbes (図 14.3) を表示し、[Follow] ボタンを押してフォローします。これにより、[Follow] ボタンが [Unfollow] に変わり、Hobbes の [followers] カウントが1人増えます (図 14.4)。CalvinがHomeページに戻ると、[following] カウントが1人増え、Hobbesのマイクロポストがステータスフィードに表示されるようになっていることがわかります (図 14.5)。この節では、以後このフローの実現に専念します。
14.1 Relationshipモデル
ユーザーをフォローする機能を実装する第一歩は、データモデルを構成することです。ただし、これは見た目ほど単純ではありません。素朴に考えれば、has_many
(1対多) の関連付けを用いて「1人のユーザーが複数のユーザーをhas_many
としてフォローし、1人のユーザーに複数のフォロワーがいることをhas_many
で表す」といった方法でも実装できそうです。しかし後ほど説明しますが、この方法ではたちまち壁に突き当たってしまいます。これを解決するためのhas_many through
についてもこの後で説明します。
Gitユーザーはこれまで同様新しいトピックブランチを作成してください。
$ git checkout -b following-users
14.1.1 データモデルの問題 (および解決策)
ユーザーをフォローするデータモデル構成のための第一歩として、典型的な場合を検討してみましょう。あるユーザーが、別のユーザーをフォローしているところを考えてみましょう。具体例を挙げると、CalvinはHobbesをフォローしています。これを逆から見れば、HobbesはCalvinからフォローされています。CalvinはHobbesから見ればフォロワー (follower) であり、CalvinがHobbesをフォローした (followed) ことになります。Railsにおけるデフォルトの複数形の慣習に従えば、あるユーザーをフォローしているすべてのユーザーの集合はfollowers
となり、user.followers
はそれらのユーザーの配列を表すことになります。しかし残念なことに、この名前付けは逆向きではうまくいきません (Railsというより英語の都合ですが)。あるユーザーがフォローしているすべてのユーザーの集合は、このままではfolloweds
となってしまい、英語の文法からも外れるうえに非常に見苦しいものになってしまいます。そこで、Twitterの慣習にならい、本チュートリアルではfollowing
という呼称を採用します (例: “50 following, 75 followers”)。したがって、あるユーザーがフォローしているすべてのユーザーの集合はcalvin.following
となります。
これにより、図 14.6のようにfollowing
テーブルと has_many
関連付けを使って、フォローしているユーザーのモデリングができます。user.following
はユーザーの集合でなければならないため、following
テーブルのそれぞれの行は、followed_id
で識別可能なユーザーでなければなりません (これはfollower_id
の関連付けについても同様です)2。さらに、それぞれの行はユーザーなので、これらのユーザーに名前やパスワードなどの属性も追加する必要があるでしょう。
図 14.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。
まずは、フォローしているユーザーを生成するために、能動的関係に焦点を当てていきます (受動的関係については14.1.5で考えていきます)。先ほどの図 14.6は実装のヒントになりました。フォローしているユーザーはfollowed_id
があれば識別することができるので、先ほどのfollowing
テーブルをactive_relationships
テーブルと見立ててみましょう。ただしユーザー情報は無駄なので、ユーザーid以外の情報は削除します。そして、followed_id
を通して、users
テーブルのフォローされているユーザーを見つけるようにします。このデータモデルを模式図にすると、図 14.7のようになります。
能動的関係も受動的関係も、最終的にはデータベースの同じテーブルを使うことになります。したがって、テーブル名にはこの「関係」を表す「relationships
」を使いましょう。モデル名はRailsの慣習にならって、Relationshipとします。作成したRelationshipデータモデルを図 14.8に示します。1つのrelationships
テーブルを使って2つのモデル (能動的関係と受動的関係) をシミュレートする方法については、14.1.4で説明します。
このデータモデルを実装するために、まずは次のように図 14.8に対応したマイグレーションを生成します。
$ rails generate model Relationship follower_id:integer followed_id:integer
このリレーションシップは今後follower_id
とfollowed_id
で頻繁に検索することになるので、リスト 14.1に示したように、それぞれのカラムにインデックスを追加します。
relationships
テーブルにインデックスを追加する db/migrate/[timestamp]_create_relationships.rb
class CreateRelationships < ActiveRecord::Migration[5.1]
def change
create_table :relationships do |t|
t.integer :follower_id
t.integer :followed_id
t.timestamps
end
add_index :relationships, :follower_id
add_index :relationships, :followed_id
add_index :relationships, [:follower_id, :followed_id], unique: true
end
end
リスト 14.1では複合キーインデックスという行もあることに注目してください。これは、follower_id
とfollowed_id
の組み合わせが必ずユニークであることを保証する仕組みです。これにより、あるユーザーが同じユーザーを2回以上フォローすることを防ぎます (リスト 6.29でメールアドレスの一意性を保証したり、リスト 13.3で使った複合キーインデックスと比較してみてください)。もちろん、このような重複 (2回以上フォローすること) が起きないよう、インターフェイス側の実装でも注意を払います(14.1.4)。しかし、ユーザーが何らかの方法で (例えばcurl
などのコマンドラインツールを使って) Relationshipのデータを操作するようなことも起こり得ます。そのような場合でも、一意なインデックスを追加していれば、エラーを発生させて重複を防ぐことができます。
relationships
テーブルを作成するために、いつものようにデータベースのマイグレーションを行います。
$ rails db:migrate
演習
- 図 14.7のid=
1
のユーザーに対してuser.following.map(&:id)
を実行すると、結果はどのようになるでしょうか? 想像してみてください。ヒント: 4.3.2で紹介したmap(&:method_name)
のパターンを思い出してください。例えばuser.following.map(&:id)
の場合、idの配列を返します。 - 図 14.7を参考にして、id=
2
のユーザーに対してuser.following
を実行すると、結果はどのようになるでしょうか? また、同じユーザーに対してuser.following.map(&:id)
を実行すると、結果はどのようになるでしょうか? 想像してみてください。
14.1.2 User/Relationshipの関連付け
フォローしているユーザーとフォロワーを実装する前に、UserとRelationshipの関連付けを行います。1人のユーザーにはhas_many
(1対多) のリレーションシップがあり、このリレーションシップは2人のユーザーの間の関係なので、フォローしているユーザーとフォロワーの両方に属します (belongs_to
)。
13.1.3のマイクロポストのときと同様、次のようなユーザー関連付けのコードを使って新しいリレーションシップを作成します。
user.active_relationships.build(followed_id: ...)
この時点で、アプリケーションコードは13.1.3のようになるのではないかと予測した方もいるかもしれません。実際似ているのですが、2つの大きな違いがあります。
まずは1つ目の違いについてです。以前、ユーザーとマイクロポストの関連付けをしたときは、次のように書きました。
class User < ApplicationRecord
has_many :microposts
.
.
.
end
引数の:microposts
シンボルから、Railsはこれに対応するMicropostモデルを探し出し、見つけることができました4。しかし今回のケースで同じように書くと、
has_many :active_relationships
となってしまい、(ActiveRelationshipモデルを探してしまい) Relationshipモデルを見つけることができません。このため、今回のケースでは、Railsに探して欲しいモデルのクラス名を明示的に伝える必要があります。
2つ目の違いは、先ほどの逆のケースについてです。以前はMicropostモデルで、
class Micropost < ApplicationRecord
belongs_to :user
.
.
.
end
このように書きました。microposts
テーブルにはuser_id
属性があるので、これを辿って対応する所有者 (ユーザー) を特定することができました (13.1.1)。データベースの2つのテーブルを繋ぐとき、このようなidは外部キー (foreign key)と呼びます。すなわち、Userモデルに繋げる外部キーが、Micropostモデルのuser_id
属性ということです。この外部キーの名前を使って、Railsは関連付けの推測をしています。具体的には、Railsはデフォルトでは外部キーの名前を<class>_id
といったパターンとして理解し、 <class>
に当たる部分からクラス名 (正確には小文字に変換されたクラス名) を推測します5。ただし、先ほどはユーザーを例として扱いましたが、今回のケースではフォローしているユーザーをfollower_id
という外部キーを使って特定しなくてはなりません。また、followerというクラス名は存在しないので、ここでもRailsに正しいクラス名を伝える必要が発生します。
先ほどの説明をコードにまとめると、UserとRelationshipの関連付けはリスト 14.2とリスト 14.3のようになります。
has_many
) の関連付けを実装する app/models/user.rb
class User < ApplicationRecord
has_many :microposts, dependent: :destroy
has_many :active_relationships, class_name: "Relationship",
foreign_key: "follower_id",
dependent: :destroy
.
.
.
end
(ユーザーを削除したら、ユーザーのリレーションシップも同時に削除される必要があります。そのため、関連付けにdependent: :destroy
も追加しています。)
belongs_to
の関連付けを追加する app/models/relationship.rb
class Relationship < ApplicationRecord
belongs_to :follower, class_name: "User"
belongs_to :followed, class_name: "User"
end
なお、follower
の関連付けについては、14.1.4に入るまでは使いません。しかしfollowerとfollowedを対称的に実装しておくことで、構造に対する理解は容易になるはずです。
リスト 14.2とリスト 14.3で定義した関連付けにより、表 13.1で以前紹介したような多くのメソッドが使えるようになりました。今回使えるようになったメソッドを表 14.1に示します。
メソッド | 用途 |
active_relationship.follower |
フォロワーを返します |
active_relationship.followed |
フォローしているユーザーを返します |
user.active_relationships.create(followed_id: other_user.id) |
user と紐付けて能動的関係を作成/登録する |
user.active_relationships.create!(followed_id: other_user.id) |
user を紐付けて能動的関係を作成/登録する (失敗時にエラーを出力) |
user.active_relationships.build(followed_id: other_user.id) |
user と紐付けた新しいRelationshipオブジェクトを返す |
14.1.3 Relationshipのバリデーション
先に進む前に、Relationshipモデルの検証を追加して完全なものにしておきましょう。テストコード (リスト 14.4) とアプリケーションコード (リスト 14.5) は素直な作りです。ただし、User用のfixtureファイル (リスト 6.30) と同じように、生成されたRelationship用のfixtureでは、マイグレーション (リスト 14.1) で制約させた一意性を満たすことができません。ということで、ユーザーのときと同じで (リスト 6.31でfixtureの内容を削除したように)、今の時点では生成されたRelationship用のfixtureファイルも空にしておきましょう (リスト 14.6)。
test/models/relationship_test.rb
require 'test_helper'
class RelationshipTest < ActiveSupport::TestCase
def setup
@relationship = Relationship.new(follower_id: users(:michael).id,
followed_id: users(:archer).id)
end
test "should be valid" do
assert @relationship.valid?
end
test "should require a follower_id" do
@relationship.follower_id = nil
assert_not @relationship.valid?
end
test "should require a followed_id" do
@relationship.followed_id = nil
assert_not @relationship.valid?
end
end
app/models/relationship.rb
class Relationship < ApplicationRecord
belongs_to :follower, class_name: "User"
belongs_to :followed, class_name: "User"
validates :follower_id, presence: true
validates :followed_id, presence: true
end
test/fixtures/relationships.yml
# 空にする
この時点では、テストは greenになるはずです。
$ rails test
14.1.4 フォローしているユーザー
いよいよRelationshipの関連付けの核心、following
とfollowers
に取りかかります。今回はhas_many through
を使います。図 14.7のように、1人のユーザーにはいくつもの「フォローする」「フォローされる」といった関係性があります (こういった関係性を「多対多」と呼びます)。デフォルトのhas_many through
という関連付けでは、Railsはモデル名 (単数形) に対応する外部キーを探します。つまり、次のコードでは、
has_many :followeds, through: :active_relationships
Railsは「followeds」というシンボル名を見て、これを「followed」という単数形に変え、 relationships
テーブルのfollowed_id
を使って対象のユーザーを取得してきます。しかし、14.1.1で指摘したように、user.followeds
という名前は英語として不適切です。代わりに、user.following
という名前を使いましょう。そのためには、Railsのデフォルトを上書きする必要があります。ここでは:source
パラメーター (リスト 14.8) を使って、「following
配列の元はfollowed
idの集合である」ということを明示的にRailsに伝えます。
following
の関連付けを追加する app/models/user.rb
class User < ApplicationRecord
has_many :microposts, dependent: :destroy
has_many :active_relationships, class_name: "Relationship",
foreign_key: "follower_id",
dependent: :destroy
has_many :following, through: :active_relationships, source: :followed
.
.
.
end
リスト 14.8で定義した関連付けにより、フォローしているユーザーを配列の様に扱えるようになりました。例えば、include?
メソッド (4.3.1) を使ってフォローしているユーザーの集合を調べてみたり、関連付けを通してオブジェクトを探しだせるようになります。
user.following.include?(other_user)
user.following.find(other_user)
following
で取得したオブジェクトは、配列のように要素を追加したり削除したりすることができます。
user.following << other_user
user.following.delete(other_user)
(4.3.1を思い出してください。<<
演算子 (Shovel Operator) で配列の最後に追記することができます。)
following
メソッドで配列のように扱えるだけでも便利ですが、Railsは単純な配列ではなく、もっと賢くこの集合を扱っています。例えば次のようなコードでは、
following.include?(other_user)
フォローしている全てのユーザーをデータベースから取得し、その集合に対してinclude?
メソッドを実行しているように見えますが、しかし実際にはデータベースの中で直接比較をするように配慮しています。なお、13.2.1でも説明したように、次のようなコードでは、
user.microposts.count
データベースの中で合計を計算したほうが高速になる点に注意してください。
次に、followingで取得した集合をより簡単に取り扱うために、follow
やunfollow
といった便利メソッドを追加しましょう。これらのメソッドは、例えばuser.follow(other_user)
といった具合に使います。さらに、これに関連するfollowing?
論理値メソッドも追加し、あるユーザーが誰かをフォローしているかどうかを確認できるようにします6。
今回は、こういったメソッドはテストから先に書いていきます。というのも、Webインターフェイスなどで便利メソッドを使うのはまだ先なので、すぐに使える場面がなく、実装した手応えを得にくいからです。一方で、Userモデルに対するテストを書くのは簡単かつ今すぐできます。そのテストの中で、これらのメソッドを使っていきます。具体的には、following?
メソッドであるユーザーをまだフォローしていないことを確認、follow
メソッドを使ってそのユーザーをフォロー、 following?
メソッドを使ってフォロー中になったことを確認、 最後にunfollow
メソッドでフォロー解除できたことを確認、といった具合でテストをしていきます。作成したコードをリスト 14.9に示します。
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
表 14.1のメソッドを参考にしながら、following
による関連付けを使ってfollow
、unfollow
、following?
メソッドを実装していきましょう (リスト 14.10)。このとき、可能な限りself
(user自身を表すオブジェクト) を省略している点に注目してください。
app/models/user.rb
class User < ApplicationRecord
.
.
.
def feed
.
.
.
end
# ユーザーをフォローする
def follow(other_user)
following << other_user
end
# ユーザーをフォロー解除する
def unfollow(other_user)
active_relationships.find_by(followed_id: other_user.id).destroy
end
# 現在のユーザーがフォローしてたらtrueを返す
def following?(other_user)
following.include?(other_user)
end
private
.
.
.
end
リスト 14.10のコードを追加することで、テストスイートは greenになるはずです。
$ rails test
14.1.5 フォロワー
リレーションシップというパズルの最後の一片は、user.followers
メソッドを追加することです。これは上のuser.following
メソッドと対になります。図 14.7を見ていて気付いた方もいると思いますが、フォロワーの配列を展開するために必要な情報は、relationships
テーブルに既にあります。つまり、リスト 14.2で作成したactive_relationships
のテーブルを再利用することができそうです。実際、follower_id
とfollowed_id
を入れ替えるだけで、フォロワーについてもフォローする場合と全く同じ方法が活用できます。したがって、データモデルは図 14.9のようになります。
図 14.9を参考にしたデータモデルの実装をリスト 14.12に示しますが、この実装はリスト 14.8とまさに類似しています。
user.followers
を実装する app/models/user.rb
class User < ApplicationRecord
has_many :microposts, dependent: :destroy
has_many :active_relationships, class_name: "Relationship",
foreign_key: "follower_id",
dependent: :destroy
has_many :passive_relationships, class_name: "Relationship",
foreign_key: "followed_id",
dependent: :destroy
has_many :following, through: :active_relationships, source: :followed
has_many :followers, through: :passive_relationships, source: :follower
.
.
.
end
一点、リスト 14.12で注意すべき箇所は、次のように参照先 (followers
) を指定するための:source
キーを省略してもよかったという点です。
has_many :followers, through: :passive_relationships
これは:followers
属性の場合、Railsが「followers」を単数形にして自動的に外部キーfollower_id
を探してくれるからです。リスト 14.12と違って必要のない:source
キーをそのまま残しているのは、has_many :following
との類似性を強調させるためです。
次に、followers.include?
メソッドを使って先ほどのデータモデルをテストしていきます。テストコードはリスト 14.13のとおりです。ちなみにリスト 14.13では、following?
と対照的なfollowed_by?
メソッドを定義してもよかったのですが、サンプルアプリケーションで実際に使う場面がなかったのでこのメソッドは省略しました。
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
リスト 14.13ではリスト 14.9に1行だけ追加していますが、実際には多くの処理が正しく動いていなければパスしません。つまり、リスト 14.12の実装に対するテストは、実装の影響を受けやすいテストだといえます。
この時点で、全てのテストが greenになるはずです。
$ rails test
演習
- コンソールを開き、何人かのユーザーが最初のユーザーをフォローしている状況を作ってみてください。最初のユーザーを
user
とすると、user.followers.map(&:id)
の値はどのようになっているでしょうか? - 上の演習が終わったら、
user.followers.count
の実行結果が、先ほどフォローさせたユーザー数と一致していることを確認してみましょう。 user.followers.count
を実行した結果、出力されるSQL文はどのような内容になっているでしょうか? また、user.followers.to_a.count
の実行結果と違っている箇所はありますか? ヒント: もしuser
に100万人のフォロワーがいた場合、どのような違いがあるでしょうか? 考えてみてください。
14.2 [Follow] のWebインターフェイス
14.1では、やや複雑なデータモデリングの技術を説明しました。理解するのに時間がかかってしまっても大丈夫なので、安心してください。また、これまでに使われた様々な関連付けを理解するのに一番良い方法は、実際にWebインターフェイスで使ってみることでしょう。
この章の最初に、フォローしているユーザーのページ表示の流れについて説明しました。この節では、モックアップで示したようにフォロー/フォロー解除の基本的なインターフェイスを実装します。また、フォローしているユーザーと、フォロワーにそれぞれ表示用のページを作成します。14.3では、ユーザーのステータスフィードを追加して、サンプルアプリケーションを完成させます。
14.2.1 フォローのサンプルデータ
1つ前の章のときと同じように、サンプルデータを自動作成するrails db:seed
を使って、データベースにサンプルデータを登録できるとやはり便利です。先にサンプルデータを自動作成できるようにしておけば、Webページの見た目のデザインから先にとりかかることができ、バックエンド機能の実装を後に回すことができます。
リスト 14.14は、リレーションシップのサンプルデータを生成するためのコードです。ここでは、最初のユーザーにユーザー3
からユーザー51
までをフォローさせ、それから逆にユーザー4
からユーザー41
に最初のユーザーをフォローさせます。ソースを見るとわかるように、このような設定を自由に行うことができます。こうしてリレーションシップを作成しておけば、アプリケーションのインターフェイスを開発するには十分です。
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) }
リスト 14.14を実行してデータベース上のサンプルデータを作り直すために、いつものコマンドを実行しましょう。
$ rails db:migrate:reset
$ rails db:seed
14.2.2 統計と [Follow] フォーム
これでサンプルユーザーに、フォローしているユーザーとフォロワーができました。プロフィールページとHomeページを更新して、これを反映しましょう。最初に、プロフィールページとHomeページに、フォローしているユーザーとフォロワーの統計情報を表示するためのパーシャルを作成します。次に、フォロー用とフォロー解除用のフォームを作成します。それから、フォローしているユーザーの一覧 ("following") とフォロワーの一覧 ("followers") を表示する専用のページを作成します。
14.1.1で指摘したように、Twitterの慣習に従ってフォロー数の単位には「following」を使い、例えば「50 following」といった具合に表示します。この単位は図 14.1のモックアップの一部でも既に使われていました。該当箇所を拡大して図 14.10に再掲します。
図 14.10の統計情報には、現在のユーザーがフォローしている人数と、現在のフォロワーの人数が表示されています。それぞれの表示はリンクになっており、専用の表示ページに移動できます。第5章では、これらのリンクはダミーテキスト'#'
を使って無効にしていました。しかしルーティングについての知識もだいぶ増えてきたので、今回は実装することにしましょう。実際のページ作成は14.2.3まで行いませんが、ルーティングは今実装します (リスト 14.15)。このコードでは、resources
ブロックの内側で:member
メソッドを使っています。これは初登場のメソッドですが、まずはどんな動作をするのか推測してみてください。
following
アクションとfollowers
アクションを追加する config/routes.rb
Rails.application.routes.draw do
root 'static_pages#home'
get '/help', to: 'static_pages#help'
get '/about', to: 'static_pages#about'
get '/contact', to: 'static_pages#contact'
get '/signup', to: 'users#new'
get '/login', to: 'sessions#new'
post '/login', to: 'sessions#create'
delete '/logout', to: 'sessions#destroy'
resources :users do
member do
get :following, :followers
end
end
resources :account_activations, only: [:edit]
resources :password_resets, only: [:new, :create, :edit, :update]
resources :microposts, only: [:create, :destroy]
end
この場合のURLは /users/1/following や /users/1/followers のようになるのではないかと推測した方もいると思います。そして、リスト 14.15のコードはまさにそれを行なっているのです。また、どちらもデータを表示するページなので、適切なHTTPメソッドはGET
リクエストになります。したがって、get
メソッドを使って適切なレスポンスを返すようにします。ちなみに、member
メソッドを使うとユーザーidが含まれているURLを扱うようになりますが、 idを指定せずにすべてのメンバーを表示するには、次のようにcollection
メソッドを使います。
resources :users do
collection do
get :tigers
end
end
このコードは /users/tigers というURLに応答します (アプリケーションにあるすべてのtigerのリストを表示します)7。
リスト 14.15によって生成されるルーティングテーブルを表 14.2に示します。この表で示したフォロー用とフォロワー用の名前付きルートを、今後の実装で使っていきます。
HTTPリクエスト | URL | アクション | 名前付きルート |
GET |
/users/1/following | following |
following_user_path(1) |
GET |
/users/1/followers | followers |
followers_user_path(1) |
ルーティングを定義したので、統計情報のパーシャルを実装する準備が整いました。このパーシャルでは、divタグの中に2つのリンクを含めるようにします (リスト 14.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ページの両方に表示されるので、リスト 14.16の最初の行では、次のコードで現在のユーザーを取得します。
<% @user ||= current_user %>
これはコラム 8.1でも説明したとおり、@user
がnil
でない場合 (つまりプロフィールページの場合) は何もせず、nil
の場合 (つまりHomeページの場合) には@user
にcurrent_user
を代入するコードです。その後、フォローしているユーザーの人数を、次のように関連付けを使って計算します。
@user.following.count
これはフォロワーについても同様です。
@user.followers.count
上のコードは、リスト 13.24でマイクロポストの投稿数を表示した方法と同じです。あのときは次のように書きました。
@user.microposts.count
なお、今回も以前と同様に、Railsは高速化のためにデータベース内で合計を計算している点に注意してください。
さて、一部の要素で、次のようにCSS idを指定していることにもぜひ注目してください。
<strong id="following" class="stat">
...
</strong>
こうしておくと、14.2.5でAjaxを実装するときに便利です。そこでは、一意のidを指定してページ要素にアクセスしています。
これで統計情報パーシャルができあがります。Homeページにこの統計情報を表示するには、リスト 14.17のようにすると簡単です。
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 %>
統計情報にスタイルを与えるために、リスト 14.18のようにSCSSを追加しましょう (なお、このSCSSにはこの章で使うすべてのスタイルが含まれています)。変更の結果、Homeページは図 14.11のようになります。
app/assets/stylesheets/custom.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 */
.
.
.
この後すぐ、プロフィールにも統計情報パーシャルを表示しますが、今のうちにリスト 14.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 %>
このコードは、follow
とunfollow
のパーシャルに作業を振っているだけです。パーシャルでは、Relationshipsリソース用の新しいルーティングが必要です。これを、リスト 13.30のMicropostsリソースの例に従って作成しましょう (リスト 14.20)。
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
フォロー/フォロー解除用のパーシャル自体は、リスト 14.21とリスト 14.22に示します。
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 %>
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 btn-default" %>
<% end %>
これらの2つのフォームでは、いずれもform_for
を使ってRelationshipモデルオブジェクトを操作しています。これらの2つのフォームの主な違いは、リスト 14.21は新しいリレーションシップを作成するのに対し、リスト 14.22は既存のリレーションシップを見つけ出すという点です。すなわち、前者はPOST
リクエストを Relationshipsコントローラに送信してリレーションシップをcreate
(作成) し、後者はDELETE
リクエストを送信してリレーションシップをdestroy
(削除) するということです (これらのアクションは14.2.4で実装します)。最終的に、このフォロー/フォロー解除フォームにはボタンしかないことを理解していただけたと思います。しかし、それでもこのフォームはfollowed_id
をコントローラに送信する必要があります。これを行うために、リスト 14.21のhidden_field_tag
メソッドを使います。このメソッドは、次のようなフォーム用HTMLを生成します。
<input id="followed_id" name="followed_id" type="hidden" value="3" />
12.3のリスト 12.14で見たように、隠しフィールドのinput
タグを使うことで、ブラウザ上に表示させずに適切な情報を含めることができます。
このテクニックを使ってフォロー用フォームをパーシャルとしてプロフィール画面に表示した結果がリスト 14.23になります。プロフィール画面に [Follow] ボタンと [Unfollow] ボタンがそれぞれ表示されることを確認してみましょう (図 14.12、図 14.13)。
app/views/users/show.html.erb
<% provide(:title, @user.name) %>
<div class="row">
<aside class="col-md-4">
<section class="user_info">
<h1>
<%= gravatar_for @user %>
<%= @user.name %>
</h1>
</section>
<section class="stats">
<%= render 'shared/stats' %>
</section>
</aside>
<div class="col-md-8">
<%= render 'follow_form' if logged_in? %>
<% if @user.microposts.any? %>
<h3>Microposts (<%= @user.microposts.count %>)</h3>
<ol class="microposts">
<%= render @microposts %>
</ol>
<%= will_paginate @microposts %>
<% end %>
</div>
</div>
これらのボタンはもうすぐ動作するようになります。実はこのボタンの実装には2通りの方法があります。1つは標準的な方法 (14.2.4)、もう1つはAjaxを使う方法 (14.2.5) です。でもその前に、フォローしているユーザーとフォロワーを表示するページをそれぞれ作成してHTMLインターフェイスを完成させてしまいましょう。
14.2.3 [Following] と [Followers] ページ
フォローしているユーザーを表示するページと、フォロワーを表示するページは、いずれもプロフィールページとユーザー一覧ページ (10.3.1) を合わせたような作りになるという点で似ています。どちらにもフォローの統計情報などのユーザー情報を表示するサイドバーと、ユーザーのリストがあります。さらに、サイドバーには小さめのユーザープロフィール画像のリンクを格子状に並べて表示する予定です。この要求に合うモックアップを図 14.14 (フォローしているユーザー用) および図 14.15 (フォロワー用) に示します。
ここでの最初の作業は、フォローしているユーザーのリンクとフォロワーのリンクを動くようにすることです。 Twitterに倣って、どちらのページでもユーザーのログインを要求するようにします。そこで前回のアクセス制御と同様に、まずはテストから書いていきましょう。今回使うテストはリスト 14.24のとおりです。なお、リスト 14.24では表 14.2でまとめた名前付きルートを使っている点に注意してください。
test/controllers/users_controller_test.rb
require 'test_helper'
class UsersControllerTest < ActionDispatch::IntegrationTest
def setup
@user = users(:michael)
@other_user = users(:archer)
end
.
.
.
test "should redirect following when not logged in" do
get following_user_path(@user)
assert_redirected_to login_url
end
test "should redirect followers when not logged in" do
get followers_user_path(@user)
assert_redirected_to login_url
end
end
この実装には1つだけトリッキーな部分があります。それはUsersコントローラに2つの新しいアクションを追加する必要があるということです。これはリスト 14.15で定義した2つのルーティングにもとづいており、これらはそれぞれfollowing
およびfollowers
と呼ぶ必要があります。それぞれのアクションでは、タイトルを設定し、ユーザーを検索し、@user.following
または@user.followers
からデータを取り出し、ページネーションを行なって、ページを出力する必要があります。作成したコードをリスト 14.25に示します。
following
アクションとfollowers
アクション red 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
を呼び出す、といった具合です。一方で、リスト 14.25のいずれのアクションも、render
を明示的に呼び出し、show_follow
という同じビューを出力しています。したがって、作成が必要なビューはこれ1つです。renderで呼び出しているビューが同じである理由は、このERbはどちらの場合でもほぼ同じであり、リスト 14.26で両方の場合をカバーできるためです。
show_follow
ビュー green 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>
リスト 14.25にあるアクションは、2通りの方法でリスト 14.26のビューを呼び出します。“following”をとおって描画したビューを図 14.16に、“followers”をとおって描画したビューを図 14.17に示します。このとき、上のコードでは現在のユーザーを一切使っていない点に注目してください。したがって、他のユーザーのフォロワー一覧ページもうまく動きます (図 14.18)。
なお、リスト 14.25でbeforeフィルターを既に実装しているため、この時点でリスト 14.24のテストは green になっているはずです。
$ rails test
次に、show_follow
の描画結果を確認するため、統合テストを書いていきます。ただし今回の統合テストは基本的なテストだけに留めており、網羅的なテストにはしていません。これは5.3.4でも指摘したように、HTML構造を網羅的にチェックするテストは壊れやすく、生産性を逆に落としかねないからです。したがって今回は、正しい数が表示されているかどうかと、正しいURLが表示されているかどうかの2つのテストを書きます。
いつものように、統合テストを生成するところから始めます。
$ rails generate integration_test following
invoke test_unit
create test/integration/following_test.rb
今度はテストデータをいくつか揃えます。リレーションシップ用のfixtureにデータを追加しましょう。13.2.3では、次のように書くことで、
orange:
content: "I just ate an orange!"
created_at: <%= 10.minutes.ago %>
user: michael
ユーザーとマイクロポストを関連付けできたことを思い出してください。上のコードではユーザー名を次のように書いていますが、
user: michael
これは内部的には次のようなコードに自動的に変換されます。
user_id: 1
この例を参考にしてRelationship用のfixtureにテストデータを追加すると、リスト 14.28のようになります。
test/fixtures/relationships.yml
one:
follower: michael
followed: lana
two:
follower: michael
followed: malory
three:
follower: lana
followed: michael
four:
follower: archer
followed: michael
リスト 14.28のfixtureでは、前半の2つでMichaelがLanaとMaloryをフォローし、後半の2つでLanaとArcherがMichaelをフォローしています。あとは、正しい数かどうかを確認するために、assert_match
メソッド (リスト 13.28) を使ってプロフィール画面のfollowingおよびfollowersの数をテストします。さらに、正しいURLかどうかをテストするコードも加えると、リスト 14.29のようになります。
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
なお、リスト 14.29では、このようなコードを加えていますが、
assert_not @user.following.empty?
このコードは次のコードを確かめるためのテストであって、
@user.following.each do |user|
assert_select "a[href=?]", user_path(user)
end
無意味なテストではないことに注意してください (followers
についても同様です)。つまり、もし@user.following.empty?
の結果がtrueであれば、assert_select
内のブロックが実行されなくなるため、その場合においてテストが適切なセキュリティモデルを確認できなくなることを防いでいます。
話を戻して、上の変更を加えるとテストが greenになるはずです。確認してみましょう。
$ rails test
14.2.4 [Follow] ボタン (基本編)
ビューが整ってきました。いよいよ [Follow] / [Unfollow] ボタンを動作させましょう。フォローとフォロー解除はそれぞれリレーションシップの作成と削除に対応しているため、まずはRelationshipsコントローラが必要です。いつものようにコントローラを生成しましょう。
$ rails generate controller Relationships
リスト 14.32でも説明しますが、Relationshipsコントローラのアクションでアクセス制御することはそこまで難しくありません。しかし、前回のアクセス制御のときと同様に最初にテストを書き、それをパスするように実装することでセキュリティモデルを確立させていきましょう。今回はまず、コントローラのアクションにアクセスするとき、ログイン済みのユーザーであるかどうかをチェックします。もしログインしていなければログインページにリダイレクトされるので、Relationshipのカウントが変わっていないことを確認します (リスト 14.31)。
test/controllers/relationships_controller_test.rb
require 'test_helper'
class RelationshipsControllerTest < ActionDispatch::IntegrationTest
test "create should require logged-in user" do
assert_no_difference 'Relationship.count' do
post relationships_path
end
assert_redirected_to login_url
end
test "destroy should require logged-in user" do
assert_no_difference 'Relationship.count' do
delete relationship_path(relationships(:one))
end
assert_redirected_to login_url
end
end
次に、リスト 14.31のテストをパスさせるために、logged_in_user
フィルターをRelationshipsコントローラのアクションに対して追加します (リスト 14.32)。
app/controllers/relationships_controller.rb
class RelationshipsController < ApplicationController
before_action :logged_in_user
def create
end
def destroy
end
end
[Follow] / [Unfollow] ボタンを動作させるためには、フォーム (リスト 14.21とリスト 14.22) から送信されたパラメータを使って、followed_id
に対応するユーザーを見つけてくる必要があります。その後、見つけてきたユーザーに対して適切にfollow
/unfollow
メソッド (リスト 14.10) を使います。このすべてを実装した結果を、リスト 14.33に示します。
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
リスト 14.33を見てみれば、先ほどのセキュリティ問題が実はそれほど重大なものではないことを理解いただけると思います。もしログインしていないユーザーが (curl
などのコマンドラインツールなどを使って) これらのアクションに直接アクセスするようなことがあれば、current_user
はnil
になり、どちらのメソッドでも2行目で例外が発生します。エラーにはなりますが、アプリケーションやデータに影響は生じません。このままでも支障はありませんが、やはりこのような例外には頼らない方がよいので、上ではひと手間かけてセキュリティのためのレイヤーを追加しました。
これで、フォロー/フォロー解除の機能が完成しました。どのユーザーも、他のユーザーをフォローしたりフォロー解除したりできます。ブラウザ上でボタンをクリックして、確かめてみてください。振る舞いを検証する統合テストは14.2.6で実装することにして、まずは2番目のユーザーをフォローする前の状態を図 14.19に、フォローした結果を図 14.20にそれぞれ示します。
演習
- ブラウザ上から /users/2 を開き、[Follow] と [Unfollow] を実行してみましょう。うまく機能しているでしょうか?
- 先ほどの演習を終えたら、Railsサーバーのログを見てみましょう。フォロー/フォロー解除が実行されると、それぞれどのテンプレートが描画されているでしょうか?
14.2.5 [Follow] ボタン (Ajax編)
フォロー関連の機能の実装はこのとおり完了しましたが、ステータスフィードに取りかかる前にもう一つだけ機能を洗練させてみたいと思います。14.2.4では、Relationshipsコントローラのcreate
アクションと destroy
アクションを単に元のプロフィールにリダイレクトしていました。つまり、ユーザーはプロフィールページを最初に表示し、それからユーザーをフォローし、その後すぐ元のページにリダイレクトされるという流れになります。ユーザーをフォローした後、本当にそのページから離れて元のページに戻らないといけないのでしょうか。この点を考えなおしてみましょう。
これはAjaxを使うことで解決できます。Ajaxを使えば、Webページからサーバーに「非同期」で、ページを移動することなくリクエストを送信することができます8。WebフォームにAjaxを採用するのは今や当たり前になりつつあるので、RailsでもAjaxを簡単に実装できるようになっています。フォロー用とフォロー解除用のパーシャルをこれに沿って更新するのは簡単です。次のコードがあるとすると、
form_for
上のコードを次のように置き換えるだけです。
form_for ..., remote: true
たったこれだけで、Railsは自動的にAjaxを使うようになります。具体的な更新の結果を、リスト 14.34とリスト 14.35に示します。
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 %>
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 btn-default" %>
<% 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 2以前では、完全なJavaScriptのコードを挿入する必要がありました。しかし先ほどの例で見たように、現在のRailsではHTMLプロパティを使って簡単にAjaxが扱えるようになっています。これは、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
アクション (リスト 14.33) にそれぞれ追加してみましょう。変更の結果をリスト 14.36に示します。このとき、ユーザーのローカル変数 (user
) をインスタンス変数 (@user
) に変更した点に注目してください。これは、リスト 14.33のときはインスタンス変数は必要なかったのですが、リスト 14.34やリスト 14.35を実装したことにより、インスタンス変数が必要になったためです。
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
(訳注: ビューで変数を使うため、user
が@user
に変わった点にも気をつけてください。)
リスト 14.36でAjaxリクエストに対応したので、今度はブラウザ側でJavaScriptが無効になっていた場合 (Ajaxリクエストが送れない場合) でもうまく動くようにします (リスト 14.37)。
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.erb
やdestroy.js.erb
など) を呼び出すからです。ご想像のとおり、これらのファイルではJavaScriptと埋め込みRuby (ERb) をミックスして現在のページに対するアクションを実行することができます。ユーザーをフォローしたときやフォロー解除したときにプロフィールページを更新するために、私たちがこれから作成および編集しなければならないのは、まさにこれらのファイルです。
JS-ERbファイルの内部では、DOM (Document Object Model) を使ってページを操作するため、RailsがjQuery JavaScriptヘルパーを自動的に提供しています。これによりjQueryライブラリの膨大なDOM操作用メソッドが使えるようになりますが、13.4.2で見たように今回使うのはわずか2つです。1つずつ見ていきましょう。まずはドル記号 ($) とCSS idを使って、DOM要素にアクセスする文法について知る必要があります。例えばfollow_form
の要素をjQueryで操作するには、次のようにアクセスします。
$("#follow_form")
リスト 14.19では、これはフォームを囲むdiv
タグであり、フォームそのものではなかったことを思い出してください。jQueryの文法はCSSの記法から影響を受けており、#
シンボルを使ってCSSのidを指定します。ご想像のとおり、jQueryはCSSと同様、ドット.
を使ってCSSクラスを操作できます。
次に必要なメソッドはhtml
です。これは、指定された要素の内側にあるHTMLを、引数の内容で更新します。例えばフォロー用フォーム全体を"foobar"
という文字列で置き換えたいn場合は、次のようなコードになります。
$("#follow_form").html("foobar")
純粋なJavaScriptと異なり、JS-ERbファイルでは組み込みRuby (ERb) が使えます。create.js.erb
ファイルでは、フォロー用のフォームをunfollow
パーシャルで更新し、フォロワーのカウントを更新するのにERbを使っています (もちろんこれは、フォローに成功した場合の動作です)。変更の結果をリスト 14.38に示します。このコードではescape_javascript
メソッドを使っている点に注目してください。このメソッドは、JavaScriptファイル内にHTMLを挿入するときに実行結果をエスケープするために必要です。
app/views/relationships/create.js.erb
$("#follow_form").html("<%= escape_javascript(render('users/unfollow')) %>");
$("#followers").html('<%= @user.followers.count %>');
各行の末尾にセミコロン ; があることに注目してください。これはプログラミング言語によくある文法で、古くは1950年代中ごろに開発されたALGOLまで遡ります。
destroy.js.erb
ファイルの方も同様です (リスト 14.39)。
app/views/relationships/destroy.js.erb
$("#follow_form").html("<%= escape_javascript(render('users/follow')) %>");
$("#followers").html('<%= @user.followers.count %>');
これらのコードにより、プロフィールページを更新させずにフォローとフォロー解除ができるようになったはずです。
演習
- ブラウザから /users/2 にアクセスし、うまく動いているかどうか確認してみましょう。
- 先ほどの演習で確認が終わったら、Railsサーバーのログを閲覧し、フォロー/フォロー解除を実行した直後のテンプレートがどうなっているか確認してみましょう。
14.2.6 フォローをテストする
フォローボタンが動くようになったので、バグを検知するためのシンプルなテストを書いていきましょう。ユーザーのフォローに対するテストでは、 /relationshipsに対してPOSTリクエストを送り、フォローされたユーザーが1人増えたことをチェックします。具体的なコードは次のとおりです。
assert_difference '@user.following.count', 1 do
post relationships_path, params: { followed_id: @other.id }
end
これは標準的なフォローに対するテストではありますが、Ajax版もやり方は大体同じです。Ajaxのテストでは、xhr :true
オプションを使うようにするだけです。
assert_difference '@user.following.count', 1 do
post relationships_path, params: { followed_id: @other.id }, xhr: true
end
ここで使っているxhr
(XmlHttpRequest) というオプションをtrue
に設定すると、Ajaxでリクエストを発行するように変わります。したがって、リスト 14.36のrespond_to
では、JavaScriptに対応した行が実行されるようになります。
また、ユーザーをフォロー解除するときも構造はほとんど同じで、post
メソッドをdelete
メソッドに置き換えてテストします。つまり、そのユーザーのidとリレーションシップのidを使ってDELETEリクエストを送信し、フォローしている数が1つ減ることを確認します。したがって、実際に加えるテストは、
assert_difference '@user.following.count', -1 do
delete relationship_path(relationship)
end
上の従来どおりのテストと、下のAjax用のテストの2つになります。
assert_difference '@user.following.count', -1 do
delete relationship_path(relationship), xhr: true
end
これらのテストをまとめた結果を、リスト 14.40に示します。
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, params: { followed_id: @other.id }
end
end
test "should follow a user with Ajax" do
assert_difference '@user.following.count', 1 do
post relationships_path, xhr: true, params: { followed_id: @other.id }
end
end
test "should unfollow a user the standard way" do
@user.follow(@other)
relationship = @user.active_relationships.find_by(followed_id: @other.id)
assert_difference '@user.following.count', -1 do
delete relationship_path(relationship)
end
end
test "should unfollow a user with Ajax" do
@user.follow(@other)
relationship = @user.active_relationships.find_by(followed_id: @other.id)
assert_difference '@user.following.count', -1 do
delete relationship_path(relationship), xhr: true
end
end
end
この時点では、テストは greenになるはずです。
$ rails test
14.3 ステータスフィード
ついに、サンプルアプリケーションの山頂が目の前に現れました。最後の難関、ステータスフィードの実装に取りかかりましょう。この節で扱われている内容は、本書の中でも最も高度なものです。完全なステータスフィードは、13.3.3で扱ったプロトフィードをベースにします。現在のユーザーにフォローされているユーザーのマイクロポストの配列を作成し、現在のユーザー自身のマイクロポストと合わせて表示します。このセクションを通して、複雑さを増したフィードの実装に進んでいきます。これを実現するためには、RailsとRubyの高度な機能の他に、SQLプログラミングの技術も必要です。
手強い課題に挑むのですから、ここで実装すべき内容を慎重に見直すことが重要です。図 14.5でお見せしたステータスフィードの最終形を図 14.21に再度掲載します。
14.3.1 動機と計画
ステータスフィードの基本的なアイデアはシンプルです。図 14.22に、microposts
のサンプルデータ付きのデータモデルとその結果を示します。図の矢印で示されているように、この目的は、現在のユーザーによってフォローされているユーザーに対応するユーザーidを持つマイクロポストを取り出し、同時に現在のユーザー自身のマイクロポストも一緒に取り出すことです。
どのようにフィードを実装するのかはまだ明確ではありませんが、テストについてはやや明確そうなので、(コラム 3.3のガイドラインに従って) まずはテストから書いていくことにします。このテストで重要なことは、フィードに必要な3つの条件を満たすことです。具体的には、1) フォローしているユーザーのマイクロポストがフィードに含まれていること。2) 自分自身のマイクロポストもフィードに含まれていること。3) フォローしていないユーザーのマイクロポストがフィードに含まれていないこと、の3つです。
詳しくはリスト 14.28で見ていきますが、まずはMichaelがLanaをフォローしていて、Archerをフォローしていないという状況を作ってみましょう。この状況のMichaelのフィードでは、Lanaと自分自身の投稿が見えていて、Archerの投稿は見えないことになります (リスト 10.47とリスト 13.53のfixtureファイルが参考になります)。先ほどの3つの条件をアサーションに変換して、Userモデル (リスト 13.46) にfeed
メソッドがあることに注意しながら、更新したUserモデルに対するテストを書いてみましょう。結果をリスト 14.42に示します。
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になるはずです。
$ rails test
14.3.2 フィードを初めて実装する
ステータスフィードに対する要件定義はリスト 14.42のテストで明確になったので (つまりこのテストにパスすれば良いので)、早速フィードの実装に着手してみましょう。最終的なフィードの実装はやや込み入っているため、細かい部品を1つずつ確かめながら導入していきます。最初に、このフィードで必要なクエリについて考えましょう。ここで必要なのは、microposts
テーブルから、あるユーザー (つまり自分自身) がフォローしているユーザーに対応するidを持つマイクロポストをすべて選択 (select) することです。このクエリを模式的に書くと次のようになります。
SELECT * FROM microposts
WHERE user_id IN (<list of ids>) OR user_id = <user id>
上のコードを書く際に、SQLがIN
というキーワードをサポートしていることを前提にしています (大丈夫、実際にサポートされています)。このキーワードを使うことで、idの集合の内包 (set inclusion) に対してテストを行えます。
13.3.3のプロトフィードでは、上のような選択を行うためにActive Recordのwhere
メソッドを使っていることを思い出してください (リスト 13.46)。このときに選択すべき対象はシンプルで、現在のユーザーに対応するユーザー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"]
上に示したような状況では、各要素に対して同じメソッドが実行されます。これは非常によく使われる方法であり、次のようにアンパサンド (Ampersand) &
と、メソッドに対応するシンボルを使った短縮表記 (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)
=> [3, 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
=> [3, 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
メソッドは、has_many :following
の関連付けをしたときにActive Recordが自動生成したものです (リスト 14.8)。これにより、user.following
コレクションに対応するidを得るためには、関連付けの名前の末尾に_ids
を付け足すだけで済みます。結果として、フォローしているユーザーidの文字列は、次のようにして取得することができます。
>> User.first.following_ids.join(', ')
=> "3, 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)
というコードが無事に動きました! 作成したコードをリスト 14.44に示します。
app/models/user.rb
class User < ApplicationRecord
.
.
.
# パスワード再設定の期限が切れている場合はtrueを返す
def password_reset_expired?
reset_sent_at < 2.hours.ago
end
# ユーザーのステータスフィードを返す
def feed
Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)
end
# ユーザーをフォローする
def follow(other_user)
following << other_user
end
.
.
.
end
これでテストは greenになるはずです。
$ rails test
いくつかのアプリケーションにおいては、この初期実装だけで目的が達成され、十分に思えるかもしれません。しかしリスト 14.44にはまだ足りないものがあります。それが何なのか、次に進む前に考えてみてください (ヒント: フォローしているユーザーが5,000人もいたらどうなるでしょうか?)。
演習
- リスト 14.44において、現在のユーザー自身の投稿を含めないようにするにはどうすれば良いでしょうか? また、そのような変更を加えると、リスト 14.42のどのテストが失敗するでしょうか?
- リスト 14.44において、フォローしているユーザーの投稿を含めないようにするにはどうすれば良いでしょうか? また、そのような変更を加えると、リスト 14.42のどのテストが失敗するでしょうか?
- リスト 14.44において、フォローしていないユーザーの投稿を含めるためにはどうすれば良いでしょうか? また、そのような変更を加えると、リスト 14.42のどのテストが失敗するでしょうか? ヒント: 自分自身とフォローしているユーザー、そしてそれ以外という集合は、いったいどういった集合を表すのか考えてみてください。
14.3.3 サブセレクト
先ほどのヒントで察した人もいると思いますが、14.3.2のフィードの実装は、投稿されたマイクロポストの数が膨大になったときにうまくスケールしません。つまり、フォローしているユーザーが5,000人程度になるとWebサービス全体が遅くなる可能性があります。この節では、フォローしているユーザー数に応じてスケールできるように、ステータスフィードを改善していきましょう。
14.3.2で示したコードの問題点は、following_ids
でフォローしているすべてのユーザーをデータベースに問い合わせし、さらに、フォローしているユーザーの完全な配列を作るために再度データベースに問い合わせしている点です。リスト 14.44の条件では、集合に内包されているかどうかだけしかチェックされていないため、この部分はもっと効率的なコードに置き換えられるはずです。また、SQLは本来このような集合の操作に最適化されています。実際、このような問題は、SQLのサブセレクト (subselect) を使うと解決できます。
まずはリスト 14.46でコードを若干修正し、フィードをリファクタリングすることから始めましょう。
where
メソッド内の変数に、キーと値のペアを使う green app/models/user.rb
class User < ApplicationRecord
.
.
.
# ユーザーのステータスフィードを返す
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
がフォローしているユーザーすべてを選択する」というSQLを既存のSQLに内包させる形になり、結果としてSQLは次のようになります。
SELECT * FROM microposts
WHERE user_id IN (SELECT followed_id FROM relationships
WHERE follower_id = 1)
OR user_id = 1
このサブセレクトは集合のロジックを (Railsではなく) データベース内に保存するので、より効率的にデータを取得することができます。
これで基礎を固めることができましたので、リスト 14.47のようにもっと効率的なフィードを実装する準備ができました。(ここに記述されているコードは生のSQLを表す文字列であり、following_ids
という文字列はエスケープされているのではなく、見やすさのために式展開しているだけだという点に注意してください。)
app/models/user.rb
class User < ApplicationRecord
.
.
.
# ユーザーのステータスフィードを返す
def feed
following_ids = "SELECT followed_id FROM relationships
WHERE follower_id = :user_id"
Micropost.where("user_id IN (#{following_ids})
OR user_id = :user_id", user_id: id)
end
.
.
.
end
このコードはRailsとRubyとSQLのコードが複雑に絡み合っていて厄介ですが、ちゃんと動作します。
$ rails test
もちろん、サブセレクトを使えばいくらでもスケールできるなどということはありません。大規模なWebサービスでは、バックグラウンド処理を使ってフィードを非同期で生成するなどのさらなる改善が必要でしょう。ただし、Webサービスをスケールさせる技術は非常に高度かつデリケートな問題なので、本書ではここまでの改善で止めておきます。
リスト 14.47をもって、ステータスフィードの実装は完了です。13.3.3でHomeページには既にフィードを追加していたことを思い出してください。第13章のときはただのプロトタイプでしたが (図 13.14)、リスト 14.47の実装によって、Homeページで完全なフィードを表示できていることがわかります (図 14.23)。
この時点で、masterブランチに変更を取り込む準備ができました。
$ rails 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 rails db:migrate
$ heroku run rails db:seed
本番環境で動作するステータスフィードは次のようになります (図 14.24)。
演習
- Homeページで表示される1ページ目のフィードに対して、統合テストを書いてみましょう。リスト 14.49はそのテンプレートです。
- リスト 14.49のコードでは、期待されるHTMLを
CGI.escapeHTML
メソッドでエスケープしています (このメソッドは11.2.3で扱ったCGI.escape
と同じ用途です)。このコードでは、なぜHTMLをエスケープさせる必要があったのでしょうか? 考えてみてください。ヒント: 試しにエスケープ処理を外して、得られるHTMLの内容を注意深く調べてください。マイクロポストの内容が何かおかしいはずです。また、ターミナルの検索機能 (Cmd-FもしくはCtrl-F) を使って「sorry」を探すと原因の究明に役立つはずです。
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((コードを書き込む)), (コードを書き込む)
end
end
end
14.4 最後に
ステータスフィードが追加され、Railsチュートリアルのサンプルアプリケーションがとうとう完成しました。このサンプルアプリケーションには、Railsの主要な機能 (モデル、ビュー、コントローラ、テンプレート、パーシャル、beforeフィルター、バリデーション、コールバック、has_many
/belongs_to
/has_many through
関連付け、セキュリティ、テスティング、デプロイ) が多数含まれています。
これだけでもかなりの量ですが、Web開発について学ぶべきことはまだまだたくさんあります。今後の学習の手始めとするために、この節ではより踏み込んだ学習方法を紹介します。
14.4.1 サンプルアプリケーションの機能を拡張する
答えのない課題に挑戦することは、より実践的な知識の獲得にもつながります。そこで本セクションでは、完成したSample Appをさらに拡張させるアイデア(拡張課題)をいくつかご紹介します。それぞれ独立しているので、簡単そうな課題や興味のある課題から取り掛かってみても良いでしょう。ここで試行錯誤した経験は、Railsチュートリアルから巣立って、自分のオリジナルなWebアプリケーションを作るときにも役立つことでしょう。
しかしいざ実装し始めてみると、全く手が動かなくかもしれません。それも当然です。「新しいことに挑戦する」というのは真っ白なキャンバスに絵を描き始めるようなもので、慣れるまでは気後れしてしまいがちです。そこで、ささやかながら2つほどアドバイスをしてみたいと思います。
- 実装していて困ったら、リファレンスを読む習慣を身につけましょう。例えばRailsならRailsガイドやRails APIをチェックしてみてください。読み物ガイドにある「リファレンスを読めるようになりたい」も役立ちます。
- できるだけ念入りに検索し、調べたいトピックに関する情報を探してみましょう。google.com から英語で検索したり、英語の記事をDeepL公式Chrome拡張で読むのもオススメです。Webアプリケーションの開発には常に困難がつきまといます。世界中から情報を集め、取捨選択し、学びとるスキルが熟練に近づく道です。
以下の拡張課題はどれも難易度が高いので、ヒントも書いておきました。ただしヒントがあったとしても、これまでの演習の中で最も難易度が高いことには変わりません。相当頑張ったにもかかわらず挫折することも当然あると思いますが、どうか落ち込まないでください。
時間にも限りがあるため個別サポートはできそうにありませんが、経験者の体験談や実装例などをブログ記事やインタビュー動画などで紹介しています。よければ @RailsTutorialJP やnoteマガジン 、YouTube動画をチェックしてみてください。
それでは、Good Luck!
Ruby/Railsのバージョンを上げてみよう
1.3.1で触れたように、RailsチュートリアルのサンプルアプリケーションではGemfile
でバージョンを固定しています。これは読者が自力で解決しづらいエラーに遭遇しないようにするための工夫ですが、実際のプロダクト開発ではこまめにアップグレードするのがオススメです。
例えば最新版にすることでパフォーマンスが大きく改善したり、逆に、サポート対象外となっているバージョンを使い続けることでWebサービスに脆弱性が生まれてしまう恐れもあります。(参考記事: RailsエンジニアのためのWebセキュリティ入門)
Ruby/Railsのコミュニティでは様々な開発が活発に行われていて、皆さんが本書を読んでいる今このときにも、RubyやRailsのエコシステムは更新され続けています。今後のプロダクト開発の実践的な演習として、RubyやRailsのバージョンを上げてみましょう。
Rubyはバージョン間の互換性が高いためそこまで苦労しないと思いますが、Railsは少々大変かもしれません。Railsガイドに『Railsアップグレードガイド』があるので、この拡張課題に取り組むときはぜひ参考にしてみてください。
もう1つのページネーション
Sample Appでは「will_paginate gem」を使ってページネーションを実装しましたが、Rubyのエコシステムは十分に大きく、他にも魅力的なgemがあります。ページネーションを実現できるもう1つのライブラリ「kaminari gem」を使って、既存のライブラリ「will_paginate gem」を置き換えてみましょう。
(ヒント: 同じページネーションでもgem毎に使い方が異なります。まずはそれぞれのREADMEを読み、メソッドの呼び出し方やパラメーターの渡し方などを確認しましょう。その後、gemを置き換え、Sample App内で変更すべき箇所を書き換えてみましょう)
もう1つのテストフレームワーク
Railsチュートリアルでは「minitest gem」を使ってテストの基本や、テスト駆動開発の流れを押さえました。ただしテストの世界も奥が深く、minitest以外のテストフレームワーク(Testing Framework)もあります。テストの基本が掴めてきたら、別のテストフレームワーク「RSpec」でもSample Appのテストを書いてみましょう(参考:minitestとRSpecの比較動画)。
RSpecが採用している手法(Behaviour Driven Development)やRubyの高度な文法(ブロック付きメソッドなど)を押さえておくと、RSpecのコードの理解も捗ります。最初はやや難解に見えるかもしれませんが、その分、実戦では大いに役立つツールでもあるので、テストの世界をもう一歩踏み込んでみたい方はぜひチャレンジしてみてください。
(ヒント:RSpecとminitestは共存できるため、minitestのコード削除は不要です。RSpecに関する情報は読み物ガイドをご参照ください)
返信機能
Twitterには、マイクロポスト入力中に@記号に続けてユーザーのログイン名を入力するとそのユーザーに返信できる機能があります。このポストは、宛先のユーザーのフィードと、自分をフォローしているユーザーにのみ表示されます。この返信機能の簡単なバージョンを実装してみましょう。具体的には、@replyは受信者のフィードと送信者のフィードにのみ表示されるようにします。これを実装するには、microposts
テーブルのin_reply_to
カラムと、追加のincluding_replies
スコープをMicropostモデルに追加する必要があると思います。スコープの詳細については、RailsガイドのActive Record クエリインターフェイスを参照してください。
このサンプルアプリケーションではユーザー名が重なり得るので、ユーザー名を一意に表す方法も考えなければならないでしょう。1つの方法は、idと名前を組み合わせて@1-michael-hartl
のようにすることです。もう1つの方法は、ユーザー登録の項目に一意のユーザー名を追加し、@replyで使えるようにすることです。
フォロワーの通知
ユーザーに新しくフォロワーが増えたときにメールで通知する機能を実装してみましょう。続いて、メールでの通知機能をオプションとして選択可能にし、不要な場合は通知をオフにできるようにしてみましょう。メール周りで分からないことがあったら、RailsガイドのAction Mailerの基礎にヒントがないか調べてみましょう。
検索機能
現在のサンプルアプリケーションには、ユーザーの一覧ページを端から探す、もしくは他のユーザーのフィードを表示する以外に他のユーザーを検索する手段がありません。この点を強化するために、検索機能を実装してください。続いて、マイクロポストを検索する機能も追加してください。(ヒント: まずは「Rails 検索」などで情報収集してみましょう)
他の拡張機能
上記以外にもいくつかの拡張例があります。他の拡張例については完走者向けコンテンツまとめページをご参照ください!
14.4.2 読み物ガイド
Ruby/Rails関連の素晴らしい書籍や動画はたくさんあります。本書を完走した皆さんにおいては、そのほとんどを理解できるようになっているでしょう。本書をもう1周する場合も含め、本セクションではRuby/Railsをさらに深く学ぶ方法についていくつかご紹介します。
- 対談シリーズ『Railsチュートリアル完走者に聴く』(YouTube)
Railsチュートリアル完走者に直接インタビューするYouTube対談動画です。『3ヶ月でWebサービスを開発した話』や『医者からエンジニアになった話』などの事例を公開しているので、ぜひお役立てください。
- Progate Journey
対話的に学べるオンラインのプログラミング学習サービス『Progate』が提供する学習ロードマップです。それぞれの目的に合わせて、より実践的なWeb技術を1つずつ紹介しています。
- RubyとRailsの学習ガイド
1時間ほどで読める学習ガイドブックです。Webではどんな技術が使われているのか、いま学んでいることはWeb技術全体の中でどのような位置づけなのか、次に学ぶ候補としてどんな技術があるのか。Web技術を学び、レベルアップしていく『冒険の旅』のお供にご活用ください。
- Railsチュートリアル実践入門シリーズ
Railsチュートリアルを読んで『ローカル開発に挑戦したい』『フロントエンドの基本も学びたい』と感じた方にオススメのテーマ別チュートリアルです。テキストエディタ編、コマンドライン編、Git/GitHub編、HTML編、CSS & Design編、JavaScript編など、それぞれのトピックをチュートリアル形式で学べます。
- Railsチュートリアル解説動画 + AIサポート + トレーニング
- 質問対応サポート付き解説動画【提供: ShareWis】
現役Rubyエンジニアのサポート付きで学べる、Railsチュートリアル解説動画の質問対応付きサービスです。Railsチュートリアルでは章を進めるにつれて徐々に難しくなっていますが、質問対応サポートを受けながら効率的に復習できます。『途中から十分に理解できなかった』『もっと早く学びたい』と感じている方にオススメです。
- Rubyの公式リファレンスが読めるようになる本
Rubyの公式リファレンスの読み方を解説している本です。『公式リファレンスを見ても変な記号や英語がたくさん出てきて全然意味がわからない・・・』という方に特にオススメの内容となっています。ユースケース別にまとめられているため、『わからない用語を調べたい』『記号の意味を調べたい』などの場面に合わせてご活用いただけます。
- Rubyist Magazine(通称「るびま」)
日本Rubyの会の有志によって発行されている、無料のWebマガジンです。最新のRuby/Railsに関する技術動向や、全国のRuby/Railsコミュニティの活動などが掲載されています。First Step Rubyでは、Rubyに関するお役立ち情報がまとめられています。
- Everyday Rails - RSpecによるRailsテスト入門
(なんと!) Railsチュートリアルの完走者を対象としたテストの入門書籍です。Railsチュートリアルでは学習コストを小さく抑えるために標準のminitestを使いましたが、プロのRailsエンジニアはRSpecでテストを書くことも多いです。信頼性の高い実践的なテストコードを書いてみたい方にオススメです。
- プロを目指す人のためのRuby入門 - 言語仕様からテスト駆動開発・デバッグ技法まで
- 現場で使える Ruby on Rails 速習実践ガイド
- パーフェクトRuby on Rails
- RailsとReactでUberEats風SPAアプリケーションをつくってみよう!
Railsチュートリアルを完走したレベルの人で、より実践的なフロントエンド技術 (React) を学びたい人にオススメの教材です。『Rails APIモード』と呼ばれる機能を使って、Railsとフロントエンド技術 (React) の組み合わせ方やデバッグ時の注意点、Chrome DevToolsの見方などが習得できます。いくつかの章は無料で読めるので、まずは試し読みからぜひ!
- Ruby on Railsガイド
14.4.3 本章のまとめ
has_many :through
を使うと、複雑なデータ関係をモデリングできるhas_many
メソッドには、クラス名や外部キーなど、いくつものオプションを渡すことができる- 適切なクラス名と外部キーと一緒に
has_many
/has_many :through
を使うことで、能動的関係 (フォローする) や受動的関係 (フォローされる) がモデリングできた - ルーティングは、ネストさせて使うことができる
where
メソッドを使うと、柔軟で強力なデータベースへの問い合わせが作成できる- Railsは (必要に応じて) 低級なSQLクエリを呼び出すことができる
- 本書で学んだすべてを駆使することで、フォローしているユーザーのマイクロポスト一覧をステータスフィードに表示させることができた
- 本書の完走者を対象としたコンテンツが「読み物ガイド」でまとめられている
14.4.4 あとがき
Railsチュートリアルをお読みいただきありがとうございました! 本サイトを運営をしているYassLab株式会社の安川です。長い長いRailsチュートリアルを読み切った感想はいかがでしょうか?
簡単だったと感じる方も、演習を飛ばしながら読んでみたという方もいらっしゃるかと思います。聞くところによると、まずはクラウドIDEで1周してみて、2周目または3周目からはローカル環境でチャレンジしてみる人が多いようですね。他にも、読み物ガイドにある完走者向けコンテンツで学ぶ人や、Sample Appの拡張課題に挑戦しながら学ぶ人、新しいWebサービスを開発しながら学ぶ人もいるようです。
いずれのケースにおいても、本書をリファレンスとして活用して頂いているという声を伺っています。Railsチュートリアルでは解説動画だけではなく電子書籍も提供していて、電子書籍版ではすべての章を横断的に検索することができます。『Railsチュートリアルを読み返しながらWebサービスを開発したい』といった場面で特に便利なので、開発時の参考書としてお役に立てば嬉しいです。
電子書籍や解説動画などで得られた売り上げは、Railsチュートリアルを継続的に更新するために活用させていただきます。ここまでの長い道のりで皆さまが様々な知識を身に付けられたように、これからプログラミングを学び始める方々にとっても役立つコンテンツであり続けたいと考えていますので、よければぜひご検討していただけると嬉しいです。
安川 要平(Railsチュートリアル共同発起人)
has_many
に渡された引数をclassify
メソッドを使ってクラス名に変換しています。例えば、このメソッドに"foo_bars"
を渡すと"FooBar"
に変換されます。underscore
メソッドを使ってクラス名をidに変換しています。例えば、"FooBar".underscore
を実行すると"foo_bar"
に変換されます。したがって、 FooBar
オブジェクトの外部キーはfoo_bar_id
になるでしょうeach
メソッドを実装していることです。このメソッドはコレクションを列挙します。
Railsチュートリアルは YassLab 社によって運営されています。
コンテンツを継続的に提供するため、書籍・動画・質問対応サービスなどもご検討していただけると嬉しいです。
研修支援や教材連携にも対応しています。note マガジンや YouTube チャンネルも始めたので、よければぜひ遊びに来てください!