before_destroy 回调可防止在 Ruby on Rails 中删除子记录

ahs*_*ele 1 validation ruby-on-rails

因为 RoR 不提供validate_on_destroy,所以我本质上是通过使用before_destroy回调来实现的。

使用before_destory可以防止已删除的项目。effort_logged?下面的实现不起作用,因为当没有记录时,我想删除该项目及其所有依赖项。只要before_destroy按照下面的方式实施,我就无法这样做。

如果我了解在调用父级方法之前如何删除:dependent => :destroy与依赖子级相关的工作。如果我的假设是正确的,那么访问方法中的子项是否会导致它们不被删除?有没有更好的方法来检查是否可以根据其子级删除父级?before_destroybefore_destroyeffort_logged?

除了对 RoR 如何工作的好奇之外,我的目标是通过以下两项测试:

  • 当没有努力记录项目删除删除依赖项时(此测试失败
  • 无法删除已记录工作量的项目(此测试通过

鉴于下面列出的所有内容,我希望这两项测试都能通过。

项目模型

class Project < ActiveRecord::Base
  has_many :project_phases, :dependent => :destroy

  def before_destroy
     if effort_logged?
        errors.add_to_base("A project with effort logged cannot be deleted")
        false
     else
        true
     end
  end

  def effort_logged?
     project_phases.each do |project_phase|
        project_phase.deliverables.each do |deliverable|
           if (deliverable.effort_logged?)
              return true
           end
        end
     end
  end
end
Run Code Online (Sandbox Code Playgroud)

项目阶段模型

class ProjectPhase < ActiveRecord::Base
  belongs_to :project
  has_many :deliverables, :dependent => :destroy
end
Run Code Online (Sandbox Code Playgroud)

可交付模型

class Deliverable < ActiveRecord::Base
  has_many :effort_logs, :dependent => :destroy

  def effort_logged?
    total_effort_logged != 0
  end

  def total_effort_logged
    effort_logs.to_a.sum {|log| log.duration}
  end
end
Run Code Online (Sandbox Code Playgroud)

努力日志模型

class EffortLog < ActiveRecord::Base
  belongs_to :deliverable
end
Run Code Online (Sandbox Code Playgroud)

测试无法删除已记录工作量的项目

test "cannot delete project with effort logged" do
   project = projects(:ProjectOne)

   assert !project.destroy, "#{project.errors.full_messages.to_sentence}"
end
Run Code Online (Sandbox Code Playgroud)

测试何时不进行任何记录的项目删除会删除依赖项

test "when no effort logged project deletion deletes dependents" do
   project = projects(:ProjectNoEffort)

   # all phases of the project
   project_phases = project.project_phases

   # all deliverables of all phases of the project
   project_phases_deliverables = {}

   # all effort logs of all deliverables of the project
   deliverables_effort_logs = {}

   project_phases.each do |project_phase|
      project_phases_deliverables[project_phase.name + "-" + project_phase.id.to_s] =
         project_phase.deliverables
   end

   project_phases_deliverables.each { |project_phase, project_phase_deliverables|
      project_phase_deliverables.each do |deliverable|
         deliverables_effort_logs[deliverable.name + "-" + deliverable.id.to_s] =
            deliverable.effort_logs
      end
   }

   project.destroy

   assert_equal(0, project_phases.count,
                "Project phases still exist for the deleted project")

   project_phases_deliverables.each { |project_phase, project_phases_deliverables|
      assert_equal(0, project_phases_deliverables.count,
      "Deliverables still exist for the project phase \"" + project_phase + "\"")
   }

   deliverables_effort_logs.each { |deliverable, deliverables_effort_logs|
      assert_equal(0, deliverables_effort_logs.count,
      "Effort logs still exist for the deliverable \"" + deliverable + "\"")
   }
end
Run Code Online (Sandbox Code Playgroud)

Mat*_*ins 5

我发现这个问题是因为我遇到了同样的问题。事实证明,回调的顺序很重要。当您在 Rails 中定义关系时,该:dependent选项实际上会在幕后创建回调。如果您在关系之后before_destroy定义回调,那么直到关系被销毁之后才会调用您的回调。

解决方案是更改回调的顺序,以便您首先定义依赖于仍然存在的关系的回调。关系定义应在之后进行。

您的代码应该看起来更像这样:

class Project < ActiveRecord::Base
  # this must come BEFORE the call to has_many
  before_destroy :ensure_no_effort_logged

  # this must come AFTER the call to before_destroy
  has_many :project_phases, :dependent => :destroy

  # this can be placed anywhere in the class
  def ensure_no_effort_logged
    if effort_logged?
      errors.add_to_base("A project with effort logged cannot be deleted")
      false
    else
      true
    end
  end
end
Run Code Online (Sandbox Code Playgroud)