MySQLのエラーでちょっとハマったのでメモ。
models/user.rb
class User < ApplicationRecord has_many :articles, dependent: :destroy end
models/article.rb
class Article < ApplicationRecord belongs_to :user end
migration(article)
create_table "articles" do |t| t.references :user, foeign_key: true t.string :title, null: false t.text :text, null: false t.datetime :created_at end
親:ユーザー
子:記事(article)
articleテーブルのuser_id
には外部キー制約を設けてます。
発生したエラー
この状態で親モデルであるユーザーを削除しようとした時にエラーがでた。
users_controller.rb
def destroy @user.delete end
Mysql2::Error: Cannot delete or update a parent row: a foreign key constraint fails....
何やら外部キー周りがおかしい様子。
外部キー制約とは
そもそも外部キー制約とは、親モデルに何らかの処理(updateやdeleteなど)が走った時に関連モデルに影響が出ないよう、勝手に親だけどうにかなってしまうのを制御するために使用するオプション。
今回の場合、articleモデルのuser_id
カラムが空の状態の子モデルを発生させないように制約する。
マイグレーションファイルでt.references :モデル
とすることで、モデル_id
カラムを生成してくれる。
(ちなみにreferenceの貼り方は色々あるけど、この書き方をするとインデックスも自動で貼ってくれる。)
単純にリレーションをもたせたい場合はこれで良し。さらに外部キー制約をかけたい場合はfoeign_key: true
オプションをつける。
reference イコール 外部キー とちょっと勘違いしていた部分があった。
dependent: :destroy が効いてない?
あれ、でもdependent: :destroy
をちゃんと書いてるので、親と一緒に子が消えてくれると思ったけど。。
子モデルも一緒に消えてくれたら、そもそも今回のようなエラーは出ないんじゃないか??と思いもう一度コントローラーを見てみると。
users_controller.rb
def destroy @user.delete end
deleteになってた。
deleteメソッドは直接SQLが走るので、ActiveRecordを介さない。ということは、ちゃんと書いてるはずのdependent: :destroy
が効いていなかった。
結果、親モデルと一緒に削除されるはずの子モデルだけレコードが残り、articleレコードのuser_id
カラムが空になってしまい、「外部キー制約ついてるから親(ユーザー)だけ消せないよ!!」というエラーが出ていた。
解決策:destroyにする
users_controller.rb
def destroy @user.destroy end
これでOK。
deleteとdestroyって間違ってても気づきづらい...見つけるのに時間かかってしまった。
まとめ
- アソシエーションのために外部キーを貼ることと外部キー制約をつけることは別物。
- 外部キーの貼り方には複数ある。マイグレーションの書き方によりインデックスがついたりつかなかったり、外部キー制約をつけられたりつけられなかったりする(ややこしいな)
- deleteとdestroyの違いに注意
deleteとdestroyに関しては、dependentオプションに関しても同じことが言えるなと思った。
SQL文のDELETEを思い浮かべると分かるように、「delete」と言われたらRailsのActiveRecordではなくSQLが直接走るのだ!!ということを肝に命じなければ、、。
参考
Ruby on Rails - 投稿が外部制約によって削除ができない|teratail
Active Record の関連付け - Railsガイド
マイグレーションファイルの詳しい書き方は以下