tawara's blog

雑記。個人の見解です。

has_many :through関連付けとsouceオプションで欲しいレコードの集合を取得する

こんにちは、たわらです。

本記事は、has_many :through関連付けとsouceオプションの情報の整理です。

ことの発端

掲示板作成アプリにて、他のユーザーが作成した掲示板にBookmark機能を実装するにあたり、

has_many :bookmark_boards, through: :bookmarks, source: :board

という記述が理解できませんでした。

Bookmarksテーブルは作成したけど、bookmark_boardsなんてテーブルは作ってないぞ。

has_manyの引数はクラス名じゃないといけないのではないか?

と思っていたので、??? となりました。

多対多の関係を関連付ける has_many :through関連付け

has_many :through関連付けは、Railsガイドによると、次のように説明されています。

has_many :through関連付けは、他方のモデルと「多対多」のつながりを設定する場合によく使われます。この関連付けは、2つのモデルの間に「第3のモデル」(joinモデル)が介在する点が特徴です。それによって、相手モデルの「0個以上」のインスタンスとマッチします。

ユーザーは複数の掲示板にブクマができるし、掲示板は複数のユーザーにブクマされることができます。なので、ユーザーと掲示板は多対多の関係にあります。

ただここで、そのものずばり、Bookmark_boardsテーブルを作り、user_id、board_id、title、bodyと持たせることも考えられます。

ですが、これだと既存のBoardテーブルにある情報と重複してしまいます。Boardテーブルにある、任意のbodyの内容が更新されたとき、Bookmark_boardsテーブルも同じ箇所を更新する必要が発生してしまいます。「1 fact 1 place」の原則からはずれ、データの冗長性により、更新時異常なるものが発生する危険があります。

このあたりは、Railsチュートリアルのユーザーフォローのところの解説に通じます。

railstutorial.jp

ブクマするというイベント系エンティティ

なので、Bookmarksテーブルをつくり、user_idとboard_idのカラムを持たせ、あるユーザーがひとつの掲示板をブクマしていることを表現します。

Railsはこのテーブルを経由して、Boardテーブルにアクセスし、あるユーザーがブックマークした掲示板のレコードの集合を取得できるのです。

このように、ユーザーとか商品という「モノ」ではなく、ブックマークとか販売などの「行為・出来事」の記録を「イベント系エンティティ」と分類し、管理対象とするのです。

とエラソーに言っていますが、「楽々ERDレッスン」(pp25)に解説されていることを、講師に教えてもらいました。

この発想は新鮮でした。モノだけでなくて、イベントも管理するのか、管理できるのか、管理しなくちゃならないのか! と思いました。

amzn.to

で、ユーザーがひとつの掲示板を何度もブクマできることは必要ないので、use_idとboard_idのペアが一意になるようにします。

#bookmark.rb

validates :user_id, uniqueness: { scope: :board_id }
#migrationに追記

add_index :bookmarks, [:user_id, :board_id], unique: true

sourceオプションでBookmarksテーブルを経由して参照するテーブルを決定する

そもそもなぜ、

has_many :boards, through: :bookmarks

ではいけないのか? Bookmarksテーブルを経由して、Boardテーブルから必要なデータが取得できるじゃないか、と。

問題は簡単で、すでにhas_many :boardsを使用してしまっているからです。has_manyに同じ引数は渡せないのです。(:boards の部分て引数だったんだ!!)

では、どうして、

has_many :bookmark_boards, through: :bookmarks

ではいけないのでしょうか? 

この場合、Railsは、Bookmarksテーブルを経由して、has_manyの引数であるBookmark_boardsというテーブルを探します。で、当然そのようなテーブルはないので、エラーが起きてしまいます。

つまり、Bookmarksテーブルを経由したあと、Railsがどこのテーブルのデータを取得すればよいかわからない、ということです。

そこで、sourceオプションが登場します。Bookmarksテーブルを経由したら、こっちのテーブルにデータを取りに行ってね、ということです。

この場合、Boardテーブルからブクマした掲示板のデータ集合を取得したいので、

has_many :bookmark_boards, through: :bookmarks, source: :board

このようになるのですね!

どうしてbookmark_boardsという名前なの?

has_manyの引数は任意に決めることができます。ではどうして、このような名前なのでしょうか。

答えは簡単で、どのような配列が返ってくるかが、人間にわかりやすくするためです。

user.bookmark_boardsなどとコードのなかに見つければ、

ああ、これはユーザーのブクマした掲示板を取得しているんだな、ということがわかります。

最後に

これを飲み込むまでにすごい時間がかかった。。。

読んでくださったかた、ありがとうございます。