現在、討論するためのプラットフォームWebアプリを開発してます。そこで、ユーザーAが管理者となって討論ルームを作成し、そのルームに複数のユーザーが参加するわけですが、その仕組みを作る方法についてアウトプットします。方法は知っている限り2つあります。
1つ目はルーム作成者(=オーナー)を管理するためのテーブルを他に作る方法。
2つ目はルームテーブル内にオーナーを管理するためのカラムを追加する方法。
今回は2つ目の方法について、特に難しかったアソシエーションを中心に解説します。難しかった原因は「ユーザーとルームは多対多のアソシエーションであるのにも関わらず、オーナー管理の部分だけ見るとユーザー:ルームは1:多の関係になってしまうことです(ルームには必ず1人だけ管理者が存在し、ユーザーは複数のルームの管理者になれる)」というものです。解決方法を要約すると、「ユーザーとルームの多対多アソシエーションを作成するのに加え、ルームからユーザーへblongs_toのアソシエーションを組む」となります。
1つ目の方法の概要やメリットについては最後におまけで少しだけ言及します。
前提
機能について
あるユーザーAがいます。そのAさんがルームを作成します。ルームを作成したAさんがそのルームの管理者となります。ルームには他のユーザーが参加します。また、ユーザーAは他にもルームを作ることができます。
⇨従って、ユーザーとルームは多対多の関係となります。
機能を作成するためにやること
やるべきことは大雑把に整理すると2つです。1つは中間テーブルを作り、ユーザーとルームの関係を管理すること。もう1つは管理者を管理するためにルームテーブルに管理者のカラムを追加すること。
マイグレーションファイル
今回ユーザーはdeviseで作成しています。
class DeviseCreateUsers < ActiveRecord::Migration[6.1]
def change
create_table :users do |t|
## Database authenticatable
t.string :email, null: false, default: ""
t.string :encrypted_password, null: false, default: ""
t.string :nickname, null: false
#(中略)
end
#(中略)
end
ルームに追加しているリファレンス型カラムのownerが管理者を登録するためのカラムとなります。
class CreateRooms < ActiveRecord::Migration[6.1]
def change
create_table :rooms do |t|
t.string :title, null: false
t.references :owner
t.datetime :deadline, null: false
t.timestamps
end
end
end
マイグレート後、アソシエーションを組みます。
アソシエーション
ユーザーモデル内ではルームテーブルと中間テーブルに対するアソシエーションを記述します。
class User < ApplicationRecord
#(中略)
#association
has_many :user_rooms,dependent: :destroy
has_many :rooms ,through: :user_rooms
end
ここからが肝で、ルームモデルには多対多のためのアソシエーションに加えて、管理者カラムのためのアソシエーションを組みます。
class Room < ApplicationRecord
has_many :user_rooms,dependent: :destroy
has_many :users, through: :user_rooms
#↓で管理者を登録。
belongs_to :owner, class_name: "User", foreign_key: :owner_id
end
解決方法の解説
belongs_to :owner, class_name: "User", foreign_key: :owner_id
ルームの管理者(以下:owner)は1人なので、roomからownerへbelongs_toの関係があることがわかります。ownerテーブルがあればいいのですが、ownerは結局はuserと同じなので、仮にownerテーブルを作ると、テーブルが増えて手間も増えます。だから、ここでは、userテーブルを利用するけれども、テーブルの名前をuserではなくownerと読み替えさせるようにします。読み替えさせる方法がclass_nameであり、上記を再度書き換えると下記になります。
belongs_to :自分で設定するテーブルの読み方, class_name: "元となるテーブル名", foreign_key: :owner_id
"foreign_key: :owner_id"は記述しなくてもテーブルの作成やオーナー管理は問題なく行えます。しかし、記述しない場合、ユーザーを削除しようとするときに、エラー原因になります。ownerカラムはマイグレーションファイルで設定した通り、リファレンス型のカラムなので、外部キーが必要になります。ただ、外部キー設定をマイグレーションファイル内に記述すると、外部キー先との関係が多対多になっているため、これもまたエラーの原因になります。ゆえに、アソシエーション部分で、ownerのカラムが外部キーになっていることを宣言します。※owner_idと記述しているのは、実はカラム名がowner_idになっているからです。マイグレーションファイルで"t.reference :カラム名"として記述しているので、テーブル作成時にはカラム名_idとしてカラムに登録されます。
自信満々に書いているように見えますが、間違っていたらコメントくださるとありがたいです。
おまけ
記事冒頭で紹介した解決方法「ルーム作成者(=オーナー)を管理するためのテーブルを他に作る方法」ですが、実は自分は最初この方法でやっていました。問題なく機能は実現できていたのですが、記述の多さやふと振り返った時に可読性が悪いなどがあり、思い切ってテーブル設計全体を変更することにしました。ちなみに解決方法のイメージとしては下図みたいな感じです。
roomからユーザー名を表示するのにも苦労が多く、このテーブル設計には嫌な思い出しかないです。ただ、利用するメリットがある場合もあると思ってます。例えば、管理者を複数人選択できるようにする場合などです。roomテーブル内のみで、オーナーを管理する場合、ユーザーがオーナーの人数を自由に設定できるようにすると、ルームAは管理者が2人、ルームBは管理者が3人などということが起こり得ます。owner1、owner2、owner3・・・とカラムを用意すると、どこかのセルにはnullが入ってしまうことがあるかもしれません。nullが発生してしまうと、そもそも、nullが発生しないために、RDBというDB設計を使っているのに、その意義が薄まってしまうことになります。話がそれましたが、管理者を自由に複数人選択することができる機能を実装する場合、ownerテーブルを作成する必要が出てくるかもしれません。
参考サイト