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

June 12, 2020

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

本記事は、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 チュートリアルのユーザーフォローのところの解説に通じます。

https://railstutorial.jp/chapters/following-users?version=3.2#sec-a_problem_with_the_data_model

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

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

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

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

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

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

https://amzn.to/3dW9Wp4

で、ユーザーがひとつの掲示板を何度もブクマできることは必要ないので、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 などとコードのなかに見つければ、

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

最後に

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

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