アソシエーションについて復習する
経緯
- 多対多のテーブルが全く理解できない
- モデルに記載されている内容が分からない
- 分からなすぎて何から質問したら良いかが正直分からない(重症)
- とりあえずアソシエーションの不理解が一つの要因としてありそうなので復習する
- 完全自分用メモ
↑こちらのサイトを参考にチュートリアルを進めました。
ブックマーク課題で自分が知りたかったことはほぼここに書いてあった😭
アソシエーション・・・モデル間の関連付け
ツイッターの下位互換のような一言だけ投稿できるだけのアプリがあるとする。あるのはTweetモデルのみ。このため誰がどんなコメントをしたのか区別する術がなく、そのツイートが誰のものか分からない。コメントが垂れ流しになっているだけという状態。
このアプリにUserモデルを追加してどのユーザーが投稿したコメントなのか区別できるようにした。この時、どのコメントが誰の投稿なのかを関連付けるものがアソシエーション。
この関連付けにおいて、一人のユーザーは複数のコメントを(所持)しているため、一対多の関係であると言える。
データベース設計
情報システム(ツイッターなどアプリそのもの)
において、どのような情報(ユーザーやツイート、お気に入り)
をデータベースに格納すべきかを検討し、その格納すべき情報(情報一つ一つ)
をどのような形(データ型)
で保持するかを設計すること。
用語集(さらっと流し読みする)
エンティティ
モデリングの対象となるアプリの中で管理する必要がある情報のことをエンティティと言う。 UserやTweet、Follow、Favorite、モデルのようであり、実際は違うらしい。近しいもの。
リレーション
関連(リレーション)とは、結び付きのあるエンティティ同士をリンクさせるもの。一言投稿アプリにおいてはUserとPostという簡単な関連のこと。アソシエーションとリレーションは同じものと思って大丈夫。
属性(プロパティ)
あるエンティティ(model)に従属する項目のこと。Userの中のid,nama,passwordと言いたもの。
関連の多重度
あるエンティティAとB(UserとPostなど)において、その繋がりは片方から見た時、相手が一つなのか、複数なのかを明らかにすること。Userは一人、Postは複数なのでこの場合は一対多。
この中のリレーション(関連)と関連の多重度がアソシエーション。エンティティ同士の結びつきと、それが一対一なのか一対多なのか、はたまた多対多なのか。。。
ツイッターアプリで考える
User | Tweet |
---|---|
-id | -id |
-name | -body |
-encrypted_password |
このままだとどのツイートがどのユーザーに所属しているのかが分からない。これを設定するために、foreigin_key
を設定する必要がある。
foreign_key(外部キー)とはその親のid(Primary Key)を保存するカラムのこと。 foreign_keyの値によって、あるツイートがどのユーザーに所属しているものかが分かる。
User | Tweet |
---|---|
-id | -id |
-name | -body |
-encrypted_password | -user_id |
ツイッター風アプリの作成
【初心者向け】丁寧すぎるRails『アソシエーション』チュートリアル【幾ら何でも】【完璧にわかる】🎸 - Qiita
↑記載のアプリを作成を実際に作ってみた。
tweetsコントローラ
def create @tweet=current_user.tweets.create(tweet_params) redirect_to tweets_path end private def tweet_params params.require(:tweet).permit(:body) end
↑
- devise gem使用のためdeviseのメソッドcurrent_userが使える
- ストロングパラメータを通してフォームの入力内容が送られてくるが、フォームからはuser_idが送られていない。このため、createアクション内でcurrent_userを紐付けたtweetの作成をしている。
user=User.first tweet=user.tweets.create(body: "本文")
userモデルにhas_many :tweets
というアソシエーションの設定が済んでいるので、上記のような形でユーザーに紐付いたツイートが作成できる。
irb(main):001:0> user = User.first User Load (0.1ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]] => #<User id: 1, email: "tanaka@example.com", created_at: "2022-02...
irb(main):003:0> tweet2 = user.tweets.create(body: "お腹すいた") (0.1ms) begin transaction Tweet Create (0.5ms) INSERT INTO "tweets" ("body", "user_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["body", "お腹すいた"], ["user_id", 1], ["created_at", "2022-02-06 10:49:40.547672"], ["updated_at", "2022-02-06 10:49:40.547672"]] (0.9ms) commit transaction => #<Tweet id: 2, body: "お腹すいた", user_id: 1, created_at: "202... irb(main):004:0> tweet2 => #<Tweet id: 2, body: "お腹すいた", user_id: 1, created_at: "2022-02-06 10:49:40", updated_at: "2022-02-06 10:49:40">
★アソシエーションしているデータの受け取り方★
正直ここが一番詰まっていた原因だと思う。。
@user.tweets
の形でユーザーに関連したツイートを取得することができる。
@userは一つのユーザーのデータ
user.rbにhas_many :tweets
にツイートをたくさん持っているアソシエーションを記載しているため、user.tweetsという形でuserに関連したtweetsを取得できる。
irb(main):002:0> user.tweets Tweet Load (0.1ms) SELECT "tweets".* FROM "tweets" WHERE "tweets"."user_id" = ? LIMIT ? [["user_id", 1], ["LIMIT", 11]] => #<ActiveRecord::Associations::CollectionProxy [#<Tweet id: 1, body: "田中のツイート", user_id: 1, created_at: "2022-02-06 10:37:57", updated_at: "2022-02-06 10:37:57">]>
過去にuser.tweets.create
という記載に対して「なんで真ん中は複数形なんだろ?」と思ったものの深く考えずに放ったらかしにいておいたことが後々響いてきた。自分としてはユーザーのツイートを全部取得する意味でtweetsと複数形指定しているものと思っていたけれど、複数形で指定しているのはアソシエーションに記載しているかららしい🙄
user.rb
にhas_many :tweets
という形でアソシエーションを記載しているため、作成する際もuserの持つtweetsをcreateするというように記述するということ。
複数形になっていることからもわかりますが、@user.tweetsは複数のツイートが入った配列
ツイートをしたユーザーの取得
@tweet.user # userが単数形
userが単数なのはtweet.rb
にbelongs_to :user
と単数でアソシエーションを設定しているから。tweetが複数のuserを持っている訳ではないので。
def show @tweet=Tweet.find(params[:id]) @user=@tweet.user end
↑アソシエーションを設定していることで、ツイートからユーザーを取得することもできる。
また、tweet.user.email
のようにメソッドチェーンで関連先を一気に取得することもできる。長くなりすぎると可読性の悪化に繋がるので注意。
一対多まとめ
- 関連付けする際は子のテーブルに親子関係を表すforeign_keyカラムが必要
- foreign_keyには親のPrimary Keyが入る
親モデルに
has_many :子モデルの複数形
、子モデルにbelongs_to :親モデル単数形
↑これは何度もやっているけど、モデル作成の際、親モデル:referencesのようにカラム:データ型を記載して作成すると、自動的にbelongs_to :親モデルが記載されている。(rails6系以降は外部キー制約も自動付与)関連したモデル情報は、関連元のモデルインスタンス.関連先のモデル名で取得可能。関連先のモデル名とは、親か子かで単数か複数形で書き分けること。
多対多のテーブル
ユーザーは複数のツイートにいいねをしており、ツイート側も複数のユーザーからいいねをされている。こういった関係を多対多の関係という。
そしてRailsでは、多対多の関係をUserモデルとTweetモデルだけで実装することができない。実装するためには中間テーブルというものが必要になる。
中間テーブルとは
多対多のプログラムを実装する際には、お互いのforeign_keyが必要になる。
userはいいねしたツイートのIDを、ツイートはいいねしてきたユーザーのIDをいいねされる度に追加しなくてはならない。いいねの度にそういった追加をそれぞれでしなくてはならないのはとても面倒くさいので、お互いのIDだけを保存するテーブルを一つ作った方が管理の面で効率的ということらしい。
中間テーブルに保存するtweet_idとuser_idからいいね数や誰のいいねなのか判断する。
- あるツイートについたいいね数を見たい時、そのtweet_idと同じレコードに保存されているuser_idの数を見れば良い。
- あるユーザーのいいねしたツイートが見たい時、そのuser_idと同じレコードに保存されているtweet_idからツイートを検索すれば良い。
ルーティングの設定
resources :tweets do resource :favorites, only: %i[create destroy] end
tweetsの中にfavoritesをネストする(入れ子状にし、親子関係を明示的に表す)ことによって、いいねする際にforeign_keyの一つであるtweet_idの取得が楽になる。
またfavoriteの詳細ページは作らないため、resource
と単数にすることでfavoriteのidを省略している。
tweet_favorite DELETE /tweets/:tweet_id/favorite(.:format) favorites#destroy POST /tweets/:tweet_id/favorite(.:format) favorites#create
↑favoriteのIDが省略されている
アソシエーションの設定
user.rb
has_many :tweets has_many :favorites
tweet.rb
belongs_to :user has_many :favorites
favorite.rb
# model作成の際user:references tweet:referencesと作ることで自動的に記載されている。 belongs_to :user belongs_to :tweet
アソシエーションを組んだことでできること
@user.favorites
や@tweet.favorites
といった形でuser_id,tweet_id二つ組の情報としてそれぞれに関連したお気に入りが取得できるようになる。
いいねの一覧を取得する
mapメソッドを使ったやり方
mapメソッド
p [1,2,3].map{|n| n*3} #=> [3,6,9]
各要素に対してブロック内で指定した処理を施した配列を返す。
def show @user = User.find(params[:id]) @tweets = @user.tweets # mapメソッドを使い、favoriteをtweetの情報に変換 @favorite_tweets = @user.favorites.map{ |favorite| favorite.tweet }
@user.favorites.map{ |favorite| favorite.tweet }
この処理では、@user.favorites
でユーザーに関連したいいねの配列を取得しており、その配列の要素全てに対しmapメソッドでブロック内の処理をかけ、処理後の配列を返している。favorite.rbに
belongs_to :tweet
というアソシエーション設定をしているので、favorite.tweet
でfavoriteに関連するtweetの取得ができる。つまり
@user.favorites
で呼び出した配列に{|favorite| favorite.tweet }
という処理をかけて、配列の要素のツイートを全て呼び出している。
ビュー
<% @favorite_tweets.each do |tweet| %> <hr> <p><span>いいねツイート内容: </span><%= link_to tweet.body, tweet_path(tweet.id) %></p> <% end %>
★has_many :throughオプションを使う★
@favorite_tweets = @user.favorites.map{ |favorite| favorite.tweet }ではなく、has_many :through
を使うことで、ユーザーがいいねしたツイートを直接アソシエーションで取得することができる。。
どういうこと??
user.rbに下記の行を追加する。
has_many :favorite has_many :favorite_tweets, through: :favorites, source: :tweet #追加!!
順番に紐解いていく
has_many :favorite_tweets, through: :favorites, source: :tweet
source
参照元のモデルを指すオプションです。 これを指定することでアソシエーションでメソッドチェーンする時の名称を変更することができます
tweet.user.emailをfavorite_tweets.user.emailみたいな形に名称変更ができる!!
アソシエーションはhas_many :モデル名
の形だと何となくで理解していたために、favorite_tweets
とかいうモデルが必要なのかと猛烈に勘違いしていたけれど、sourceオプションによって、tweetを参照することで呼び出し時の名称が変えられるというだけのことだった。
なぜ名称を変える必要があるのか
本当はhas_many :tweets, through: :favorites
としたいが、1行目でUserとTweetのアソシエーションhas_many :tweets
が既に記述されているため名称を変更している。
(重複を避けるため)
これにより@user.favorite_tweets
でユーザーがいいねしたツイートが取得できる。
user_controller.rb
@favorite_tweets = @user.favorites.map{ |favorite| favorite.tweet } ↓変更 @favorite_tweets = @user.favorite_tweets
参考にする記事
【初心者向け】丁寧すぎるRails『アソシエーション』チュートリアル【幾ら何でも】【完璧にわかる】🎸 - Qiita