
[Rails] 外部キー制約がついているデータを削除するにはどうすればいいの? @TECH CAMP#14
どうもお久しぶりです、個人アプリの開発が忙しいとだです。全然思い通りにならない。助けて欲しいくらいです、ダメだけど。
何かアウトプットしなきゃと、今日は小ネタを持ってきました。
タイトルにもある通り、Railsでの開発で、外部キー制約がついているデータを削除したい、destroyアクションで削除機能を実装したい時にやらなければいけない1ステップについて紹介します。やるべきことというのは、決まり文句をちょっと記述するだけなのですが、知らないと私のようにエラー画面とずっとにらめっこしてしまうことになるのでシェアしたいと思います。
チャットグループを削除したい
RailsでLINEのようなグループチャット機能を持つアプリを作っているとします。モデルにはUserモデル(deviseというgemを使っています)とGroupモデルがあるとします。アソシエーションは以下の通りです。(中間テーブルが設定されていますが、これについてはまた今度記事にします。)
class Group < ApplicationRecord
has_many :group_users
has_many :users, through: :group_users
end
class User < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
has_many :group_users
has_many :groups, through: :group_users
end
また、データベースのGroupsテーブルや中間テーブルのgroups_usersテーブルには以下のような外部キー制約がついています。
## groupsテーブル
|Column|Type|Options|
|------|----|-------|
|groupname|string|null: false|
|user_id|integer|null: false, foreign_key: true|
## groups_usersテーブル
|Column|Type|Options|
|------|----|-------|
|user_id|integer|null: false, foreign_key: true|
|group_id|integer|null: false, foreign_key: true|
そこで、チャットをする部屋のようなグループを削除できるような機能を実装したいと思った時、単にdestroyアクションで
def destroy
group = Group.find(params[:id])
group.destroy
redirect_to root_path, notice: 'グループを削除しました'
end
のような記述をしても怒られます。以下のようなエラーが出ます。
ActiveRecord::StatementInvalid: Mysql2::Error: Cannot delete or update a parent row: a foreign key constraint fails
原因
なんで!、と暫く一人でプンプン怒っていましたが原因を調べると以下のことがわかりました。
原因:外部キーの参照整合性が原因
そもそも外部キーの目的は、主キーを持つテーブルと、外部キーのテーブルをリンクを作ることで、主キー側のテーブルのデータに対する変更へ制限を加えることです。今回のように、Eventが削除された際に、Eventの主キーを参照するParticipationテーブルのデータは参照先を持たないデータとなってしまう恐れがあります。外部キーの設定をすることで、この不具合を未然に防いでいます。
つまり自分の作っているアプリに言い換えると、Groupが削除された時にUserが参照先を持たないデータを持つ恐れがあるのを未然に防いでいる、もっと言うと、あるユーザーが存在しないGroupに所属していることになっている事態を防いでくれているんですね。
どうすれば削除できるようになるのか
じゃあどうすればGroupを削除できるの?と言うことなんですが、なんと簡単、以下のようにdestoryで外部キーとして設定しているレコード削除させるように、dependent: :destroyを設定してあげればよいです。
class Group < ApplicationRecord
has_many :group_users, dependent: :destroy
has_many :users, through: :group_users, dependent: :destroy
end
class User < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
has_many :group_users, dependent: :destroy
has_many :groups, through: :group_users, dependent: :destroy
end
こうすれば、先ほどのようなdestroyアクション
def destroy
group = Group.find(params[:id])
group.destroy
redirect_to root_path, notice: 'グループを削除しました'
end
を記述した時、グループを削除する機能が実装できます。
終わりに
個人アプリを開発している時に出会った新しい知識は、なるべく早くシェアできるようにしたいですね…。何より間違いがあれば指摘して欲しいですし、それが早いタイミングであれば早く修正できますから。
記事の途中でも引用しましたが、今回の実装について大変お世話になった記事があるのでもう一度いかにリンクを貼っておきますので、興味のある方は参考になさってください。
個人アプリの開発、頑張ります!