railsで多対多のテーブルを実装する

October 16, 2014
ruby rails rails4 model 多対多 ER図 migrate table テーブル

railsでmodelを作成するとき、多対多の実装方法が分かりづらかったのでまとめてみた。 railsでの多対多の書き方はいろいろあるらしい。

  1. 中間テーブルを作成し、has_many :throughを使う
  2. has_and_belongs_to_manyを使う
  3. create_join_tableを使う

1 → 3の順に情報が多い気がする。手軽さ的には 3 → 1の順かな。

個人的には1がおすすめ。理由としては、中間テーブル名を用いてDBにアクセス出来ることと、中間テーブルに属性を後で付加できるから。

例として画像にtagデータを持たせるテーブルを考える。

Toxi法で多対多のテーブルを作成する。railsの規約に沿ってDB設計をした。ER図は以下の通り。

1の場合

2, 3の場合

imagesテーブルにはnameURLの情報を保存し、tagsテーブルにはnameの情報を保存させる。tag_idimage_idは外部キー。

違いは中間テーブルの有無だけで、やってることは同じ。実際にはrailsが中間テーブルを自動生成してくれるので、2,3も1のようなテーブルになる。

ではER図を元にrailsで実装してみる。

1. 中間テーブルを作成し、has_many :throughを使った場合

rails g modelコマンドを用いて、テーブルを作成する。

$ rails g model image name:string url:string
$ rails g model tag name:string
$ rails g model image_tag image:references tag:references

テーブル名が単数形であることに注意。railsではActive Record経由で自動的に複数形にしてくれる。

中間テーブルと関連付けさせる。

class Image < ActiveRecord::Base
  has_many :image_tags
  has_many :tags, :through => :image_tags
end
class Tag < ActiveRecord::Base
  has_many :image_tags
  has_many :images, :through => :image_tags
end
class ImageTag < ActiveRecord::Base
  belongs_to :image
  belongs_to :tag
end

ちなみにmigrateファイルは以下のようになっている。

class CreateImages < ActiveRecord::Migration
  def change
    create_table :images do |t|
      t.string :name
      t.string :url

      t.timestamps
    end
  end
end
class CreateTags < ActiveRecord::Migration
  def change
    create_table :tags do |t|
      t.string :name

      t.timestamps
    end
  end
end
class CreateImageTags < ActiveRecord::Migration
  def change
    create_table :image_tags do |t|
      t.references :image, index: true, null: false
      t.references :tag, index: true, null: false

      t.timestamps
    end
  end
end

外部キーにnot null制約を付加している。

最後にマイグレートする

$ rake db:migrate

2. has_and_belongs_to_manyを使った場合

rails g modelコマンドを用いて、テーブルを作成する。

$ rails g model image name:string url:string
$ rails g model tag name:string
$ rails g migration create_images_tags image:references tag:references

中間テーブルは作らない。migrationファイルで関連付けさせる。

テーブルを関連付けさせる。

class Image < ActiveRecord::Base
  has_and_belongs_to_many :tags
end
class Tag < ActiveRecord::Base
  has_and_belongs_to_many :images
end

migrationファイルを編集する

元のファイル

class CreateImagesTags < ActiveRecord::Migration
  def change
    create_table :images_tags do |t|
      t.references :image, index: true
      t.references :tag, index: true
    end
  end
end

これをhas_and_belongs_to_manyの規約で編集する

has_and_belongs_to_many アソシエーションの規約

  • 中間テーブルを作成しなければならない。
  • 中間テーブルのテーブル名は参照先のテーブル名を辞書順に「_」で連結しなければならない。(※)
  • 中間テーブルの主キー列を無効化しなくてはならない。
  • 中間テーブルの外部キー列は「参照先のモデル名_id」の形式にしなければならない。
  • 中間テーブルのタイムスタンプ列を削除しなくてはならない。

変更後

class CreateImagesTags < ActiveRecord::Migration
  def change
    create_table :images_tags, id: false do |t|
      t.references :image, index: true, null: false
      t.references :tag, index: true, null: false
    end
  end
end

外部キーにnot null制約を付加している。

変更前にid: falseを追加しただけ。外部キー名を明記しないのは、自動生成してくれるため。

ちなみに他のmigrateファイルは以下のようになっている。

class CreateImages < ActiveRecord::Migration
  def change
    create_table :images do |t|
      t.string :name
      t.string :url

      t.timestamps
    end
  end
end
class CreateTags < ActiveRecord::Migration
  def change
    create_table :tags do |t|
      t.string :name

      t.timestamps
    end
  end
end

最後にマイグレートする

$ rake db:migrate

3. create_join_tableを使った場合

rails g modelコマンドを用いて、テーブルを作成する。

$ rails g model image name:string url:string
$ rails g model tag name:string
$ rails g migration create_join_table_images_tags image tag

中間テーブルは作らない。migrationファイルで関連付けさせる。

テーブルを関連付けさせる。

class Image < ActiveRecord::Base
  has_and_belongs_to_many :tags
end
class Tag < ActiveRecord::Base
  has_and_belongs_to_many :images
end

migrationファイルを編集する

元のファイル

class CreateJoinTableImagesTags < ActiveRecord::Migration
  def change
    create_join_table :images, :tags do |t|
      # t.index [:image_id, :tag_id]
      # t.index [:tag_id, :image_id]
    end
  end
end

コメントアウトを外す。

変更後

class CreateJoinTableImagesTags < ActiveRecord::Migration
  def change
    create_join_table :images, :tags do |t|
      t.index [:image_id, :tag_id]
      t.index [:tag_id, :image_id]
    end
  end
end

ちなみに他のmigrateファイルは以下のようになっている。

class CreateImages < ActiveRecord::Migration
  def change
    create_table :images do |t|
      t.string :name
      t.string :url

      t.timestamps
    end
  end
end
class CreateTags < ActiveRecord::Migration
  def change
    create_table :tags do |t|
      t.string :name

      t.timestamps
    end
  end
end

最後にマイグレートする

$ rake db:migrate