PG :: ForeignKeyViolation:ERROR:表"xxx"上的更新或删除违反了外键约束

gan*_*elo 4 postgresql ruby-on-rails rails-activerecord ruby-on-rails-5

我有几个表具有与之关联的外键约束,每个表以分层方式引用另一个,如下所述.

当我试图销毁一个至少有1个项目的公司时,至少有1个任务,至少有1个任务时间,如此......

irb(main):014:0> Company.first.destroy
Run Code Online (Sandbox Code Playgroud)

我得到以下输出和错误.我现在的印象是,只是dependent: :delete_all没有处理外键约束,这是真的吗?如果是这样,我该如何处理这种情况?我知道before_destroy回调,在这种情况下我是否必须使用它?如果是这样,我如何暂时禁用外键约束以销毁所有相关的行?是什么让这更令人困惑的是,我有一个旧的rails项目,它具有相同的表/模型设置,只是它model_a has_many model_bs, dependent: delete_all与外键约束的单一关系,我可以ModelB.destroy_all,它的工作原理,所以我不明白.我还阅读了在桌面上设置级联删除的帖子和一些帖子说如果你自己在代码中处理它就不需要这样做; 如果解决方案不是太多毛,我想在我的代码中处理这个问题.

Company Load (0.4ms)  SELECT  "companies".* FROM "companies" ORDER BY 
                              "companies"."id" ASC LIMIT $1 [["LIMIT", 1]]
   (0.2ms)  BEGIN
             SQL (0.9ms)  DELETE FROM "projects" 
                          WHERE "projects"."company_id" = $1 [["company_id", 3]]
   (0.1ms)  ROLLBACK
             Traceback (most recent call last):
   1: from (irb):13
             ActiveRecord::InvalidForeignKey (PG::ForeignKeyViolation: ERROR:  update or delete on table "projects" violates foreign key constraint "fk_rails_02e851e3b7" on table "tasks"
                          DETAIL:  Key (id)=(4) is still referenced from table "tasks".
                        : DELETE FROM "projects" WHERE "projects"."company_id" = $1)
Run Code Online (Sandbox Code Playgroud)

架构

# /db/schema.rb

create_table "companies", force: :cascade do |t|
...
end

create_table "projects", force: :cascade do |t|
...
end

create_table "tasks", force: :cascade do |t|
...
end

create_table "task_times", force: :cascade do |t|
...
end
...

add_foreign_key "projects", "companies"
add_foreign_key "tasks", "projects"
add_foreign_key "task_times", "tasks"
Run Code Online (Sandbox Code Playgroud)

楷模

# /models/company.rb

class Company < ApplicationRecord
  has_many :projects, dependent: :delete_all
...
end

# /models/project.rb

class Project < ApplicationRecord
  has_many :tasks, dependent: :delete_all
...
end

# /models/task.rb

class Task < ApplicationRecord
  has_many :task_times, dependent: :delete_all
...
end

# /models/task_time.rb

class TaskTime < ApplicationRecord
...
end
Run Code Online (Sandbox Code Playgroud)

mu *_*ort 6

精细手册:

has_many(name,scope = nil,options = {},&extension)
[...]

  • :dependent
    控制关联对象在其所有者被销毁时发生的情况.请注意,这些是作为回调实现的,Rails按顺序执行回调.因此,其他类似的回调可能会影响:dependent行为,并且:dependent行为可能会影响其他回调.
    • :destroy 导致所有相关对象也被销毁.
    • :delete_all 导致所有关联的对象直接从数据库中删除(因此不会执行回调).
    • [...]

所以:delete_all要处理外键,但是,由于没有调用回调,它只会深入一级.所以这个Company:

has_many :projects, dependent: :delete_all
Run Code Online (Sandbox Code Playgroud)

意味着呼叫#destroy公司将直接删除projects数据库中的关联.但那不会看到这个:

has_many :tasks, dependent: :delete_all
Run Code Online (Sandbox Code Playgroud)

您有,Project并最终尝试删除仍然引用的项目,tasks如错误消息所示.

您可以切换所有关联dependent: :destroy,这将在销毁之前从数据库中取出所有内容并调用回调(这将从数据库中加载更多东西,只是为了销毁它们,这会从数据库中加载更多东西...... ).最终结果将是大量数据库活动,但将正确遵循所有外键.

或者,您可以通过指定on delete cascade外键约束将逻辑放在数据库中通常所属的位置:

CASCADE指定当删除引用的行时,也应自动删除引用它的行

你的add_foreign_key电话看起来像:

add_foreign_key "projects", "companies", on_delete: :cascade
add_foreign_key "tasks", "projects", on_delete: :cascade
add_foreign_key "task_times", "tasks", on_delete: :cascade
Run Code Online (Sandbox Code Playgroud)

在这种情况下.您可能希望将dependent: :delete_alls 留在模型中以提醒您发生了什么,或者您可以留言.