n対nの関係で中間テーブルにid以外のカラムを持たせ登録・編集を行う方法

Ruby On Railsでのアソシエーションの応用のお話。

下記のようなUserモデルとGroupモデルにn対nの関係を持たせ、UserにGroupの所有者情報を持たせたい時、中間テーブルに所有者情報を持たせた方がDB設計的になんとなく綺麗な気がする。 f:id:t1gerk1ngd0m:20190826225801p:plain

しかし、通常中間テーブルのレコードには直接アクセス出来ないため、モデルのアソシエーションで一工夫する必要がある。

n対nのアソシエーション

通常のn対nの関係は、以下のように表現する。まずはschema.rbから、

db/schema.rb

ActiveRecord::Schema.define(version: 2019_08_26_011656) do

  create_table "groups", force: :cascade do |t|
    t.string "name", null: false
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

  create_table "users", force: :cascade do |t|
    t.string "name", null: false
    t.string "email", null: false
    t.string "password_digest", null: false
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

  create_table "group_users", force: :cascade do |t|
    t.bigint "group_id"
    t.bigint "user_id"
    t.index ["group_id"], name: "index_group_users_on_group_id"
    t.index ["user_id"], name: "index_group_users_on_user_id"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

end

次にモデル。has_manythroughオプションに中間テーブルのシンボルを渡す事でn対nを表現している。

app/models/group.rb

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

app/models/user.rb

class User < ApplicationRecord
  has_many :tasks, dependent: :destroy
  has_many :group_users, dependent: :destroy
end

app/models/group_user.rb

class GroupUser < ApplicationRecord
  belongs_to :group
  belongs_to :user
end

中間テーブルにカラムを持たせる方法

上記で作成したGroup_userにGroupの所有者、つまりownerを持たせたい。 その場合、Groupモデルに下記のように記述を加えていく。

app/models/group.rb

class Group < ApplicationRecord

  has_many :group_users, dependent: :destroy
  has_many :users, through: :group_users

  has_one :owner_group_user, -> {where(owner: true)}, class_name: 'GroupUser'
  has_one :owner_user, through: :owner_group_user, source: :user
end

# migrationファイルも忘れずに実行する
db/schema.rb

ActiveRecord::Schema.define(version: 2019_08_26_011656) do

  create_table "group_users", force: :cascade do |t|

# ~ 中略 ~

   t.boolean "owner", default: false, null: false

  end
end

中間テーブルであるowner_group_userでownerがtrueなレコードを取得し、それを使ってUserモデルからowner_userを取得している。 class_name: 'GroupUser'で用いるモデルはGroupUserであることを明記し、source: :userでUserモデルの情報を参照することを明記している。 これでViewで、

group.owner_user.name

と書くと、Groupの所有者名を取得することが出来る。 またController側でも、Viewから送られてくるparamsにowner_userの値をmergeすればそのままcreateupdateにparamsを渡す事で、owner_userを格納できる。

def group_params
  params.require(:group).permit(
    :name, 
    { user_ids: [] }
  ).merge(owner_user: current_user)
end

以上!