- 投稿日:2019-02-09T22:43:06+09:00
gitを使っててファイルが消えたときはまずreflog
概要
備忘録を共有します。
ブランチからmasterブランチへ移動できていない状況で、merge
->add
->commit
->push
したら、ファイルが消えたように見えました。対処法
git reflog
でcommitやpushの状況確認します。$ git reflog c57e199 (HEAD -> master, origin/master) HEAD@{0}: commit: v17 commit 3ef1bb8 (v16) HEAD@{1}: checkout: moving from v17 to master e1a7554 (v17) HEAD@{2}: commit: v17 mod commit 0ffbfa4 HEAD@{3}: commit: v17 commit a87f8e1 (origin/v17) HEAD@{4}: commit: v17.1 3ef1bb8 (v16) HEAD@{5}: checkout: moving from master to v17 3ef1bb8 (v16) HEAD@{6}: checkout: moving from master to master 3ef1bb8 (v16) HEAD@{7}: merge v16: Fast-forward ...今回の場合、
0ffbfa4
までは正常なので、そこに戻ればよいです。
以下のようなよくわからないエラーを吐き出しましたが、ファイルは回復しました。$ git checkout 0ffbfa4 README.md: needs merge error: you need to resolve your current index first
参考文献
- 投稿日:2019-02-09T21:07:10+09:00
Git で差分確認するときに利用頻度が高いオプション
Git を使っていると、差分をチェックする機会には事欠かないはず(少なくとも私はそう)。
普通に
git diff
とだけすると、
- ちょっと構造だけが変わったようなのは、差分量が無駄に多く見える
- 要は関連箇所のインデントが変わることで全部差分に見える
- コミット直前、実際に反映される予定の差分は見れない
- バージョン管理されていないファイルの差分は確認できない
- 差分のあったファイルだけを把握するには少々向かない
- いやまぁそれは
git status
でも使えばいいやんという感じですが等と、ちょくちょく使い勝手が悪い感じがするというか。個人的にはコミット前のチェックができないのは辛すぎる。
まぁ別にコミットミスった後の微調整反映についてはgit commit --amend
なりgit rebase -i
なりで適宜すりゃ良いんですけどただ、当たり前ながら、その辺の要求に対しては適切に利用可能なオプションがあるので、別に諦める必要はないです。やりたいことをスマートに、 提供された機能で 解決するのが技能者の本懐。作り込むなら最低限に。
git diff -w
: ホワイトスペース差分の無視ホワイトスペース差分、つまり「空行の追加 / 削除」とか「半角スペースのみの変更」とかを冗長視した差分確認ができます。リファクタリングで「内部挙動のインデントだけがちょっと変わった」みたいな時に重宝します。
Git と直接関係があるわけではないですが、 GitHub で差分を見るときは、URLの末尾に
?w=1
と付与してやると同じように差分が見れるので、たまに便利。他にも色々ありそうな気はしますが、あまり把握してません。そもそも差分チェック自体を行う時にいちいち指定するのは-w
くらいな気がするし
git diff --staged
: ステージング差分の確認ステージング差分、つまり「コミット時に実際に反映される」差分の確認ができます。コミットそのものの妥当性を事前検証(最終チェックか)する時に非常に重宝します。
私は直前に3回くらい見ます
コミット予定の差分が見れるということは、 バージョン管理される前のファイルの追加についても捕捉することが出来る ので、事前に追加予定の対象ファイルをgit add
しておくことによって、そのファイルも単に同コマンドで「シームレスに変更差分として認識できるようになる」というメリットもあります。ちなみに
git diff --cached
も同じ作用を持ち(エイリアス)、私個人は--cached
の方をよく使っています。その辺は各人の運指とかの好みとかで好きな方を使うといいのではないでしょうか。
git diff --name-only
: 変更があったファイル名称だけ確認文字通り、差分のあるファイルの名前だけが出力されます。正確には相対パスか。
…とだけ言うと「いや、何に使うねん」という話なのですが、コマンドを連結して色々やるような場面では「名称だけが取得できると便利」という場面もあるので、知っておくと便利な場合がたまにあります。# 例えば Guard に更新を捕捉させれば、検証の自動実行を発火するように仕向けられる環境なら # 「コミット直前の反映対象」に対して touch を実行するとか git diff --staged --name-only | xargs touch # 「結合要求先と差分があるファイル群」に対して touch を実行するとか git diff --name-only origin/develop | xargs touch
こいつずっとtouch
ばっかやってんな
というかこれは実際に業務で「出来んかな」ってなってただけなんですけどね。実際できました。今回は検証向けに
touch
がやりたかっただけで、後者は理想的にはorigin/develop
みたいな指定を抜きに upstream の設定対象を起点にしてチェックできると良かったのですが、多分その辺はgit diff
じゃなくて別のコマンドを使うべきなんだろうなと予想しています。というか本当に困ったら
git diff --help
を見ようまぁ実際のところ、こういうので困ったときは、普通に公式のドキュメントを閲覧するのが一番確実なんですけどね。差分確認周りは
git diff --help
で見れますし。
- 投稿日:2019-02-09T16:06:16+09:00
【Git for Windows】Gitをアップデートする
- 投稿日:2019-02-09T12:29:14+09:00
xonshでpromptのgitstatusの表示をカスタマイズする
xonshでは、
{gitstatus}
を含んだプロンプトの書式を$PROMPT
に格納することで、gitの状態をプロンプトに追加できる。https://xon.sh/tutorial.html#customizing-the-prompt
この機能のマークや色などの書式変更は、
$XONSH_GITSTATUS_*
を変更することで実現できる。https://xon.sh/envvars.html#xonsh-gitstatus
それ以上のカスタマイズは、promptで実行する関数は、
$PROMPT_FIELDS["some_command"] = lambda: xxx
と関数を定義し、{some_command}
を書式に追加する必要がある。ちょっとした改修であれば、元の
{gitstatus}
のコードは以下にあるため、これをコピーして(xonsh:2条項BSDライセンス)流用すると簡単である。https://github.com/xonsh/xonsh/blob/master/xonsh/prompt/gitstatus.py
私の場合、masterの時には気づくようにしたいため、ブランチに絵文字を追加した。
def gitstatus_prompt() ~ ret = "" if s.branch == "master": ret += "?" ~これを、
$PROMPT_FIELDS
に追加し、書式に関数を追加すれば良い。$PROMPT_FIELDS["git"] = gitstatus_promptやはりPythonでShellを拡張できるxonshは便利です!!
- 投稿日:2019-02-09T05:12:36+09:00
Ruby on Rails チュートリアル 第14章 データモデルの関連付け(フォロー フォロー解除)フィードの実装など 演習 解答
著者略歴
YUUKI
ポートフォリオサイト:Pooks
RailsTutorial2周目14章 ユーザーをフォローする
この章では、他のユーザーをフォローしたり、フォロー解除したりするソーシャル的な仕組みと、
フォローしているユーザーの投稿をステータスフィード(いわゆるタイムライン)
に表示する仕組みを追加する。そのために、まずはユーザー間の関係性をどうモデリングするかについて学ぶ。
その後、モデリング結果に対応するWebインターフェースを実装していく。
Webインターフェースの例としてAjaxについても後に詳解する。
最後に、ステータスフィードの完成版を実装する。
この最終章では、本書の中で最も難易度の高い手法をいくつか使っている。
その中には、ステータスフィードの作成のためにRuby/SQLを騙すテクニックも含まれる。この章の例全体にわたって、これまでよりも複雑なデータモデルを使います。
ここで学んだデータモデルは、今後自分用のWebアプリケーションを開発するときに必ず役に立つ。
この章で学ぶことは今まで最も難易度が高いため、
コードを書く前に一旦インターフェースの流れを理解する。モックアップはこちら
出典:図 14.3: ユーザーのプロフィール画面に [Follow] ボタンが表示されている
出典:図 14.4: プロフィールに [Unfollow] ボタンが表示され、フォロワーのカウントが1つ増えた
出典:図 14.5: Homeページにステータスフィードが表示され、フォローのカウントが1増えた
ページ操作の全体的なフローは次の通りとなる。
①あるユーザーは(John Calvin)は自分のプロフィールページを最初に表示する
②フォローするユーザーを選択するためにUsersページに移動する
③Calvinは2番目のThomas Hobbesを表示し、Followボタンを押してフォローする
④Homeページに戻ると、followingカウントが1人増える
⑤Hobbesのマイクロポストがステータスフィードに表示されるになる14.1 Relationshipモデル
ユーザーをフォローする機能を実装する第一歩は、データモデルを構成する。
今回のデータモデルは単純ではなく、
has_many(1対多)の関連付けを用いて
「1人のユーザーが複数のユーザーをhas_manyとしてフォローし、1人のユーザーに複数のフォロワーがいることをhas_manyで表す」
といった方法でも実装できる。しかし、この方法ではたちまち壁に突き当たってしまう。
これを解決する為の
has_many_through
についても解説する。Gitユーザーはこれまで同様新しいトピックブランチを作成する
$ git checkout -b following-users14.1.1 データモデルの問題(および解決策)
ユーザーをフォローするデータモデル構成のための第一歩として、典型的な場合を検討してみる。
あるユーザーが、別のユーザーをフォローしているところを考えてみる。
- CavinはHobbesをフォローしている。 - 逆から見ればHobbesはCalvinからフォローされている。 - CalvinはHobbesから見ればフォロワーであり、Calvinがhobbesをフォローしたことになる。 - Railsにおけるデフォルトの複数形の慣習に従えば、あるユーザーをフォローしているすべてのユーザーの集合(フォローされてる人数)はfollowersとなり、user.followersはそれらのユーザーの配列を表すことになる。 - しかし、これを逆で考えた場合(フォローしている人数)、英語の文法的`followeds`となり、英語の文法からも外れてしまう - そこで、Railsではフォロー人数を`following`という呼称を採用している。 - したがって、あるユーザーがフォローしている全てのユーザーの集合は`calvin.following`となるつまり、
followers
がフォロワー人数で、following
がフォロー人数を表すデータの表となる。まずは
following
テーブル(フォロー人数)を見ていく。followingテーブルとhas_many関連付けを使って、フオローしているユーザーのモデリングができる。
user.following
はユーザーの集合でなければならないため、followingテーブルのそれぞれの行は、followed_idで識別可能なユーザーでなければならない。さらに、それぞれの行はユーザーなので、これらのユーザーに名前(name)やパスワード(password)などの属性を追加する。
出典:図 14.6: フォローしているユーザーの素朴な実装例
上記のデータモデルの問題点は非常に無駄が多いこと。
各行には、フォローしているユーザーのidのみならず、名前やメールアドレスまである。
これらはいずれも
usersテーブル
に既にあるものばかり。さらによくないことに、followersの方をモデリングする時にも、
同じくらい無駄の多いfollowersテーブルを別に作成しなければならなくなってしまう。結論としては、このデータモデルはメンテナンスの観点から見て悪夢。
というのも、ユーザー名を変更するたびに、usersテーブルのそのレコードだけでなく、followingテーブルとfollowersテーブルの両方について、そのユーザーを含む全ての行を更新しなければならなくなる。この問題の根本は、必要な抽象化を行なっていないことである。
正しいモデルを見つけ出す方法の1つは、
Webアプリケーションにおけるfollowingの動作をどのように実装するかをじっくり考えることにある。7章において、RESTアーキテクチャは、作成されたり削除されたりするリソースに関連していたことを思い出してみる。
ここで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に対して受動的関係を持っていることになる。
まずは、フォローしているユーザーを生成するために、能動的関係に焦点を当てていく。
(受動的関係についてはのちに考える)先ほどのfollowingデータモデルは実装のヒントにして考える。
フォローしているユーザーは
followed_id
があれば識別することができるので、先ほどのfollowing
テーブルをactive_relationships
(能動的関係)テーブルと見立ててみる。ただし、ユーザー情報は無駄なので、ユーザーid以外の情報は削除する。
そして、followed_idを通して、usersテーブルのフォローされているユーザーを見つけるようにする。このデータモデルを模式図にすると、以下のようになる。
出典:図 14.7: 能動的関係をとおしてフォローしているユーザーを取得する模式図
間に
active_relationships
を挟むことで、フォローとフォロワーの関係性がスムーズに繋がっている。能動的関係も受動的関係も、最終的にはデータベースの同じテーブルを使うことになる。
したがって、テーブル名にはこの「関係」を表す「relationships」を使う。
モデル名はRailsの慣習にならって、Relationshipとする。
作成したRelationshipデータモデルを以下に示す。1つのrelationshipsテーブルを使って2つのモデル(能動的関係と受動的関係)をシミュレートする方法については後に説明する。
このデータモデルを実装するために、まずは上記のデータモデルに対応したマイグレーションを生成する。
$ rails g model Relationship follower_id:integer followed_id:integerこのリレーションシップは今後
follower_id
とfollowed_id
で頻繁に検索することになるので、
それぞれのカラムインデックスを追加する。[timestamp]_create_relationships.rbclass 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複合キーインデックスで、
follower_id
とfollowed_id
の組み合わせが必ずユニークであることを保証する仕組みを作っている。これにより、あるユーザーが同じユーザーを2回以上フォローすることを防いでいる。
もちろん、このような重複が起きないよう、インタフェース側の実装でも注意を払う。
しかし、ユーザーが何らかの方法で(例えばcurlなどのコマンドラインツール)Relationshipのデータを操作するようなことも起こり得る。
そのような場合でも、一意なインデックスを追加していれば、エラーを発生させて重複を防ぐことができる。
relationshipsテーブルを作成するために、いつものようにデータベースのマイグレーションを行う。
$ rails db:migrate演習
1:id=1のユーザーに対して
user.following.map(&:id)
を実行すると、結果はどのようになるか?引数で受け取ったid=1にフォローされているユーザー(id: 2,7,10,8)のidをそれぞれ1つずつ返す
>> user.following.map(id:1) 2 7 10 82:id=2のユーザーに対して
user.following
を実行すると、結果はどうなるか?
また、同じユーザーに対してuser.following.map(&:id)
を実行すると、結果はどのようになるか?>> user.following(id:2) => [id:1,name:Michael Hartl,email:mhartl@example.com] >> user.following.map(id:2) => 114.1.2 User/Relationshipの関連付け
フォローしているユーザーとフォロワーを実装する前に、UserとRelationshipの関連付けを行う。
1人のユーザーには
has_many
(1対多)のリレーションシップがあり、このリレーションシップは2人のユーザーの間の関係なので、フォローしているユーザーとフォロワーの両方に属している。(belongs_to)マイクロポスト作成の時と同様、下記のようなユーザー関連付けのコードを使って新しいリレーションシップを作成する。
user.active_relationships.build(followed_id: ...) #user.active_relationshipsをデータモデルとして引数で受け取った値と関連付けて、カラムを生成するこの時点では、User/Micropostの関連付けのモデルのようにはならない。
1つ目の違いとして、以前、ユーザーとマイクロポストの関連付けをした時は
class User < ApplicationRecord has_many :microposts endこのように書いた。
引数の
:microposts
シンボルから、Railsはこれに対応するMicropostモデルを探し出し、見つけることができた。しかし、今回のケースで同じように書くと
has_many :active_relationshipsとなってしまい、ActiveRelationshipモデルを探してしまうので、
相互にフォローユーザーを繋ぐRelationshipモデルを見つけることができない。このため、今回のケースではRailsに探して欲しいモデルのクラス名を明示的に伝える必要がある。
2つ目の違いは、先ほどの逆のケースについて。
以前はMicropostモデルで
class Micropost < ApplicationRecord belongs_to :user endこのように書いた。
micropostsテーブルにはuser_id属性があるので、
これを辿って対応するユーザーを特定できた。DBの2つのテーブルを繋ぐとき、このようなidは外部キー(foreign key)と呼ぶ。
すなわち、Userモデルに繋げる外部キーが、Micropostモデルのuser_id属性ということ。
この外部キーの名前を使って、Railsは関連付けの推測をしている。
具体的には、Railsはデフォルトでは外部キーの名前を_idといったパターンとして理解し、
に当たる部分からクラス名(正確には小文字に変換されたクラス名)を推測する。class Micropost < ApplicationRecord belongs_to :user #Micropostはmicropostモデルのuser_id属性が外部キーと自動で推測する)ただし、マイクロポストではユーザーを例として扱ったが、
今回のケースでは
フォローしているユーザーをfollower_id
という外部キーを持って特定しなくてはならないまた、followerというクラス名は存在しないので、ここでもRailsに正しいクラス名を伝える必要が発生する。
先ほどの説明をコードにまとめると、UserとRelationshipの関連付けは以下のようになる。
user.rbclass User < ApplicationRecord # 関連付け has_many :microposts, dependent: :destroy has_many :active_relationships, class_name: "Relationship", foreign_key: "follower_id", dependent: :destroy明示的にclass名や外部キー、destroyも追加している。
(ユーザーを削除したら、ユーザーのリレーションシップも同時に削除される必要があるため)relationship.rbclass Relationship < ApplicationRecord # 1対1の関連付け belongs_to :follower, class_name: "User" belongs_to :followed, class_name: "User" endなお、followerの関連付けはまだ使わない。
(書いておくことで構造の理解の手助けになる)
user.rb
とrelationship.rb
で定義した関連付けにより、13.1で以前紹介したような多くのメソッドが使えるようになった。出典:表 14.1: ユーザーと能動的関係の関連付けによって使えるようになったメソッドのまとめ
これらのメソッドを使えば、フォロワーを返したり、フォローしているユーザーを返したりできる。
演習
1:コンソールを開き、上記表のcreateメソッドを使って、ActiveRelationshipを作ってみる。
DB上に2人以上のユーザーを用意し、最初のユーザーが2人目のユーザーをフォローしている状態を作ってみる。>> user = User.first User Load (0.2ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]] => #<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2019-01-31 11:25:14", updated_at: "2019-01-31 11:25:14", password_digest: "$2a$10$ufvz2x2ljsYgknbfQaQrNOF5uG5PP.1YP2jIXom1qCU...", remember_digest: nil, admin: true, activation_digest: "$2a$10$YtXwZx1hETpK66tpv23VJO7a47hav3sFWdwHIpfQDLy...", activated: true, activated_at: "2019-01-31 11:25:14", reset_digest: nil, reset_sent_at: nil> >> user_second = User.second User Load (0.1ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? OFFSET ? [["LIMIT", 1], ["OFFSET", 1]] => #<User id: 2, name: "Jordyn Heaney", email: "example-1@railstutorial.org", created_at: "2019-01-31 11:25:14", updated_at: "2019-01-31 11:25:14", password_digest: "$2a$10$3IGSHyPf/ofme0Fump8NF.4kP13rVb9UmiSRnNJkiLt...", remember_digest: nil, admin: false, activation_digest: "$2a$10$vJENgJrXHkTu3.d8/Bpz8OKUK.AJUW1objabSVuoWHE...", activated: true, activated_at: "2019-01-31 11:25:14", reset_digest: nil, reset_sent_at: nil> >> user.active_relationships.create(followed_id: user_second.id) (0.1ms) begin transaction User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]] User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]] SQL (2.1ms) INSERT INTO "relationships" ("follower_id", "followed_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["follower_id", 1], ["followed_id", 2], ["created_at", "2019-02-04 18:18:22.015668"], ["updated_at", "2019-02-04 18:18:22.015668"]] (6.2ms) commit transaction => #<Relationship id: 1, follower_id: 1, followed_id: 2, created_at: "2019-02-04 18:18:22", updated_at: "2019-02-04 18:18:22">2:active_relationships.followerとactive_relationships.followedの値がそれぞれ正しいことを確認。
フォローしてるのが1で、フォローされてるのが2だと確認できる。
=> #<Relationship id: 1, follower_id: 1, followed_id: 2, created_at: "2019-02-04 18:18:22", updated_at: "2019-02-04 18:18:22">14.1.3 Relationshipのバリデーション
ここでRelationshipモデルの検証を追加して完全なものにしておく。
テストコードとアプリケーションコードを作って実装していく。
ただし、User用のfixtureファイルと同じように、生成されたRelationship用のfixtureでは、
マイグレーションで制約させた一意性を満たすことができない。このままだと正しくテストを行えないので、今の時点では、
生成されたRelationship用のfixtureファイルを空にしておく。fixtures/relationships.yml# 空にする
早速、簡単なテストとバリデーションを記入する。
relationship_test.rbrequire '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 follwer_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 endrelationship.rbclass Relationship < ApplicationRecord # 1対1の関連付け belongs_to :follower, class_name: "User" belongs_to :followed, class_name: "User" validates :follower_id, presence: true validates :followed_id, presence: true endこれでテストはパスする。
$ rails t演習
1:Relationshipモデルのvalidatesをコメントアウトしてもテストが成功することを確認。
relationship.rb# validates :follower_id, presence: true # validates :followed_id, presence: true end $ rails t 3 tests, 3 assertions, 0 failures, 0 errors, 0 skipsテストが成功する理由は、Rails5だと初期の時点でバリデーションが掛かってるから。
14.1.4 フォローしているユーザー
Relationshipの関連付けの核心
following
とfollowers
に取りかかる。今回は
has_many through
を使う。1人のユーザーにはいくつもの「フォローする」「フォローされる」といった関係性がある。
この関係性を多対多と呼ぶ。デフォルトの
has_many through
という関連付けでは、
Railsはモデル名(単数形)に対応する外部キーを探す。has_many :followeds, through: :active_relationshipsRailsは
followeds
というシンボルを見て、
これをfollowed
単数形に変え、
relationships
テーブルのfollowed_id
を使って対象のユーザーを取得してくる。しかし、
user.followeds
という使い方は英語としては不適切。代わりに、
user.following
という名前を使う。そのためには、Railsのデフォルトを上書きする必要がある。
ここでは:source
パラメーターを使って、following配列の元はfollowed idの集合である
ということを明示的にRailsに伝える。user.rbclass User < ApplicationRecord # 関連付け has_many :microposts, dependent: :destroy # 1対多の関連付け has_many :active_relationships, class_name: "Relationship", foreign_key: "follower_id", dependent: :destroy has_many :following, through: :active_relationships, source: :followed上記で定義した関連付けにより、フォローしているユーザーを配列の様に扱える様になった。
例えば、
include?
メソッドを使ってフォローしているユーザーの集合を調べてみたり、
関連付けを通してオブジェクトを探しだせるようになる。user.following.include?(other_user) user.following.find(other_user)
following
で取得したオブジェクトは、配列の様に要素を追加したり削除したりすることができる。user.following << other_user user.following.delete(other_user)<<演算子で配列の最後に追記することができる。
followingメソッドで配列の様に扱えるだけでも便利だが、
Railsは単純な配列ではなく、もっと賢くこの集合を扱っている。例えば次のようなコードではfollowing.include?(other_user)フォローしている全てのユーザーをDBから取得し、その集合に対して
include?
メソッドを時実行しているように見えるが、実際はDBの中で直接比較をするように配慮している。(other_userがいるかどうかの比較を行なっている)なお、次のようなコードでは
user.microposts.countDBの中で合計を計算した方が高速になる点に注意する。
次に、followingで取得した場合をより簡単に取り扱うために、
follow
やunfollow
といった便利メソッドを追加する。これらのメソッドは、例えば
user.follow(other_user)
といった具合に使う。さらに、これに関連する
following?
論理値メソッドも追加し、あるユーザーが誰かをフォローしているかどうかを確認できるようにする。今回は、こういったメソッドはテストから先に書いていく。
と言うのも、Webインターフェイスなどで便利メソッドを使うのはまだ先なので、すぐに使える場面がなく、実装した手応えを得にくいから。一方で、Usermモデルに対するテストは書くのは簡単かつ今すぐできるので、
先に書いていく。具体的には、
following?
メソッドであるユーザーをまだフォロしていないことを確認follow
メソッドを使ってそのユーザーをフォローできたことを確認unfollow
メソッドでフォロー解除できたことを確認といった具合でテストしていく。
user_test.rbtest "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
following
による関連付けを使ってfollow
、unfollow
、following?
メソッドを実装していく。このとき、可能な限り
self
を省略している点に注目。user.rb# ユーザーをフォローする 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上記コードを追加することで、テストはパスする。
13 tests, 19 assertions, 0 failures, 0 errors, 0 skips演習
1:コンソールを開き、
user_test.rb
のコードを順々に実行してみる。>> michael = User.find(3) User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 3], ["LIMIT", 1]] => #<User id: 3, name: "Brittany Schiller", email: "example-2@railstutorial.org", created_at: "2019-01-31 11:25:15", updated_at: "2019-01-31 11:25:15", password_digest: "$2a$10$/KeYm3kd5PfnTaPWl.o/q.yf4I.Q5iXW7K3oSqywWb0...", remember_digest: nil, admin: false, activation_digest: "$2a$10$QctyRqieo7GHcwqke8DSZOm/bbSlBeJ/66VLUF6eukO...", activated: true, activated_at: "2019-01-31 11:25:14", reset_digest: nil, reset_sent_at: nil> >> archer = User.find(4) User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 4], ["LIMIT", 1]] => #<User id: 4, name: "Zula Wilkinson", email: "example-3@railstutorial.org", created_at: "2019-01-31 11:25:15", updated_at: "2019-01-31 11:25:15", password_digest: "$2a$10$16RQ.clWpUqPfhNsAvTJmekbHJiU9fNmjpJ8Ar7FYEX...", remember_digest: nil, admin: false, activation_digest: "$2a$10$TwjgTo5YKF7QJKiT1aKJ2eliFXquMvkNXLwBvqRh/jg...", activated: true, activated_at: "2019-01-31 11:25:15", reset_digest: nil, reset_sent_at: nil> >> michael.following?(archer) User Exists (0.2ms) SELECT 1 AS one FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? AND "users"."id" = ? LIMIT ? [["follower_id", 3], ["id", 4], ["LIMIT", 1]] => false >> michael.follow(archer) (0.1ms) begin transaction User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 3], ["LIMIT", 1]] SQL (5.7ms) INSERT INTO "relationships" ("follower_id", "followed_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["follower_id", 3], ["followed_id", 4], ["created_at", "2019-02-05 16:36:58.158969"], ["updated_at", "2019-02-05 16:36:58.158969"]] (9.8ms) commit transaction User Load (0.2ms) SELECT "users".* FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? LIMIT ? [["follower_id", 3], ["LIMIT", 11]] => #<ActiveRecord::Associations::CollectionProxy [#<User id: 4, name: "Zula Wilkinson", email: "example-3@railstutorial.org", created_at: "2019-01-31 11:25:15", updated_at: "2019-01-31 11:25:15", password_digest: "$2a$10$16RQ.clWpUqPfhNsAvTJmekbHJiU9fNmjpJ8Ar7FYEX...", remember_digest: nil, admin: false, activation_digest: "$2a$10$TwjgTo5YKF7QJKiT1aKJ2eliFXquMvkNXLwBvqRh/jg...", activated: true, activated_at: "2019-01-31 11:25:15", reset_digest: nil, reset_sent_at: nil>]> >> michael.following?(archer) User Exists (0.2ms) SELECT 1 AS one FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? AND "users"."id" = ? LIMIT ? [["follower_id", 3], ["id", 4], ["LIMIT", 1]] => true >> michael.unfollow(archer) Relationship Load (0.3ms) SELECT "relationships".* FROM "relationships" WHERE "relationships"."follower_id" = ? AND "relationships"."followed_id" = ? LIMIT ? [["follower_id", 3], ["followed_id", 4], ["LIMIT", 1]] (0.1ms) begin transaction SQL (3.3ms) DELETE FROM "relationships" WHERE "relationships"."id" = ? [["id", 2]] (10.6ms) commit transaction => #<Relationship id: 2, follower_id: 3, followed_id: 4, created_at: "2019-02-05 16:36:58", updated_at: "2019-02-05 16:36:58"> >> michael.following?(archer) User Exists (0.3ms) SELECT 1 AS one FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? AND "users"."id" = ? LIMIT ? [["follower_id", 3], ["id", 4], ["LIMIT", 1]] => false2:先ほどの演習の各コマンド実行時の結果を見返してみて、実際にはどんなSQLが出力されたのか確認してみる。
上記で確認できる。
14.1.5 フォロワー
リレーションシップに
user.followers
メソッドを追加する。
user.following
はフォローしている人数
user.followers
はフォローされてる人数(フォロワー)である。フォロワーの配列を展開するために必要な情報は、relationshipsテーブルに既にあり、
active_relationships
テーブルを再利用することで出来る。実際、
follower_id
とfollowed_id
を入れ替えるだけで、
フォロワーについてもフォローする場合と全く同じ活用が出来る。データモデルは以下。
出典:図 14.9: Relationshipモデルのカラムを入れ替えて作った、フォロワーのモデル
要は、
active_relationships
をpassive_relationships
に入れ替えて、
followed_id
とfollower_id
を入れ替えるだけ。上記のデータモデルの実装を
user.rb
にhas_manyを使って行う。user.rbclass User < ApplicationRecord # 関連付け has_many :microposts, dependent: :destroy # 1対多の関連付け 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一点、上記で注意すべき箇所は次の様に参照先(followers)を指定するための
:source
キーを省略してもよかった点。has_many :followers, through: :passive_relationshipsこれは、
:followers
属性の場合、
Railsがfollowers
を単数形にして自動的に外部キーfollower_id
を探してくれるから。ただ、必要がないが
has_many :following
との類似性を強調させるために書いている。次に、
followers.include?
メソッドを使って先ほどのデータモデルをテストしていく。テストコードは以下の通り。ちなみに
following?
と対照的なfollowed_by?
メソッドを定義してもよかったが、
サンプルアプリケーションで実際に使う場面がなかったのでこのメソッドは省略している。user_test.rbtest "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↑archerのフォロワーにmichaelは含まれているかどうかテストしている。
上記のテストは実際には多くの処理が正しく動いていなければパスしない。
つまり、受動的関係に対するテストは実装の影響を受けやすい。この時点で、全てのテストはパスする。
13 tests, 20 assertions, 0 failures, 0 errors, 0 skips演習
1:コンソールで、何人かのユーザーが最初のユーザーをフォローしている状況作ってみる。
最初のユーザーをuser
とすると、user.followers.map(&:id)
のidの値はどのようになっているか?>> user = User.first >> user_second = User.second >> user.followers.map(&:id) " = ? [["followed_id", 1]] => [2, 3]2:
user.followers.count
の実行結果が、先ほどフォローさせたユーザー数と一致していることを確認。>> user.followers.count (0.3ms) SELECT COUNT(*) FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."follower_id" WHERE "relationships"."followed_id" = ? [["followed_id", 1]] => 23:
user.followers.count
を実行した結果、出力されるSQL文はどのような内容になっているか?
また、user.followers.to_a.count
の実行結果と違っている箇所はあるか?
100万人ユーザーがフォロワーにいた場合はどうなるか?(0.3ms) SELECT COUNT(*) FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."follower_id" WHERE "relationships"."followed_id" = ? [["followed_id", 1]] >> user.followers.to_a.count => 2フォロワーが100万人いたらそのまま100万と言う数値が返されるが配列を生成する為、時間が掛かるしDBにも負担が掛かる。
14.2 FollowのWebインターフェイス
これまでやや複雑なデータモデリングの技術を駆使して実装した。
次は、モックアップで示したようにフォロー/フォロー解除の基本的なインターフェイスを実装する。
また、フォローしているユーザーと、フォロワーにそれぞれ表示用のページを作成する。後に、ユーザーのステータスフィードを追加して、サンプルアプリケーションを完成させる。
14.2.1 フォローのサンプルデータ
前章と同じように、サンプルデータを自動生成する
rails db:seed
を使って、DBにサンプルデータを登録できるとやや便利。先にサンプルデータを自動生成出来るようにしておけば、Webページの見た目のデザインから先に取り掛かることができ、バックエンド機能の実装を後に回すことが出来る。
リレーションシップのサンプルデータを生成するためのコードをseedに書いていく。
ここでは、最初のユーザーにユーザー3からユーザー51をフォローさせ、それから逆にユーザー4からユーザー41に最初のユーザーをフォローさせる。seed.rb$ rails db:migrate:reset $ rails db:seed演習
1:コンソールを開き、
User.first.followers.count
の結果がリスト14.14で期待している結果と合致していることを確認。>> User.first.followers.count User Load (0.2ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]] (0.3ms) SELECT COUNT(*) FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."follower_id" WHERE "relationships"."followed_id" = ? [["followed_id", 1]] => 382:
User.first.following.count
の結果も合致していることを確認。>> User.first.following.count User Load (0.2ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]] (0.3ms) SELECT COUNT(*) FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? [["follower_id", 1]] => 4914.2.2 統計と[Follow]フォーム
これでサンプルユーザに、フォローしているユーザーとフォロワーができました。
プロフィールページとHomeページを更新して、これを反映する。最初に、プロフィールページとHomeページに、
フォローしているユーザーとフォロワーの統計情報を表示するためのパーシャルを作成する。次に、フォローしているユーザーの一覧(following)と
フォロワーの一覧(followers)を表示する専用のページを作成する。Twitterの慣習にしたがってフォロー数の単位には
following
を使い、
例えば50 following
といった具合に表示する。上記の統計情報には、現在のユーザーがフォローしている人数と、
現在のフォロワーの人数が表示されている。それぞれの表示はリンクになっており、専用の表示ページに移動できる。
これらのリンクはダミーテキスト
#
を使って無効にしていた。
しかし、ルーティングについての知識もだいぶ増えてきたので、今回は実装することにする。実際のページ作成は後にルーティングは今実装する。
このコードでは、
resources
ブロックの内側で:member
メソッドが使っている。
これは初登場のメソッドだが、まずはどんな動作するのか推測してみる。routes.rbRails.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' post '/signup', to: 'users#create' get '/login', to: 'sessions#new' post '/login', to: 'sessions#create' delete '/logout', to: 'sessions#destroy' resources :users do # usersリソースをRESTfullな構造にするためのコード。 member do get :following, :followers end end resources :account_activations, only: [:edit] # editアクションのみaccount_activationsリソースを適用 resources :password_resets, only: [:new, :create, :edit, :update] # password再設定用のリソースを適用 resources :microposts, only: [:create, :destroy] # micropostsリソースをcreateとdestroyアクションにのみ適用 endこの場合のURLは
/users/1/following
や/users/1/followers
のようになるのではないかと推測。また、どちらもデータを表示するページなので、適切なHTTPメソッドはGETリクエストになる。
したがって、getメソッドを使って適切なレスポンスを返す。
ちなみに、memberメソッドを使うとユーザーidが含まれているURLを扱うようになる。
idを指定せずに全てのメンバーを表示するには、次のように
collection
メソッドを使う。resources :users do collection do get :tigers end endこのコードは
/users/tiggers
というURLに応答する。
生成されるルーティングテーブルは以下。この表で示したフォロー用とフォロワー用の名前付きルートを、今後の実装で使う。
HTTPリクエスト URL アクション 名前付きルート
GET /users/1/following following following_user_path(1)
GET /users/1/followers followers followers_user_path(1)出典:表 14.2: カスタムルールで提供するリスト 14.15のRESTfulルート
ルーティングを定義したので、統計情報のパーシャルを実装する準備が整った。
このパーシャルでは、divタグの中に2つのリンクを含めるようにする。_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ページの両方に表示されるので、
最初の行では、次のコードで現在のユーザーを取得する。<% @user ||= current_user %>@userがnilでない場合(つまりプロフィールページ)は何もせず、
nilの場合には@userにcurrent_userに代入するコードである。その後、フォローしているユーザーの人数を、次のように関連付けを使って計算する。
@user.following.countこれはフォロワーについても同様。
@user.microposts.countなお、今回も以前と同様に、Railsは高速化のためにDB内で合計を計算している点に注意。
一部の要素で、次のようにCSS idを指定していることにも注目。
<strong id="following" class="stat"> </strong>こうしておくと、Ajaxを実装するときに便利です。
そこでは、一意のidを指定してページ要素にアクセスしている。これで統計情報パーシャルが出来上がる。Homeページにこの統計情報を表示するには、以下のようにすると良い。
home.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>統計情報にスタイルを与えるために、SCSSを追加する。
変更の結果、Homeページは以下のようにする。
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; }出典:図 14.11: Homeページにフォロー関連の統計情報を表示する
この後すぐ、プロフィールにも統計情報パーシャルを表示するが、
今のうちに[Follow]/[Unfollow]ボタン用のパーシャルを作成する。_follow_form.html.erb<!--現在のユーザーがURLのユーザーとは違う場合--> <% unless current_user?(@user) %> <div id="follow_form"> <% if current_user.following?(@user) %> <%= render 'unfollow' %> <% else %> <%= render 'follow' %> <% end %> </div> <% end %>このコードは、followとunfollowのパーシャルに作業を振っているだけ。
(urlのユーザーをログインユーザーがフォローしていればunfollow,フォローしていなければfollowをレンダリング)パーシャルでは、Relationshipsリソース用の新しいルーティングが必要。
これを、Micropostsリソースの例に従って作成する。routes.rbRails.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' post '/signup', to: 'users#create' 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 :users # usersリソースをRESTfullな構造にするためのコード。 resources :account_activations, only: [:edit] # editアクションのみaccount_activationsリソースを適用 resources :password_resets, only: [:new, :create, :edit, :update] # password再設定用のリソースを適用 resources :microposts, only: [:create, :destroy] # micropostsリソースをcreateとdestroyアクションにのみ適用 resources :relationships, only: [:create, :destroy] # endフォロー/フォロー解除用のパーシャルも書く。
_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 %>_unfollow.html.erb<%= form_for(current_user.active_relationships.find_by(followed_id: @user.id), html: { method: :delete }) do |f| %> <%= f.submit "Unfollow", class: "btn" %> <% end %>これら2つのフォームでは、いずれも
form_for
を使ってRelationshipモデルオブジェクトを操作している。これらの2つのフォームの主な違いは、フォローフォームでは新しいリレーションシップを作成するのに対し、
アンフォローフォームでは既存のリレーションシップを見つけ出すという点。すなわち、前者はPOSTリクエストをRelationshipsコントローラに送信してリレーションシップをcreate(作成)し、
後者はDELETEリクエストを送信してリレーションシップをdestroy(削除)するということ。最終的に、このフォロー/アンフォローフォームにはボタンしかないことが理解できる。
しかし、それでもフォローフォームでは
followed_id
をコントローラに送信する必要がある。
これを行うために、hidden_field_tag
メソッドを使う。このメソッドは、次のフォーム用HTMLを生成する。
<input id="followed_id" name="followed_id" type="hidden" value="3" />12章で見たように、隠しフィールドのinputタグを使うことで、ブラウザ上に表示させずに適切な情報を含めることができる。
これでパーシャルとしてフォロー用フォームをプロフィールページに表示できるようになった。
showビューに表示用のhtmlを書く。
show.html.erb</section> <section class="stats"> <%= render 'shared/stats' %> </section> </aside> <div class="col-md-8"> <%= render 'follow_form' if logged_in? %> <% if @user.microposts.any? %>プロフィールには、それぞれ[Follow][Unfollow]ボタンが表示される。
これらのボタンを実装するには、二通りの方法がある。
1つは標準的な方法、もう1つはAjaxを使う方法。でもその前に、フォローしているユーザーとフォロワーを表示するページをそれぞれ作成してHTMLインターフェイスを完成させる。
演習
1:ブラウザから
/users/2
にアクセスし、フォローボタンが表示されていることを確認する。
同様に、/users/5ではUnfollow]ボタンが表示されているはず。さて、
/users/1
にアクセスすると、どのような結果が表示されるか?users/1はログインユーザーなのでボタンが消える。
2:ブラウザからHomeページとプロフィールページを表示してみて、統計情報が正しく表示されているか確認。
確認済み。
3:Homeページに表示されている統計情報に対してテストを書いてみる。
同様にして、プロフィールページにもテストを追加してみる。site_layout_test.rbtest "count relationships" do log_in_as(@user) get root_path assert_match @user.active_relationships.count.to_s, response.body assert_match @user.passive_relationships.count.to_s, response.body endusers_profile_test.rbend assert_select @user.microposts.count assert_match @user.active_relationships.to_s, response.body assert_match @user.passive_relationships.to_s, response.body endテストがパスしたのでOK。
14.2.3 [Following][Followers]ページ
フォローしているユーザーを表示するページと、フォロワーを表示するページは、
いずれもプロフィールページとユーザー一覧ページを合わせたような作りになるという点で似ている。どちらにもフォローの統計情報などのユーザー情報を表示するサイドバーと、ユーザーのリストがある。
さらに、サイドバーには小さめのユーザープロフィール画像のリンクを格子状に並べて表示する。
出典:図 14.14: フォローしているユーザー用ページのモックアップ
出典:図 14.15: ユーザーのフォロワー用ページのモックアップ
ここでの最初の作業は、フォローしているユーザーのリンクとフォロワーのリンクを動くようにすること。
Twitterに倣って、どちらのページでもユーザーのログインを要求するようにする。
そこで前回のアクセス制御と同様に、まずはテストから書いていく。
今回使うテストは以下の通り。
上記コードでは
following/followers
の名前付きルートを使っている点に注意。users_controller_test.rbtest "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この実装には1つだけトリッキーな部分がある。
それはUsersコントローラに2つの新しいアクションを追加する必要があるということ。これはroutesで定義した2つのルーティングに基づいており、これらはそれぞれ
following
およびfollowers
と呼ぶ必要がある。それぞれのアクションでは、タイトルを認定し、ユーザーを検索し、@user.followingまたは
@user.follower
sからデータを取り出し、ページネーションを行なって、ページを出力する必要がある。users_controller.rbdef 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これまで見てきたように、
Railsは慣習に従って、アクションに対応するビューを暗黙的に呼び出す。例えば、
show
アクションの最後でshow.html.erb
を呼び出す、といった具合。一方で、上記のいずれのアクションも
render
を明示的に呼び出し、show_follow
という同じビューを出力している。
したがって、作成が必要なビューはこれ1つ。renderで呼び出しているビューが同じである理由は、このERBはどちらの場合でもほぼ同じであり、
1つのファイルで両方の場合をカバーできるから。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>
users_controller
では、
- followingアクションで
following
を通してshow_follow
ビューを呼び出し、- followersアクションでは
followers
を通してshow_follow
ビューを呼び出す。この時、上記コードでは現在のユーザーを一切使っていないので、
他のユーザーのフォロワー一覧ページもうまく動く。beforeフィルターを既に実装しているため、テストはパスする。
12 tests, 21 assertions, 0 failures, 0 errors, 0 skips次に、
show_follow
の描画結果を確認するため、統合テストを書いていく。
ただし、今回は基本的なテストだけに留めておき、網羅的なテストにはしない。これはHTML構造を網羅的にチェックするテストは壊れやすく、生産性を落としかねないから。
したがって今回は、
正しい数が表示されているかどうかと、正しいURLが表示されているかどうかの2つのテストを書く。いつものように統合テストを生成するところから始める。
$ rails g integration_test following Running via Spring preloader in process 8224 invoke test_unit create test/integration/following_test.rb次に、テストデータをいくつか揃える。
リレーションシップ用のfixtureにデータを追加する。次のように書くことで
orange: content: "I just ate an orange!" created_at: <%= 10.minutes.ago %> user: michaelユーザーとマイクロポストは関連付けできる。
ユーザー名を書かずに
user: michaelではなく
user_id: 1このようなユーザーidを指定しても関連付けできる。
この例を参考に、Relationship用のfixtureにテストデータを追加する。relationships.ymlone: follower: michael followed: lana two: follower: michael followed: malory three: follower: lana followed: michael four: follower: archer followed: michael上記のfixtureでは、
前半の2つでMichaelがLanaとMaloryをフォローし、
後半の2つでLanaとArcherがMichaelをフォローしている。あとは、正しい数かどうかを確認するために、
assert_match
メソッドを使ってプロフィール画面のマイクロポスト数をテストする。さらに、正しいURLかどうかをテストするコードも加えると、以下のようになる。
following_test.rbdef 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上記では
assert_not @user.following.empty?このようなコードを書いているが、これは次のコードを確かめる為のテスト。
@user.following.each do |user| assert_select "a[href=?]", user_path(user) endつまり、
@user.following
の結果がtrueであれば、上記のブロックが実行できなくなる為、
その場合においてテストが適切なセキュリティモデルを確認できなくなることを防いでいる。上の変更を加えるとテストが成功する筈。
66 tests, 324 assertions, 0 failures, 0 errors, 0 skips演習
1:ブラウザから
/users/1/followers
と/users/1/following
を開き、それぞれが適切に表示されていることを確認。
サイドバーにある画像は、リンクとしてうまく機能しているか?OK
2:
following_test
のassert_select
関連のコードをコメントアウトしてみて、正しくテストが失敗することを確認。show_html.erb<% @users.each do |user| %> <%= #link_to gravatar_for(user, size: 30), user %> <% end %> $ rails t app/controllers/users_controller.rb:61:in `followers' test/integration/following_test.rb:20:in `block in <class:FollowingTest>' app/controllers/users_controller.rb:54:in `following' test/integration/following_test.rb:11:in `block in <class:FollowingTest>' 66/66: [=================================================================================================================] 100% Time: 00:00:02, Time: 00:00:02 Finished in 2.39352s 66 tests, 318 assertions, 0 failures, 2 errors, 0 skips12.2.4 [Follow]ボタン(基本編)
ビューが整ってきた。
いよいよ[Follow]/[Unfollow]ボタンを動作させる。フォローとフォロー解除はそれぞれリレーション湿布の作成と削除に対応しているため、まずはRelationshipsコントローラが必要。
いつものようにコントローラを生成させる。$ rails g controller RelationshipsRelationshipsコントローラのアクションでアクセス制御することはそこまで難しくない。
しかし、前回のアクセス制御の時と同様に最初にテストを書き、それをパスするように実装することでセキュリティモデルを確立させていく。
今回はまず、コントローラのアクションにアクセスする時、ログイン済みのユーザーであるかどうかをチェックする。もしログインしていなければログインページにリダイレクトされるので、Relationshipのカウントが変わっていないことを確認する。
relationships_controller_test.rbrequire '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次に、上記のテストをパスさせるために、
logged_in_user
フィルターを
Relationshipsコントローラのアクションに対して追加する。relationships_controller.rbclass RelationshipsController < ApplicationController before_action :logged_in_user def create end def destroy end end[Follow]/[Unfollow]ボタンを動作させるためには、フォームから送信されたパラメータを使って、
followed_id
に対応するユーザーを見つけてくる必要がある。その後、見つけてきたユーザーに対して適切に
follow/unfollow
メソッド(Userモデルで定義した)を使う。relationships_controller.rbclass 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上記をみてれば、先ほどのセキュリティ問題が実はそれほど重要なものではないことを理解できる。
もし、ログインしていないユーザーが(curlなどのコマンドラインツールなどを使って)これらのアクションに直接アクセスするようなことがあれば、
current_user
はnil
になり、
どちらのメソッドでも2行目で例外が発生する。エラーにはなるが、アプリケーションやデータに影響は生じない。
このままでも支障はないが、このような例外には頼らない方がいいので、セキュリティの為のレイヤーを追加した。
これで、フォロー/フォロー解除の機能が完成した。
どのユーザーも、他のユーザーをフォローしたりリフォローしたりできる。
ブラウザ上でボタンをクリックして、確かめてみる。
(振る舞いを検証する統合テストはのちに実装する)フォローしていないユーザーの画面
ユーザーをフォローした結果
演習
1:ブラウザ上から/users/2のFollow/Unfollow実行して動いているか確認
確認済み。
2:先ほどの演習を終えたら、Railsサーバーのログを見てみる。
フォロー/フォロー解除が実行されると、それぞれどのテンプレートが描画されているか?Started GET "/users/2" for 122.50.45.13 at 2019-02-07 19:55:41 +0000 Cannot render console from 122.50.45.13! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255 Processing by UsersController#show as HTML Parameters: {"id"=>"2"} User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]] Rendering users/show.html.erb within layouts/applicationフォローした場合、
/users/2
のビューが描画されているStarted GET "/users/2" for 122.50.45.13 at 2019-02-07 19:56:44 +0000 Cannot render console from 122.50.45.13! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255 Processing by UsersController#show as HTML Parameters: {"id"=>"2"} User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]] Rendering users/show.html.erb within layouts/applicationフォロー解除した場合、
/users/2
のビューが描画されている。14.2.5 [Follow]ボタン(Ajax編)
フォロー関連の機能の実装は完了したが、ステータスフィードに取りかかる前にもう1つだけ機能を洗練させてみる。
先ほどは
Relationships
コントローラのcreateアクション
とdestroyアクション
を単に元のプロフィールにリダイレクトしていた。
つまり、①ユーザーはプロフィールページを最初に表示
②ユーザーをフォロー
③すぐ元のページにリダイレクトという流れになる。
ユーザーをフォローした後、本当にそのページから離れて元のページに戻らないといけないのか。
答えは否(ハンターハンター風)で、同じページにリダイレクトさせる必要はない。この問題は、
Ajax
を使えば解決できる。Ajaxを使うことで、Webページからサーバーに「非同期」でページを遷移させることなくリクエストを送信することができる。
WebフォームにAjaxを採用するのは今や当たり前で、RailsでもAjaxを簡単に実装できるようになっている。
フォロー用とフォロー解除用のパーシャルをこれに沿って更新するのは簡単。
例えば、次のコードがあるとすると
form_for
上のコードを次のように置き換えるだけ
form_for ..., remote: trueこれだけでRailsは自動的にAjaxを使うようになる。
_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 %>_unfollow.html.erb<%= form_for(current_user.active_relationships.find_by(followed_id: @user.id), html: { method: :delete }, remote: true) do |f| %> <%= f.submit "Unfollow", class: "btn" %> <% end %>ERbによって実際に生成されるHTMLはこちら
<form action="/relationships/117" class="edit_relationship" data-remote="true" id="edit_relationship_117" method="post" > </form>ここでは、formタグの内部で
data-remote="true"
を設定している。これは、JavaScriptによるフォーム操作を許可することをRailsに知らせるためのもの。
(現在のRailsではHTMLプロパティを使って簡単にAjaxが扱えるようになっている)フォームの更新が終わったので、今度はこれに対応する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アクションにそれぞれ追加してみる。この時、ユーザーのローカル変数(user)を@userに変更している点に注目。
これは、
_follow.html.erb
と_unfollow.html.erb
を実装したことにより、
ビューで変数を使うインスタンス変数が必要になったからである。
(Ajaxによる)relationships_controller.rbclass 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 endAjaxリクエストに対応したので、今度はブラウザ側でJavaScriptが無効になっていた場合でもうまく動くようにする。
application.rbclass Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. config.load_defaults 5.1 # Settings in config/environments/* take precedence over those specified here. # Application configuration should go into files in config/initializers # -- all .rb files in that directory are automatically loaded. # 認証トークンをremoteフォームに埋め込む config.action_view.embed_authenticity_token_in_remote_forms = true end end一方で、JavaScriptが有効になっていても、まだ十分に対応できていない部分がある。
というのも、Ajaxリクエストを受診した場合は、Railsが自動的にアクションと同じ名前を持つ
JavaScript用の埋め込みRubyファイル(create.js.erb destroy.js.erb)などを呼び出す為、これらのファイルを作成する必要がある。.js.erbでは、JSと埋め込みRubyをミックスして現在のページに対するアクションを実行することができる。
ユーザーをフォローしたときや、フォロー解除した時にプロフィールページを更新するために、
これらのファイルが使われる。
JS-ERb
ファイルの内部では、DOM(Document Object Model)を使ってページ操作するため、
RailsがjQuery JavaScriptヘルパーを自動的に提供している。これによりjQueryライブラリの膨大なDOM操作用メソッドが使えるようになるが、
今回使うのはわずか2つ。まず1つ目は、$とCSS idを使って、DOM要素にアクセスする文法について知る必要がある。
例えば、follow_formの要素をjQueryで操作するには、次のようにアクセスする。$("#follow_form")これはフォームを囲む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を使っている。このコードでは
escape_javascript
メソッドを使っている点に注目。このメソッドは、
JavaScriptファイル内にHTMLを挿入する時に実行結果をエスケープするために必要。create.js.erb$("#follow_form").html("<%= escape_javascript(render('users/unfollow')) %>"); $("#followers").html('<%= @user.followers.count %>');各行の末尾にセミコロン;があることに注目。
(これは1950年代中頃に開発されたALGOLまで遡るらしい)
destroy.js.erb
ファイルの方も同様です。create.js.erb$("#follow_form").html("<%= escape_javascript(render(`users/unfollow`)) %>"); $("#followers").html('<%= @user.followers.count %>');destroy.js.erb$("#follow_form").html("<%= escape_javascript(render(`users/follow`)) %>"); $("#followers").html('<%= @user.followers.count %>');これらのコードにより、プロフィールページを更新させずにフォローとフォロー解除できるようになった筈。
演習
1:ブラウザから
/users/2
にアクセスし、うまく動いているかどうか確認。確認済み。
2:先ほどの演習で確認が終わったら、Railsサーバーのログを閲覧し、フォロー/フォロー解除を実行した直後のテンプレートがどうなっているか確認。
(0.0ms) begin transaction CACHE User Load (0.0ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]] SQL (2.2ms) INSERT INTO "relationships" ("follower_id", "followed_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["follower_id", 1], ["followed_id", 2], ["created_at", "2019-02-08 00:24:36.494125"], ["updated_at", "2019-02-08 00:24:36.494125"]] (8.0ms) commit transaction Rendering relationships/create.js.erb Relationship Load (0.1ms) SELECT "relationships".* FROM "relationships" WHERE "relationships"."follower_id" = ? AND "relationships"."followed_id" = ? LIMIT ? [["follower_id", 1], ["followed_id", 2], ["LIMIT", 1]] Rendered users/_unfollow.html.erb (2.0ms) (0.1ms) begin transaction SQL (2.8ms) DELETE FROM "relationships" WHERE "relationships"."id" = ? [["id", 100]] (8.5ms) commit transaction Rendering relationships/destroy.js.erb Rendered users/_follow.html.erb (1.2ms)きちんと
js.erb
ファイルがレンダリングされている。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(XlHttpRequest)というオプションをtrueにすると
Ajaxでリクエストを発行するよに変わる。したがって、
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これらのテストをまとめた結果
following_test.rbrequire 'test_helper' class FollowingTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) @other = users(:archer) 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 test "should follow a user 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この時点でテストはパスする。
演習
1:
relationships_controller.rb
のrespond_to
ブロック内の各行を順にコメントアウトしていき、テストが正しくエラーを検知できるかどうか確認。relationships_controller.rbrespond_to do |format| # format.html { redirect_to @user } format.js endERROR["test_should_follow_a_user_standard_way", FollowingTest, 1.5789846269981354] test_should_follow_a_user_standard_way#FollowingTest (1.58s) ActionController::UnknownFormat: ActionController::UnknownFormat: ActionController::UnknownFormat app/controllers/relationships_controller.rb:7:in `create' test/integration/following_test.rb:31:in `block (2 levels) in <class:FollowingTest>' test/integration/following_test.rb:30:in `block in <class:FollowingTest>' ERROR["test_should_unfollow_a_user_the_standard_way", FollowingTest, 1.6338956370018423] test_should_unfollow_a_user_the_standard_way#FollowingTest (1.63s) ActionController::UnknownFormat: ActionController::UnknownFormat: ActionController::UnknownFormat app/controllers/relationships_controller.rb:16:in `destroy' test/integration/following_test.rb:45:in `block (2 levels) in <class:FollowingTest>' test/integration/following_test.rb:44:in `block in <class:FollowingTest>' 72/72: [=================================================================================================================] 100% Time: 00:00:02, Time: 00:00:02 Finished in 2.13223s 72 tests, 334 assertions, 0 failures, 2 errors, 0 skips2:
xhr: true
がある行のうち、片方のみを削除するとどういった結果になるか?
このとき発生する問題の原因と、なぜ先ほどの演習で確認したテストがこの問題を検知できたのかを感がてみる。following_test.rbtest "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 endFAIL["test_should_follow_a_user_with_Ajax", FollowingTest, 1.2568579829967348] test_should_follow_a_user_with_Ajax#FollowingTest (1.26s) "@user.following.count" didn't change by 1. Expected: 3 Actual: 2 test/integration/following_test.rb:36:in `block in <class:FollowingTest>'Ajaxを用いたフォローで、postリクエストを送信していない為、フォロー数が変化せずテストが失敗する。
14.3 ステータスフィード
ステータスフィードの実装では、現在のユーザーにフォローされているユーザーのマイクロポストの配列を作成し、
現在のユーザー自身のマイクロポストと合わせて表示する。このセクションを通して、複雑さを増したフィードの実装に進んでいく。
これを実現するためには、RailsとRubyの高度な機能の他に、SQLプログラミングの技術も必要。
ステータスフィードの最終形のモックアップがこれ
出典:図 14.21: ステータスフィード付きのHomeページのモックアップ
14.3.1 動機と計画
ステータスフィードの基本的なアイデアはシンプル。
以下の図の矢印で示されているように、この目的は、現在のユーザーによってフォローされているユーザーに対応するユーザーidを持つマイクロポストを取り出し、同時に現在のユーザー自身のマイクロポストも一緒に取り出すこと。
出典:図 14.22: id 1のユーザーがid 2、7、8、10をフォローしているときのフィード
どのようにフィードを実装するのかはまだ明確ではないが、テストについてはやや明確そうなので、
まずはテストから書いていく。このテストで重要なことは、以下の3つの条件を満たすこと。
- フォローしているユーザーのマイクロポストがフィードに含まれている
- 自分自身のマイクロポストもフィードに含まれている
- フォローしていないユーザーのマイクロポストがフィードに含まれていない
まずは、MichaelがLanaをフォローしていて、Archerをフォローしていないという状況を作ってみる。
この状況のMichaelのフィードでは、Lanaと自分自身の投稿が見えていて、Archerの投稿は見えないことになる。
先ほどの3つの条件をアサーションに変換して、Userモデルにfeedメソッドがあることに注意しながら、
更新したUserモデルに対するテストを書いていく。user_test.rbtest "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 endfeedメソッドはまだ定義していないのでテストは失敗する。
$ rails t演習
1:マイクロポストのidが正しく並んでいると仮定(昇順ソート)して、データセットで
user.feed.map($:id)
を実行すると、どのような結果が表示されるか?考える。user.feed.map($:id) =>[1,2,7,8,10]このように、引数として受け取った自分のidと、フォローしているidが組み合わさって表示される。
14.3.2 フィードを初めて実装する
ステータスフィードに対する要件定義は、先ほどのテストで明確になったので、
早速フィードの実装に着手する。最終的なフィードの実装はやや込み入っているため、細かい部品を1つずつ確かめながら導入していく。
最初に、このフィードで必要なクエリについて考える。
ここで必要なのは、microposts
テーブルから、
あるユーザーがフォローしているユーザーに対応するidを持つマイクロポストを全て選択すること。このクエリを模式的に書くと
SELECT * FROM microposts WHERE user_id IN (<list of ids>) OR user_id = <user id>上記のコードを書く際に、SQLが
IN
というキーワードをサポートしていることを前提にしている。
(Railsではサポートされている)このキーワードを使うことで、idの集合を内包(setinclusion)に対してテストを行える。
13章のプロトフィードでは、上のような選択を行うために
Active Recordのwhere
メソッドを使っていることを思い出す。この時に選択すべき対象はシンプルで、
現在のユーザーに対応するユーザーidを持つマイクロポストを選択すればよかった。Micropost.where("user_id = ?", id)今回必要になる選択は、上よりも少し複雑で、例えば次のような形になる。
Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)これらの条件から、フォローされているユーザーに対応するidの配列が必要であることがわかってきた。
これを行う方法の1つは、Rubyのmapメソッドを使うこと。
このメソッドはすべての「列挙可能」なオブジェクト
(配列やハッシュなど、要素の集合で構成されたあらゆるオブジェクト)
で使える。なお、このメソッドは四章でも出てきた。
他の例題として、mapメソッドを使って配列を文字列に変換すると、以下のようになる。$ rails console >> [1,2,3,4].map { |i| i.to_s } => ["1","2","3","4"]上記に示したような状況では、各要素に対して同じメソッドが実行される。
これは非常によく使われる方法であり、次のようにアンバサンド(&)と、メソッドに対応するシンボルを使った短縮表記が使える。
この短縮表記であれば、変数iを使わずに済む。>> [1,2,3,4].map(&:to_s) => ["1","2","3","4"]この結果に対してjoinメソッドを使うと、idの集合をカンマ区切りの文字列として繋げることができる。
>> [1,2,3,4].map(&:to_s).join(', ') => "1,2,3,4"上記のコードを使えば、
user.following
にある各要素のidを呼び出し、フォローしているユーザーのidを配列として扱うことができる。例えばDBの最初のユーザーに対して実行すると、次のような結果になる。
>> User.first.following.map(&:id) User Load (0.9ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]] User Load (1.0ms) SELECT "users".* FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? [["follower_id", 1]] => [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 User Load (0.1ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]] (0.5ms) SELECT "users"."id" FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? [["follower_id", 1]] => [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が自動生成したもの。これにより、user.followingコレクション対応するidを得るためには、
関連付けの名前の末尾に_ids
を付け足すだけで済む。結果として、フォローしているユーザーidの文字列は次のようにして取得することができる。
>> User.first.following_ids.join(', ') User Load (0.1ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]] (0.2ms) SELECT "users"."id" FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."followed_id" WHERE "relationships"."follower_id" = ? [["follower_id", 1]] => "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文字列に挿入するときは、このように記述する必要はない。
実は、?を挿入すると自動的にこのあたりの面倒を見てくれる。
さらに、DBに依存する一部の非互換性まで解消してくれる。
つまり、ここではfollowing_ids
メソッドをそのまま使えば良いだけ。Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)というコードが無事に動いた。
作成したコードはこれ
user.rb# パスワード再設定の期限が切れている場合は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演習
1:Userモデルにおいて、現在のユーザー自身の投稿を含めないようにするにはどうすれば良いか?
また、そのような変更を加えると、user_test.rbのど部分のテストが失敗するか?$ user = User.first $ user.feed Micropost Load (0.9ms) SELECT "microposts".* FROM "microposts" WHERE (user_id IN (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) OR user_id = 1) ORDER BY "microposts"."created_at" DESC LIMIT ? [["LIMIT", 11]]OR user_id = 1にて、自分自身のユーザーidを渡している点に注目。
この渡す為の処理をメソッドから削除する。
user.rb# ユーザーのステータスフィードを渡す def feed Micropost.where("user_id IN (?) ", following_ids, id) endテストの失敗箇所
app/models/user.rb:90:in `feed' test/models/user_test.rb:97:in `block (2 levels) in <class:UserTest>' test/models/user_test.rb:96:in `block in <class:UserTest>'2:Userモデルにて、フォローしているユーザーの投稿を含めないように内容にするには?
また、テストの失敗箇所を見てみる。user.rb# ユーザーのステータスフィードを渡す def feed Micropost.where("user_id = ?", following_ids, id) endapp/models/user.rb:90:in `feed' test/models/user_test.rb:97:in `block (2 levels) in <class:UserTest>' test/models/user_test.rb:96:in `block in <class:UserTest>'3:フォローしていないユーザーの投稿を含めるためにはどうすればいいか?
また、そのような変更を加えると、テストがどう失敗するか?user.rb# ユーザーのステータスフィードを渡す def feed Micropost.all end全部含めてみる。
Expected true to be nil or false test/models/user_test.rb:105:in `block (2 levels) in <class:UserTest>' test/models/user_test.rb:104:in `block in <class:UserTest>'falseだけど〜って怒られてる。
14.3.3 サブセレクト
先ほどのフィードの実装は、投稿されたマイクロポストの数が膨大になった時にうまくスケールしない。
つまり、フォローしているユーザーが5000人程度になると、
Webサービス全体が遅くなる可能性がある。この節では、フォローしているユーザー数に応じてスケールできるように、ステータスフィードを改善していく。
following_idsでフォローしている全てのユーザーをDBに問い合わせし、
さらに、フォローしているユーザーの完全な配列を作るために再度DBに問い合わせしているのは問題である。。feedメソッドでは、集合に内包されているかどうかだけしかチェックされていない為、この部分はもっと効率的なコードに置き換えられるはず。
また、SQLは本来このような集合の操作に最適化されている。
実際、このような問題は、SQLのサブセレクト(subselect)を使うと解決できる。まずは、Userモデルのコードを若干修正し、フィードをリファクタリングすることから始める。
user.rb# ユーザーのステータスフィードを渡す def feed Micropost.where("user_id IN (:following_ids) OR user_id = :user_id", following_ids: following_ids, user_id: id) 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を追加する。
特に、次のコードはfollowing_ids
このようなSQLに置き換えることができる。
following_ids = "SELECT followed id FROM relationships WHERE follower_id = :user_id"このコードをSQLのサブセレクトとして使う。
SELECT * FROM microposts WHERE user_id IN (SELECT followed_id FROM relationships WHERE follower_id = 1) OR user_id = 1つまり、このような階層構造になっている。
- user_id
- 「ユーザー1がフォローしているユーザー全てを選択する」(サブセレクト)
このように、SELECT文を入れ子の中に内包させる形を「サブセレクト」と言う。
このサブセレクトは、集合のロジックをDBに保存するので、より効率的にデータを取得できる。
上記を元に、効率的なフィードを実装する。
user.rb# ユーザーのステータスフィードを渡す 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このコードは、Rails+Ruby+SQLのコードが複雑に絡み合っているが、きちんと動作する。
14 tests, 58 assertions, 0 failures, 0 errors, 0 skips大規模なWebサービスでは、バックグラウンド処理を使ってフィードを非同期生成するなどのさらなる改善が必要だが、Railsチュートリアルではここまでの改善にしておく。(Railsの入門書なので)
これで、ステータスフィードの実装が完了した。
いつも通り、masterブランチに変更を取り込む。
$ rails t $ git add -A $ git commit -m "Add user following" $ git checkout master $ git merge following-usersあとはコードをリポジトリにpushして、本番環境にデプロイ
$ git push $ git push heroku $ source <(curl -sL https://cdn.learnenough.com/heroku_install) $ heroku pg:reset DATABASE $ heroku run rails db:migrate $ heroku run rails db:seed新規作成アカウントの有効化ができない場合は、以下のid、passでログインしてみてください。
email:example-2@railstutorial.org
pass:password演習
1:Homeページで表示される1ページ目のフィードに対して、統合テストを書いてみる。
following_test.rbtest "feed on Home page" do get root_path @user.feed.paginate(page: 1).each do |micropost| assert_match CGI.escapeHTML(micropost.content), response.body end end2:上記コードでは、期待されるHTMLを
CGI.escapeHTML
メソッドでエスケープしている。
その理由は?
また、試しにエスケープ処理を外して、得られるHTMLの内容を調べてみて、マイクロポストの内容がおかしい点を挙げよ。following_test.rbtest "feed on Home page" do get root_path @user.feed.paginate(page: 1).each do |micropost| assert_match micropost.content, response.body end endA:contentをエスケープしている為、CGI.esapeHTMLを加える必要がある。
14.4 最後に(サンプルアプリケーションで学んだこと)
サンプルappで学んだことをまとめてみる。
- MVCモデル
- テンプレート
- パーシャル
- beforeフィルター
- バリデーション
- コールバック
- データモデルの関連付け(has_many/belongs_to/has_many through)
- セキュリティ
- テスティング
- デプロイ
今後はサンプルAppに
- 返信機能
- メッセージ機能
- フォロワーの通知
- RSSフィード
- RESTAPI
- 検索機能
- いいね機能
- シェア機能
などを加えてオリジナルアプリケーションを完成させていくと良い。
終わったー
やっとRailsチュートリアルを終えることができました。
長かったですねー。全部Qiitaにメモったおかげで、流し読みせずに熟読できました。
お陰で、全体を通してWebアプリケーション制作の基礎を理解できたような気がします。正直、1周目の理解度は30パーセントぐらいでした。
今回の2周目で理解度は80パーセントぐらいまで上がった気がします。これからはオリジナルのWebアプリケーションを作成していくことで、
今回覚えた内容を自分の物にしていきたいと思います!みなさんお疲れ様でした!
YUUKI.
単語集
- has_many through
多対多の関係性を定義する関連付けメソッド。
- source
has_manyに対してパラメータを与えるオプション。
sourceオブションで与えた値は配列の元を表しているので、実際の配列のインデックスは変わらない。
- collection
コレクションルーティングを追加するメソッド。
idを指定せずに全てのメンバーを表示したりできる
- Ajax(エイジャックス)
Asynchronous(非同期な) JS + XMLで作られている非同期通信を行いながらインターフェイスの構築を行うプログラミング手法のこと。
サーバーからのレスポンスを待たずにクライアント側の表示を変更させることができる。例えば、Ajaxを使用することで画面遷移せずにHTMLを更新することが可能で、ユーザビリティの向上やサーバー負荷の軽減に繋がる。
Railsでは、
remote: true
で使える。
- respond_to
リクエストの種類によって応答を場合分けするメソッド。
処理内に書いたいずれかの1行が実行されるよう書くことができる。書き方の例
respond_to do |format| format.htlm {...} format.json {...}
- xhr
Xmlhttprequestの略で、Ajax通信かどうかを判定するオプション。
trueを渡すことでAjaxでリクエストを発行出来る。Ajax
- DOM
Document Object Modelの略で、JSファイル内で別のHTMLファイルなどを読み込む時の役割のこと。
- following_ids
followしているユーザーのidをそれぞれ文字列に変換して、,で区切る値として返すメソッド。
- サブセレクト
SQLのSELECT文を入れ子にしたものを指す。
入れ子構造にすることで、より効率的にデータを取得できる。