Railsの関連付け(association)について

1. 関連付けとは?

2つのActive Recordモデル同士のつながりのことです。
どのモデルとどのモデルの間に関連があるのか、その関連はどのような形態なのかを定義することができます。

一つの例として以下のように記述します。

class User < ApplicationRecord
  has_many :posts
end

class Post < ApplicationRecord
  belongs_to :user
end

上の関連付けでは、 User と Post の間に1対多の関連付けを定義しています。

2. 関連付けの種類

Railsの関連付けにはいくつかの種類があります。この記事で取り上げる関連付けを以下に示します。

  • belongs_to
  • has_one
  • has_many
  • has_many :through
  • has_one :through
  • has_and_belongs_to_many

2.1. belongs_to関連付け

belongs_toは1対1の関連付けを表します。belongs_toの宣言を行ったモデルのインスタンスは別のモデルに従属 (belongs to) します。

例えば、記事(Post)とユーザー(User)の関係を考えましょう。

1つの記事は1人のユーザーに所属(従属)しています。つまり、記事はユーザーにbelongs_toしていると言えます。

この場合のモデルは以下のようになります。

class Post < ApplicationRecord
  belongs_to :user
end

class User < ApplicationRecord
end

Postモデルではbelongs_to :userと定義することで、Userモデルとの1対1の所属関係を表現しています。

belongs_toでは、外部キーとして所属するモデルのidが自動でカラムとして追加されます。 この場合、postsテーブルにはuser_idというカラムができます。

また、belongs_toを定義したモデルでは、所属するモデルのインスタンスに簡単にアクセスできるようになります。

post = Post.first
post.user # postに関連付いたuserオブジェクトを取得できる

この関連付けに対応するマイグレーションは例えば、以下のようになります。

class CreatePosts < ActiveRecord::Migration[7.1]
  def change
    create_table :users do |t|
      t.string :name
      t.timestamps
    end

    create_table :posts do |t|
      t.belongs_to :user
      t.datetime :published_at
      t.timestamps
    end
  end
end

2.2. has_one関連付け

has_oneもbelongs_toと同様に1対1の関連付けを表します。ただし、belongs_toとは逆に関連付けしたモデルを従属させます。

例えば、ユーザーとプロフィールの関係を考えましょう。

1人のユーザーは、1つのプロフィールを持っています。 逆に、1つのプロフィールは1人のユーザーに所属しています。

この場合のモデルは以下のようになります。

class User < ApplicationRecord
  has_one :profile
end

class Profile < ApplicationRecord
  belongs_to :user
end

Userモデルではhas_one :profileと定義することで、Profileモデルとの1対1の関係を表現しています。

has_oneの場合、外部キーは相手側のモデルに置かれます。 この例では、profilesテーブルにuser_idというカラムが追加されます。

また、has_oneを定義した側では、関連するモデルのインスタンスにアクセスできます。

user = User.first 
user.profile # userに関連づいたprofileオブジェクトを取得できる

この関連付けに対応するマイグレーションは例えば、以下のようになります。

class CreateUsers < ActiveRecord::Migration[7.1]
  def change
    create_table :users do |t|
      t.string :name
      t.timestamps
    end

    create_table :profiles do |t|
      t.belongs_to :user
      t.string :profile
      t.timestamps
    end
  end
end

このように、has_oneによって「1つ持っている」関係を表現できます。
belongs_toとhas_oneは同じ1対1の関係ですが、視点が逆なので注意が必要です。

2.3. has_many関連付け

has_many関連付けは1対多の関連付けを表します。has_oneと似ていますが、一つのインスタンスが相手のインスタンスを複数持っている点が異なります。

例えば、ユーザーと記事の関係を考えましょう。

1人のユーザーは、多数の記事を書くことができます。 逆に、1つの記事は1人のユーザーに所属しています。

この場合の定義は以下のようになります。

class User < ApplicationRecord
  has_many :posts
end

class Post < ApplicationRecord
  belongs_to :user
end

Userモデルではhas_many :postsと定義することで、1対多の関係を表現しています。

has_manyの場合も、belongs_to側のモデルに外部キーが置かれます。 postsテーブルにはuser_idが追加されます。

また、has_manyを定義した側では、関連するモデルのコレクションにアクセスできます。

user = User.first
user.posts # userが書いた複数のpostオブジェクトを取得できる

この関連付けに対応するマイグレーションは例えば、以下のようになります。

class CreateUsers < ActiveRecord::Migration[7.1]
  def change
    create_table :users do |t|
      t.string :name
      t.timestamps
    end

    create_table :posts do |t|
      t.belongs_to :user
      t.timestamps
    end
  end
end

このように、has_manyを使うことで1対多の関連付けができます。アプリケーションではよく利用する関連付けの1つです。

2.4. has_many :through関連付け

has_many :through関連付けは2つのモデルの多対多の関連付けを表現します。中間テーブルを介して、それを経由(through)して2つのモデルを関連付けます。

例えば、ユーザーとグループの関係を考えましょう。

  • 1人のユーザーは複数のグループに所属できます
  • 1つのグループは複数のユーザーを含むことができます

この場合の定義は以下のようになります。

class User < ApplicationRecord
  has_many :group_users
  has_many :groups, through: :group_users
end

class Group < ApplicationRecord
  has_many :group_users
  has_many :users, through: :group_users
end

class GroupUser < ApplicationRecord
  belongs_to :user
  belongs_to :group
end

GroupUserが中間テーブルとなり、このテーブルを介してUserとGroupは多対多で関連付けられます。

このように、has_many :throughを使うことで、複雑な多対多の関連付けが定義できます。

この関連付けに対応するマイグレーションは例えば、以下のようになります。

class CreateUsers < ActiveRecord::Migration[7.1]
  def change
    create_table :Users do |t|
      t.string :name
      t.timestamps
    end

    create_table :Groups do |t|
      t.string :name
      t.timestamps
    end

    create_table :GroupUsers do |t|
      t.belongs_to :user
      t.belongs_to :group
      t.timestamps
    end
  end
end

またhas_many :throughは、中間テーブルにカラムを追加して、関連付けに属性を持たせることもできます。

2.5. has_one :through関連付け

has_one :through関連付けは、2つのモデル間の1対1の関連付けを中間テーブルを介して表現するものです。

例えば、ユーザーとプロフィールの関係を考えましょう。 ユーザー登録時に住所情報も保存したい場合を考えます。

  • 1人のユーザーは1つのプロフィールを持つ
  • 1つのプロフィールは1つの住所を持つ

この場合の定義は以下のようになります。

class User < ApplicationRecord
  has_one :profile
end

class Profile < ApplicationRecord
  belongs_to :user
  has_one :address
end

class Address < ApplicationRecord
  belongs_to :profile
end

この場合、UserとAddressは直接関連付けされていませんが、中間のProfileモデルを介することで、 1対1の関連付けが表現できます。

この関連付けに対応するマイグレーションは例えば、以下のようになります。

class CreateUsers < ActiveRecord::Migration[7.1]
  def change
    create_table :users do |t|
      t.string :name
      t.timestamps
    end

    create_table :profiles do |t|
      t.belongs_to :user
      t.timestamps
    end

    create_table :addresses do |t|
      t.belongs_to :profile
      t.timestamps
    end
  end
end

2.6. has_and_belongs_to_many関連付け

has_and_belongs_to_many関連付けは、2つのモデル間の多対多の関連付けを、中間モデルを使用することなく直接定義できるものです。ただし、データベースに中間テーブルは必要です。

例えば、ユーザーとスキルの関係を考えましょう。

  • 1人のユーザーは複数のスキルを持っている
  • 1つのスキルは複数のユーザーが持っている

この場合の定義は以下のようになります。

class User < ApplicationRecord
  has_and_belongs_to_many :skills
end

class Skill < ApplicationRecord
  has_and_belongs_to_many :users
end

この定義により、バックグラウンドでusers_skillsという中間テーブルが作成されます。 このテーブルを介して、ユーザーとスキルは多対多で関連付けられます。

また、関連するレコードには以下のようにアクセスできます。

user = User.first
user.skills # ユーザーの持つスキル

skill = Skill.first
skill.users # そのスキルを持つユーザー

このように、has_and_belongs_to_manyは中間テーブルを意識せずに、 多対多の関連付けを定義できる便利な関連付けです。

3. 関連付けを使うメリット

関連付けを行うと、モデル間の関連データを簡単に操作できるようになります。

例えば、UserとPostの関連について考えてみましょう。

関連付けを行っていない場合は、新しいpostを作成したいときに以下のようなコードを実行する必要があります。

@post = Post.create(content: "Hello world", user_id: @user.id)

同じようにユーザーを1人削除する場合は、ユーザーとそのユーザーのポストをすべて削除する必要があります。

@posts = Post.where(user_id: @user_id)
@posts.each do |post|
  post.destroy
end
@user.destroy

以下の関連付けを定義しておくと上記の操作をもっと簡単に記述することができます。

class User < ApplicationRecord
  has_many :posts, dependent: :destroy
end

class Post < ApplicationRecord
  belongs_to :user
end

上記の関連付けを記述した場合は以下のように1行で新しいポストを追加することができるようになります。

@post = @user.posts.create(content: "Hello World")

同じように1行でuserと関連するpostをすべて削除することができます。

@user.destroy

4. まとめ

関連付けを使うことでモデル同士の関連を簡単に扱えるようになります。そのことにより適切に使用すれば、アプリケーションの保守性や拡張性を向上させることができます。