Rails Diary

プログラミングの学習記録です。

アソシエーションについて復習する

経緯

  • 多対多のテーブルが全く理解できない
  • モデルに記載されている内容が分からない
  • 分からなすぎて何から質問したら良いかが正直分からない(重症)
  • とりあえずアソシエーションの不理解が一つの要因としてありそうなので復習する
  • 完全自分用メモ

qiita.com

↑こちらのサイトを参考にチュートリアルを進めました。
ブックマーク課題で自分が知りたかったことはほぼここに書いてあった😭

アソシエーション・・・モデル間の関連付け

ツイッターの下位互換のような一言だけ投稿できるだけのアプリがあるとする。あるのは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.rbhas_many :tweetsという形でアソシエーションを記載しているため、作成する際もuserの持つtweetsをcreateするというように記述するということ。

複数形になっていることからもわかりますが、@user.tweetsは複数のツイートが入った配列

ツイートをしたユーザーの取得

@tweet.user 
# userが単数形

userが単数なのはtweet.rbbelongs_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 }
  1. @user.favorites.map{ |favorite| favorite.tweet }
    この処理では、@user.favoritesユーザーに関連したいいねの配列を取得しており、その配列の要素全てに対しmapメソッドでブロック内の処理をかけ、処理後の配列を返している。

  2. favorite.rbにbelongs_to :tweetというアソシエーション設定をしているので、favorite.tweetでfavoriteに関連するtweetの取得ができる。

  3. つまり@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

掲示板にお気に入り機能を実装する① - Programming Learning Diary

【Rails】 アソシエーションを図解形式で徹底的に理解しよう! | Pikawaka