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)に解説されていることを、講師に教えてもらいました。
この発想は新鮮でした。モノだけでなくて、イベントも管理するのか、管理できるのか、管理しなくちゃならないのか! と思いました。
で、ユーザーがひとつの掲示板を何度もブクマできることは必要ないので、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 などとコードのなかに見つければ、
ああ、これはユーザーのブクマした掲示板を取得しているんだな、ということがわかります。
最後に
これを飲み込むまでにすごい時間がかかった。。。
読んでくださったかた、ありがとうございます。