✨
[Rails]関連先のレコードをdelete_allしたらMysql2::Error: Column cannot be null
foo.hoges.delete_all
という風にhas_manyで関連付けされてるレコードをdelete_all
しようとしたときにエラーが発生したのでその対処法です。
以下のような1対多のテーブル、Modelがあるとします。
class DeleteAllParent < ApplicationRecord
has_many :delete_all_children
end
class DeleteAllChild < ApplicationRecord
belongs_to :delete_all_parent
end
CREATE TABLE `delete_all_parents` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;
CREATE TABLE `delete_all_children` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`delete_all_parent_id` bigint(20) NOT NULL,
PRIMARY KEY (`id`),
KEY `index_delete_all_children_on_delete_all_parent_id` (`delete_all_parent_id`),
CONSTRAINT `fk_rails_ae902ddcaf` FOREIGN KEY (`delete_all_parent_id`) REFERENCES `delete_all_parents` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;
子のレコードを物理削除しようと思い、
DeleteAllParent.first.delete_all_children.delete_all
とすると、
ActiveRecord::NotNullViolation: Mysql2::Error: Column 'delete_all_parent_id' cannot be null
とエラーが発生し、削除が失敗します。
ログを見てみると
[3] pry(main)> DeleteAllParent.first.delete_all_children.delete_all
DeleteAllParent Load (0.7ms) SELECT `delete_all_parents`.* FROM `delete_all_parents` ORDER BY `delete_all_parents`.`id` ASC LIMIT 1
DeleteAllChild Update All (5.8ms) UPDATE `delete_all_children` SET `delete_all_children`.`delete_all_parent_id` = NULL WHERE `delete_all_children`.`delete_all_parent_id` = 1
delete_all_childrenに対してdeleteではなくupdate処理が走っているのがわかります。
delete_all_children.delete_all_parent_id
カラムはNOT NULL
としてるのでエラーが起きているようです。
deleteの処理が走るのかなと思ったらupdate処理だったのでなぜ?と思ってコードを見てみます。
ActiveRecordのコードを追ってみる
[8] pry(main)> DeleteAllParent.first.delete_all_children.class
DeleteAllParent Load (2.8ms) SELECT `delete_all_parents`.* FROM `delete_all_parents` ORDER BY `delete_all_parents`.`id` ASC LIMIT 1
=> DeleteAllChild::ActiveRecord_Associations_CollectionProxy
ActiveRecord_Associations_CollectionProxy
クラスを見に行きます。
delete_all
生えてました。これですね。
def delete_all(dependent = nil)
if dependent && ![:nullify, :delete_all].include?(dependent)
raise ArgumentError, "Valid values are :nullify or :delete_all"
end
dependent = if dependent
dependent
elsif options[:dependent] == :destroy
:delete_all
else
options[:dependent]
end
delete_or_nullify_all_records(dependent).tap do
reset
loaded!
end
end
コードを追っていくと
def delete_or_nullify_all_records(method)
count = delete_count(method, scope)
update_counter(-count)
count
end
ここでmethod
によってdeleteかupdateか分けていました。
def delete_count(method, scope)
if method == :delete_all
scope.delete_all
else
scope.update_all(nullified_owner_attributes)
end
end
ここのmethod
はdelete_all
メソッドのdependent
にセットされているものです。
今回の場合だとdependent
はnil
なので、update_allになっていたいうことがわかりました。
どうすればdeleteされるか
どういうときにdelete_allになるかというと、
delete_all
に引数として:delete_all
を渡す。
または、
options[:dependent]に:delete_all
or:destroy
がセットされた状態にする。つまり、
class DeleteAllParent < ApplicationRecord
has_many :delete_all_children, dependent: :delete_all
# or
has_many :delete_all_children, dependent: :destroy
end
dependentを設定しておく。
これで無事関連先のレコードを物理削除することができるようになりました。
Discussion