【Ruby on Rails】アソシエーション(has_many・belongs_to)の書き方

Ruby

こんにちは、かつコーチです。

Railsでモデルを作っていると、必ず出てくるのが「モデル同士の関連付け」です。

たとえば「記事にはたくさんのコメントがつく」「コメントは1つの記事に属する」といった関係を、Railsではアソシエーション(モデル同士の関連付けを宣言する仕組み)を使って表現します。

書き方自体はシンプルですが、外部キーの向きやbelongs_tohas_manyの対応を間違えると、原因がわかりにくいエラーに悩まされがちです。

今回は、記事(Article)とコメント(Comment)というよくある例を使って、アソシエーションの基本を丁寧に解説します。

アソシエーションの基本の書き方

has_manyとbelongs_toの対応関係

Railsのアソシエーションで最初に覚えるべきは、has_manybelongs_toはセットで使うという点です。

「1つの記事に複数のコメントがつく」という関係を、まずマイグレーションで表現します。

rails generate model Article title:string body:text
rails generate model Comment body:text article:references

article:referencesと指定すると、Commentテーブルにarticle_idという外部キー(他のテーブルの主キーを参照する列)が自動で追加されます。

生成されるマイグレーションファイルは以下のようになります。

# db/migrate/xxxxxx_create_comments.rb
class CreateComments < ActiveRecord::Migration[8.0]
  def change
    create_table :comments do |t|
      t.text :body
      t.references :article, null: false, foreign_key: true

      t.timestamps
    end
  end
end

マイグレーションを実行してテーブルを作成します。

rails db:migrate

続いて、モデルにアソシエーションを宣言します。

# app/models/article.rb
class Article < ApplicationRecord
  has_many :comments, dependent: :destroy
end
# app/models/comment.rb
class Comment < ApplicationRecord
  belongs_to :article
end

「複数持つ側」にhas_many、「1つに属する側」にbelongs_toを書くのが基本のルールです。

外部キー(article_id)を持っているテーブルのモデルに、必ずbelongs_toを書くと覚えておくと迷いません。

has_oneで1対1の関係を表す

「1人のユーザーに1つのプロフィールが紐づく」のような1対1の関係にはhas_oneを使います。

# app/models/user.rb
class User < ApplicationRecord
  has_one :profile, dependent: :destroy
end
# app/models/profile.rb
class Profile < ApplicationRecord
  belongs_to :user
end

has_manyとの違いは、呼び出したときに配列ではなく単体のオブジェクトが返る点だけです。

user.profile       # => Profileオブジェクト(単数)
article.comments   # => Commentの配列(複数)

実際に関連データを操作する

アソシエーションを定義すると、関連レコードの作成・取得がとても簡単になります。

article = Article.create!(title: "初めての投稿", body: "よろしくお願いします")

# 記事に紐づくコメントを作成する
article.comments.create!(body: "参考になりました")

# 記事に紐づくコメント一覧を取得する
article.comments
# => [#<Comment id: 1, body: "参考になりました", article_id: 1>]

# コメントから記事を逆にたどる
comment = article.comments.first
comment.article.title
# => "初めての投稿"

article.comments.create!のように、アソシエーション経由でレコードを作ると、article_idが自動的にセットされるため書き漏れの心配がありません。

よくあるつまずきポイント・エラー対処

外部キーの向きを逆に書いてしまう

私が実際に経験したつまずきが、belongs_tohas_manyをどちらのモデルに書くか迷って逆にしてしまったケースです。

❌ Before:外部キーがないモデルにbelongs_toを書いてしまう

# app/models/article.rb
class Article < ApplicationRecord
  belongs_to :comments
end

このコードを実行すると、以下のようなエラーが出ました。

NameError: uninitialized constant Article::Comments

belongs_toは単数形のシンボルを想定する書き方なので、複数形の:commentsを渡した時点で、RailsがCommentsという単数の関連クラスを探しに行ってしまい、見つからずにエラーになります。

✅ After:外部キーを持つ側にbelongs_toを、持たない側にhas_manyを書く

# app/models/article.rb
class Article < ApplicationRecord
  has_many :comments, dependent: :destroy
end

# app/models/comment.rb
class Comment < ApplicationRecord
  belongs_to :article
end

判断基準はシンプルで、「テーブルにarticle_idという外部キーの列があるかどうか」です。

外部キーの列がある側(今回はComment)にbelongs_toを書くと覚えておけば、迷わなくなります。

dependent: :destroyを忘れて孤立レコードが残る

もう一つよくあるのが、記事を削除したときにコメントが削除されずに残ってしまうケースです。

# ❌ dependent オプションなし
class Article < ApplicationRecord
  has_many :comments
end

article.destroy
# コメントは削除されず、article_idが存在しないレコードとしてDBに残る

この状態で後からComment.find(1).articleのように親を参照すると、ActiveRecord::RecordNotFoundが発生することがあります。

dependent: :destroyを指定しておけば、記事を削除したタイミングで紐づくコメントも自動的に削除されるため、孤立レコードを防げます。

class Article < ApplicationRecord
  has_many :comments, dependent: :destroy
end

削除件数が多くなる場合は、1件ずつコールバックを実行する:destroyではなく、SQLのDELETE文でまとめて削除するdependent: :delete_allを検討すると処理が速くなります。

まとめ

この記事のポイント

  • 外部キー(article_idなど)を持つ側のモデルにbelongs_toを書く
  • 複数のレコードを持つ側のモデルにhas_manyを書く
  • 1対1の関係にはhas_oneを使い、単体のオブジェクトが返る
  • dependent: :destroyを忘れると、親を削除したときに孤立レコードが残る
  • アソシエーションを定義するとarticle.comments.create!のように関連レコードを簡単に操作できる

次に読むべき記事

→ 次の記事:バリデーションの基本

タグ: Ruby on Rails, 中級者向け, データベース

タイトルとURLをコピーしました