has_many(1対多アソシエーション)
| 対応: | Rails 6(2019) |
|---|
『Ruby on Rails』のhas_manyは、ActiveRecordが提供するアソシエーション定義メソッドで、1対多の関係をモデルに設定します。例えば「1人のユーザーは複数の投稿を持つ」といった関係を定義するときに使用します。has_manyを定義すると、関連するレコードを取得・作成・削除するための便利なメソッドが自動的に追加されます。
構文
# app/models/モデル名.rb に記述します class モデル名 < ApplicationRecord # 基本的な定義(外部キーは「モデル名_id」が規約で自動的に使用されます) has_many :関連モデル名の複数形 # オプションを指定する場合 has_many :関連モデル名の複数形, オプション名: 値 end
オプション一覧
| オプション | 概要 |
|---|---|
| class_name: | 関連するモデルのクラス名を明示的に指定します。関連名とクラス名が異なる場合に使用します。 |
| foreign_key: | 外部キーのカラム名を指定します。規約(モデル名_id)と異なる場合に使用します。 |
| primary_key: | 結合に使用する親モデル側のキーカラムを指定します。デフォルトはidです。 |
| dependent: | 親レコードを削除したときの子レコードの扱いを指定します。:destroy・:delete_all・:nullify・:restrict_with_errorなどが使用できます。 |
| through: | 中間テーブルを経由した多対多のアソシエーションを定義するときに、中間モデルの関連名を指定します。 |
| source: | through:を使用する際に、中間モデル側での関連名を明示的に指定します。 |
| scope | ブロック形式でデフォルトのスコープ(並び順や条件)を指定します。 |
| inverse_of: | 双方向アソシエーションの反対側の関連名を指定し、オブジェクトの同一性を保証します。 |
サンプルコード
ブログサイトを例に、has_manyの基本的な使い方を示します。1人のユーザーが複数の記事を持ち、1つの記事が複数のコメントを持つ構成です。
# app/models/user.rb class User < ApplicationRecord # ユーザーは複数の記事を持ちます(articles テーブルの user_id カラムで紐付けます) has_many :articles, dependent: :destroy # ユーザーは複数のコメントを持ちます has_many :comments, dependent: :destroy end # app/models/article.rb class Article < ApplicationRecord # 記事は1人のユーザーに属します belongs_to :user # 記事は複数のコメントを持ちます has_many :comments, dependent: :destroy end # app/models/comment.rb class Comment < ApplicationRecord # コメントは1つの記事に属します belongs_to :article end
# app/controllers/articles_controller.rb の一部 # ---- has_many で追加される主なメソッドの使い方 ---- # ログイン中のユーザーの記事一覧を取得します(SELECT * FROM articles WHERE user_id = ?) @articles = current_user.articles # 特定ユーザーの記事を条件付きで絞り込みます @published = current_user.articles.where(published: true).order(created_at: :desc) # 関連レコードを新規作成します(user_id が自動的にセットされます) @article = current_user.articles.build(article_params) # 関連レコードを作成してそのまま保存します @article = current_user.articles.create!(article_params) # 関連レコードの件数を取得します(SELECT COUNT(*) を発行します) @count = current_user.articles.count
# ---- through: を使った多対多アソシエーションの例 ---- # app/models/user.rb class User < ApplicationRecord # 中間テーブル(followings)を経由して、フォロー中のユーザー一覧を取得します has_many :followings, foreign_key: :follower_id, class_name: 'Follow', dependent: :destroy has_many :following_users, through: :followings, source: :followed end # app/models/follow.rb class Follow < ApplicationRecord # follower_id(フォローする側)と followed_id(フォローされる側)を持ちます belongs_to :follower, class_name: 'User' belongs_to :followed, class_name: 'User' end
# ---- dependent: のオプション比較 ---- class User < ApplicationRecord # :destroy → 削除前に各子レコードの before_destroy コールバックを呼び出して削除します has_many :articles, dependent: :destroy # :delete_all → コールバックを呼ばずに一括 DELETE します(高速ですがコールバック非実行に注意) has_many :logs, dependent: :delete_all # :nullify → 親削除時に子の外部キーを NULL にします(子レコード自体は残ります) has_many :comments, dependent: :nullify # :restrict_with_error → 子レコードが存在する場合は親の削除を拒否してエラーを返します has_many :orders, dependent: :restrict_with_error end
概要
has_manyを定義すると、user.articles・user.articles.build・user.articles.create・user.articles.count・user.articles << article・user.articles.delete(article)など、関連レコードを操作するメソッドが自動的に追加されます。これらのメソッドは内部でSQLを発行するため、必要に応じてincludes / joinsを活用してN+1問題を防ぐことが重要です。
dependent:オプションは、親レコード削除時の子レコードの扱いを決める重要な設定です。:destroyはコールバックを実行しながら1件ずつ削除するため安全ですが、件数が多い場合はパフォーマンスに影響します。大量データを扱う場合は:delete_allの使用も選択肢になります。なお、dependent:を省略すると親レコードを削除しても子レコードはそのままデータベースに残る点に注意が必要です。
1対多の反対側(子モデル側)にはbelongs_toを定義します。ActiveRecordの基本もあわせてご覧ください。
よくあるミス
dependent: 未指定で子レコードが残る
dependent:を指定しないと、親レコードを削除しても子レコードはデータベースにそのまま残ります。子レコードを一緒に削除したい場合はdependent: :destroyまたはdependent: :delete_allを、外部キーをNULLにして残したい場合はdependent: :nullifyを指定します。
ng_example.rb
class User < ApplicationRecord # dependent: を指定していないため、ユーザー削除後も articles が残り続ける has_many :articles end
ok_example.rb
class User < ApplicationRecord # ユーザー削除時に関連する記事も削除する has_many :articles, dependent: :destroy end
N+1問題:ループ内で関連を呼び出すと SQL が大量に発行される
ループ内で関連レコードにアクセスするたびにSQLが1件ずつ発行されます。includesで事前にまとめて読み込むことで、発行されるSQLを最小限に抑えられます。
ng_example.rb
# User ごとに SELECT * FROM articles WHERE user_id = ? が発行される users = User.all users.each do |user| puts user.articles.count end
ok_example.rb
# includes で articles を一括取得し、SQL の発行を 2 回に抑える users = User.includes(:articles).all users.each do |user| puts user.articles.size end
inverse_of 未指定によるオブジェクト不一致
inverse_of:を指定しないと、親子で同じインスタンスが共有されず、メモリ上でオブジェクトの不一致が起きることがあります。双方向アソシエーションではinverse_of:を指定しておくと安全です。
ng_example.rb
class User < ApplicationRecord has_many :articles end class Article < ApplicationRecord belongs_to :user end user = User.first article = user.articles.first # user と article.user が同一インスタンスではない場合がある puts user.equal?(article.user) # => false になりうる
ok_example.rb
class User < ApplicationRecord has_many :articles, inverse_of: :user end class Article < ApplicationRecord belongs_to :user, inverse_of: :articles end user = User.first article = user.articles.first puts user.equal?(article.user) # => true
記事の間違いや著作権の侵害等ございましたらお手数ですがこちらまでご連絡頂ければ幸いです。