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

时间:2009-11-11 05:42:04

标签: ruby-on-rails validation

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

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

如果我理解:dependent => :destroybefore_destroy相关的工作方式,则在调用父级before_destroy方法之前删除相关子级。如果我的假设是正确的,那么访问effort_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

项目阶段模型

class ProjectPhase < ActiveRecord::Base
  belongs_to :project
  has_many :deliverables, :dependent => :destroy
end

可交付模式

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

努力日志模型

class EffortLog < ActiveRecord::Base
  belongs_to :deliverable
end

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

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

   assert !project.destroy, "#{project.errors.full_messages.to_sentence}"
end

在没有努力记录项目删除时删除依赖项

进行测试
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

4 个答案:

答案 0 :(得分:4)

我发现了这个问题,因为我遇到了同样的问题。事实证明,回调的顺序很重要。在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

答案 1 :(得分:0)

你是对的,孩子们在你到达之前就已经被消灭了。 它不优雅,但你可以这样做吗? : (顺便说一句,对不起,我没有对此进行测试。这是一个比其他任何事情更多的想法。)

在EffortLog中,有一个before_destroy :ready_to_die?

ready_to_die?会检查它是否具有零努力值。 如果是,请自行销毁。如果否,则引发异常(我的示例是EffortLogError)。 注意:如果你想手动销毁某些东西,你需要先将它归零。

然后在您的Project上有一个具有更具描述性名称的方法:

def carefully_destroy
  Begin
    Project.transaction do
       self.destroy
    end 
  rescue EffortLogError
     self.errors.add_to_base("A Project with effort can't be deleted")
     #do some sort of redirect to the right spot.
  end
end

答案 2 :(得分:0)

尝试在将错误添加到before_destroy过滤器后立即添加引发ActiveRecord :: Rollback。

答案 3 :(得分:0)

在调试测试并密切关注我的方法和变量的值后,我能够确定effort_logged?方法存在问题。当有记录的努力时,它将返回true。但是,当没有记录的努力时,它将返回project_phases的数组。我修改了effort_logged?以使用retval并解决了问题。以下方法可以代表重构。

  def effort_logged?
    retval = false

    project_phases.each do |project_phase|
      project_phase.deliverables.each do |deliverable|
         if (deliverable.effort_logged?)
            retval = true
         end
      end
    end

    return retval
  end