【Rails】モデルの関連付けの読み方

最終的なコード

最終的なコードは以下となる。
初めてこのコードを見たときに何がなんだかさっぱりわからなかったので、学んだことをひとつひとつを解説していく。

# app/models/user.rb
class User < ApplicationRecord
  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
# app/models/relationship.rb
class Relationship < ApplicationRecord
  belongs_to :follower, class_name: 'User'
  belongs_to :followed, class_name: 'User'
end

事前に抑えて置きたいポイント

以下の4つを抑えて置くと関連付けのコードを理解しやすくなる。

  • has_manybelongs_toの本質
  • class_nameの意味とデフォルトの挙動
  • foreign_keyの意味とデフォルトの挙動
  • throughsourceの挙動

has_manybelongs_toの本質

has_manybelongs_toの本質は「モデルのインスタンスが引数で指定した値をメソッドとして使えるようにする」こと。つまり、「メソッドを生成するメソッド」ということになる。

has_manybelongs_toの違いは

  • 「1:多」の関連付けをする1側のモデルにhas_manyを使い、引数は多側のモデル名の複数形にする
  • 「1:多」の関連付けをする多側のモデルにbelongs_toを使い、引数は1側のモデル名の単数形にする(モデル名はもともと単数形)

    例:よくある投稿機能のあるSNSアプリがあるとして、PostモデルとCommentモデルが1対多の関係であるとする。
# app/models/post.rb
class Post < ApplicationRecord
  has_many :comments
end
# app/models/comment.rb
class Comment < ApplicationRecord
  belongs_to :post
end

Postモデルにhas_many :commentsと書いた場合、Post.first.commentsPost.first.comments.buildPost.first.comments.find_byなどが使えるようになる。
また、Commentモデルにbelongs_to :postと書くことで、Comment.first.postComment.first.post.buildComment.first.post.find_byなどが同じく使えるようになる。

class_nameの意味とデフォルトの挙動

モデルの関連付けをする場合、デフォルトの挙動としてhas_manybelongs_toの引数に指定したモデルクラス(モデル)に関連付けをしようとする。

例:

# app/models/post.rb
class Post < ApplicationRecord
  has_many :comments # Commentモデルに関連付けされる
end
# app/models/comment.rb
class Comment < ApplicationRecord
  belongs_to :post # Postモデルに関連付けされる
end

しかし、has_manybelongs_toの引数に指定したモデル以外に関連付けをしたい場合もある。
その場合はclass_nameを使って引数にモデルクラス名(モデル名)を指定すれば良い。class_name: '関連付けしたいモデル名(単数形で頭文字を大文字にする)'と記述する。

例:

# app/models/post.rb
class Post < ApplicationRecord
  has_many :comments, class_name: 'Favorite' # Commentモデルではなくfavoriteモデルに関連付けされる
end
# app/models/comment.rb
class Comment < ApplicationRecord
  belongs_to :post, class_name: 'Like' # PostモデルではなくLikeモデルに関連付けされる
end

foreign_keyの意味とデフォルトの挙動

前提:親モデルと子モデルの外部キーは同じものにする必要がある(外部キーのカラムを持つモデルが子モデル)。子モデルの外部キーを親モデルにも設定する。

has_manyを使う場合、デフォルトで#{自分のモデルクラス名}_idが外部キーとして扱われる。

# app/models/post.rb
class Post < ApplicationRecord
  has_many :comments # 外部キーはpost_id
end

一方belongs_toを使う場合は、#{belongs_toの引数}_idがデフォルトで外部キーとして扱われる。

# app/models/comment.rb
class Comment < ApplicationRecord
  belongs_to :post # 外部キーはpost_id
end
# app/models/comment.rb
class Comment < ApplicationRecord
  belongs_to :post, class_name: 'Like' # 外部キーlike_id
end

外部キーをデフォルトものから別のものに指定したい場合は、foreign_keyを使用する。 書き方はforeign_key: #{外部キー名}
※デフォルトと違って末尾に自動で_idとはならないので注意

# app/models/post.rb
class Post < ApplicationRecord
  has_many :comments, foreign_key: 'user_id' # 外部キーをuser_idへ変更
end
# app/models/comment.rb
class Comment < ApplicationRecord
  belongs_to :post, class_name: 'Like', foreign_key: 'comment_id' # 外部キーをuser_idへ変更
end

throughsourceの挙動

throughは引数で指定したメソッドをモデルのインスタンスに対して実行し、そこで得られたデータの集合の1つ1つの要素に対してsourceで指定したメソッドを実行し、その結果を返す。 mapメソッドをしているイメージ。

フォロー機能の解説

# app/models/user.rb
class User < ApplicationRecord
  # 外部キーをfollower_idに設定し、relationshipモデルと関連付け、データを取得できるactive_relationshipsというメソッドを生成
  has_many :active_relationships, class_name: 'Relationship', foreign_key: 'follower_id', dependent: :destroy

  # active_relationshipsメソッドをuserモデルのインスタンス(ユーザ1やユーザ2など)に対して実行し、得られたデータの集合1つ1つのデータに対してfollowedメソッドを実行し(得られた1つ1つのデータはrelationshipモデルのインスタンスなのでfollowerメソッドが使える)、その結果を返すfollowingメソッドを作成
  has_many :following, through: :active_relationships, source: :followed
end
# app/models/relationship.rb
class Relationship < ApplicationRecord
  # 外部キーをfollower_idに設定し、userモデルと関連付け、データを取得できるfollowerというメソッドを生成
  belongs_to :follower, class_name: 'User'
end

参考

railstutorial.jp