プログラミング備忘録

プログラミングの学習状況をメモしています

65日目

今日の学習

Ruby on Rails

Railsチュートリアル 第14章

railstutorial.jp

ついに最終章となった。ユーザーごとに投稿ができる機能などの実装を行ったことはあるものの、ユーザーをフォローするという機能を作成するのは初めてなので楽しみ。

Relationshipモデル
データモデルの作成

ユーザーをフォローするデータモデル構成で想定される一般的なケースを考えてみる。

  • hogeさんはfugaさんをフォローする
    hogeさんはfugaさんを、フォローした(followed
    fugaさんはhogeさんの、フォロワー(follower

Railsの複数形の慣習に従うと、「あるユーザーをフォローしている全てのユーザー」の集合はfollowersという名前になり、

user.followersで、フォロワーの配列を呼び出すことができる。

しかし、この慣習に従えば、その逆の「あるユーザーがフォローしているすべてのユーザー」の集合はfollowedsという名前になる。これは英語の文法から外れており、意味も分かりにくい。

そのため、Twitterの監修にならって、「あるユーザーがフォローしているすべてのユーザー」に対してfollowingという呼称を採用する。

そのため、例えばhogeさんがフォローしている全てのユーザーを呼び出したい場合は、

hoge.followingという形になる。

名称を決定したので、followingテーブルとhas_manyで関連付けを行い、フォローしているユーザーのモデリングができる。

ユーザーの集合であるfollowingテーブルのそれぞれの行には、followed_idというユーザーを識別するカラムが必要となり、さらにユーザーの名前やパスワードなども必要になる。

例:userのidが1のhogeさんが、idが2, 5, 10のユーザーをフォローしている場合

userテーブル id1のデータ

follower_id name email password
1 hoge hoge@email.com hogehoge

followingテーブル

follower_id followed_id name email password
1 2 fuga fuga@email.com fugafuga
1 5 ... ... ...
1 10 ... ... ...

follower_idは、hogeさんのidが登録される。一人のユーザーが複数持てるため、has_manyで関連付けられる。

followed_idには、フォローされている人のidが記述されており、それ以降のnameやemailといった情報は該当idの情報が入る。

このデータモデルの問題点は、userモデルには既にemailやpasswordが格納されているにもかかわらず、followingにも同じカラムがあり非常に無駄があるところ。

また、followersモデリングを作る際も、followingと同じようにnameやemailが格納されることになる。

もしも、誰かがユーザー名などを変更すれば、usersテーブルのレコードだけではなく、followingfollowersテーブルのレコード全ての行を更新することになり、負荷も多い。

これを解決するために、フォローの動作をもう一度確認する。

  • あるユーザーが別のユーザーをフォローするときに、作成されるものはなにか
  • あるユーザーが別のユーザーのフォローを解除するときに、何が削除されるのか

フォローという動作で作成・削除されるものは、ユーザー間の「関係(realationship)」であることが分かる。(フォローしている関係、フォローされている関係)

さらに、フォローという機能は左右非対称の性質がある。一般的なフレンド機能とは異なり、hogeさんがfugaさんをフォローしていたとしても、fugaさんがhogeさんをフォローしているとは限らない。

Railsチュートリアルでは、このような関係を以下のように読んでいる。

  • 能動的関係Active Relationship
    hogeさんから見たfugaさんとの関係(フォローしている)
  • 受動的関係Passive Relationship
    fugaさんから見たhogeさんとの関係(フォローされている)

まずはフォローしているユーザーを生成するために、能動的関係に焦点を当てる。

フォローしているユーザーをfollowed_idで識別することにし、followingテーブルをactive_relationshipテーブルに置き換えて考える。

usersテーブルと重複している、nameやemailのユーザー情報は無駄なので、ユーザーのid以外の情報は消してしまう。

followed_idを通じて、usersテーブルのユーザーを見つけるようにする。

先ほどと同じように、ユーザーのidが1の人が、id2, 5, 10の人をフォローしている場合は以下のように表すことができる。

active_relationshipsテーブル

follower_id followed_id
1 2
1 5
1 10

followed_idと一致しているusersテーブルのユーザーidを探し出し、フォローしているユーザーを取得できる。(has_manyの関係)

relationshipsデータモデルを作成し、カラムにfollower_idfollowed_id(データ型はどちらもinteger)を加える。

この二つのカラムは、今後頻繁に検索することになるため、検索の効率を上げるためにインデックスを追加する。

また、follower_idfollowed_idの組み合わせが必ずユニークであるように設定し、同じユーザーを二回以上フォローすることを防ぐ。

add_index :relationships, :follower_id
add_index :relationships, :followed_id
add_index :relationships, []follower_id, :followed_id], unique: true
User/Relationshipの関連付け

1人のユーザーにはhas_many(1対多)の関係(relationship)があり、この関係は2人のユーザーの間の関係なので、フォローしているユーザーとフォロワーの両方に属する(belongs_to)。

前回UserMicropostを関連付けたときのようにして、新しいリレーションシップを作成する。

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

このコードを実行するために、has_manyで関連づける必要があるが、今回のケースでは書き方を変えないとRelationshipモデルを探すことができない。

# 前回は引数のシンボルからMicropostモデルを探すことができた
class User < ApplicationRecord
  has_many :microposts
end

# user_id属性をたどってユーザーを特定できる
class Micropost < ApplicationRecord
  belongs_to :user
end

データベースの2つのテーブルを繋いでくれるidを外部キー(foreign key)と呼ぶ。

マイクロポストの例で言うと、Userモデルに繋げる外部キーが、Micropostモデルのuser_id属性になる。

Railsはこの外部キーの名前を使い、関連付けの推測をしてくれる。

<class>_idのように推測をしてくれるため、上のコードの場合はuser_idとなる。

しかし、今回はフォローしているユーザーをfollower_idという外部キーを使って特定する必要がある上に、followerというクラス名が存在しないため、Railsに正しいクラス名を伝える必要がある。

モデル名と関係名が異なるため、class_nameforeign_keyを利用し、指定する。

app/models/user.rb

has_many :active_relationships, class_name: "Relationship",
                                foreign_key: "follower_id",
                                dependent: :destroy

能動的関係に対して、1対多の関連付けを行う。

ユーザーを削除したら、ユーザーのリレーションシップも同時に削除してほしいためdependent: :destroyを付け加えてある。

次に、フォロワーに対してbelongs_toの関連付けを行う。また、ついでにバリデーションも追加しておく。

app/models/relationship.rb

belongs_to :follower, class_name: "User"
belongs_to :followed, class_name: "User"
validates :follower_id, presence: true
validates :followed_id, presence: true

二つの関連付けにより、以下のメソッドが使えるようになった。

  • active_relationship.follower
    フォロワーを返す
  • active_relationship.followed
    フォローしているユーザーを返す
  • user.active_relationship.create(followed_id: other_user.id){<br> user`と紐づけて能動的関係を作成する
  • user.active_relationship.build(followed_id: other_user.id)
    userと紐づけた新しいRelationshipオブジェクトを返す
has_many through

followingfollowersを結びつけるにあたって、has_many throughを使って関連付けを行う。

1人のユーザーには、いくつもの「フォローする」「フォローされる」といった関係がある。これは1対多ではなく、多対多の関係。

Railsはモデル名(単数)に対応する外部キーを探す。

has_many :followeds, through: :active_relationships

Railsfollowedsというシンボル名を見て、followedという単数系に変えてrelationshipsテーブルのfollowed_idを使い、対象のユーザーを取得する。

しかし、followedsという英単語は不自然なので、followingという名前を利用したい。

そのために、Railsのデフォルト設定であるモデル名(単数)に対応する外部キーを探すという部分を、:sourceパラメータを使って変更する。

following配列の元はfollowedidの集合だということをRailsに伝えたいため、以下のようにする。

has_many :following, through: :active_relationships, source: :followed

関連付けを行ったため、フォローしているユーザーを配列のように扱えるようになった。

以下のように、メソッドを利用してオブジェクトを探すことができる。

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

followingで取得したオブジェクトは、配列のように要素を追加したり削除したりすることができる。とても簡潔かつ、分かりやすい。

user.following << other_user
user.following.delete(other_user)
follow関連のメソッドの追加

関連付けによって集合を簡単に取得できるようになった。次は、これをより簡単に扱うために、followunfollowといった便利メソッドを追加していく。

メソッドの実装にあたり、テストを先に書き起こした。このメソッドを実際に使えるのはまだ先であるため、しっかり効用があるかどうかを事前に確認しておく必要がある。

今回用意するメソッドは以下の三種類。

  • ユーザーをフォローするfollow
  • ユーザーのフォローを解除するunfollow
  • あるユーザーが誰かをフォローしているかどうか確認するfollowing?

さらに、上記のメソッドを利用し、以下の流れでテストを行う。

  1. following?メソッドであるユーザーをまだフォローしていないことを確認
  2. followメソッドでそのユーザーをフォロー
  3. following?メソッドでフォローしていることを確認
  4. unfollowメソッドでフォローを解除
  5. following?メソッドでフォローしていないことを確認

test/models/user_test.rb

test "should follow and unfollow a user" do
  # fixtureを使ってユーザーを設定する
  user1 = users(:user1)
  user2 = users(:user2)
  assert_not user1.following?(user2)
  user1.follow(user2)
  assert user1.following?(user2)
  user1.unfollow(user2)
  assert_not user.following?(user2)
end

テストの準備ができたので、実際にメソッドを登録していく。user自身を表すselfが省略されている。

app/models/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

def following?(other_user)
  following.include?(other_user)
end
フォロワー

今度は、user.followingと対になる、user.followersメソッドを追加したい。

relationshipsテーブルには、既にフォロワーを取得するために必要な情報が含まれているため、active_relationshipsテーブルを再利用する。

app/models/user.rb

has_many :passive_relationships, class_name: "Relationship",
                                foreign_key: "followed_id",
                                dependent: :destroy

has_many :followers, through: :passive_relationships, source: :follower

能動的関係を使った時とは逆になるように、受動的関係を使ってuser.followersを実装する。

なお、本来followersを指定するためにsourceを使用する必要はない。Railsfollowersの単数系であるfollowerを外部キーとしてそのまま探してくれる。

必要はないが、あえて書き足すことでhas_many :followingと対になっている類似性を強調してある。

これで、followers.include?user.followers.countといったメソッドが利用できるようになった。

countなどでDBを参照する場合、DBから直接算出しているため、処理が高速になるそうだ。

今日のやらかし

テストが実行できない

Relationshipモデルのバリデーションテストを行っていたが、エラーが出てしまう。

`test': test_should_require_a_follower_id is already defined in RelationshipTest (RuntimeError)

内容はチュートリアル通りで問題ないはずだが...と思い、チュートリアルのお手本をコピペしてみると、テスト名が重複していたことに気づく。

test "should require a follower_id" do

test "should require a followed_id" do

上のテスト名が正しいが、誤ってどちらもfollowed_idと書いてしまっており、テスト名が重複しているのが原因でエラーが出ていた。

already definedで、「すでに定義されています」という意味になるが、テスト名でミスしているとは思わず内容が重複してしまっているかを確認してしまっていたので反省。