使用自引用记录防止无限循环

Prevent infinite loop with self-referencial records

我有一个模型,它通过连接 table 进行自引用连接,定义如下:

class Task < ActiveRecord::Base
    has_many :dependency_dependents, foreign_key: :dependency_id, class_name: 'TaskDependency', dependent: :destroy
    has_many :dependency_dependencies, foreign_key: :task_id, class_name: 'TaskDependency', dependent: :destroy, autosave: true

    has_many :dependencies, through: :dependency_dependencies
    has_many :dependents, through: :dependency_dependents, source: :task
end

class TaskDependency < ActiveRecord::Base
    belongs_to :task
    belongs_to :dependency, class_name: 'Task'
end

连接的一切都很好。保存记录时,它会根据它所依赖的记录执行一系列计算,然后继续更新依赖记录,这些记录又会对这些记录执行相同的计算,依此类推。

问题是,如果在依赖任务链中的某个位置,存在对树上其他位置的任务的依赖,首先计算会失败,但最重要的是,它会导致计算的无限循环和更新。

在保存记录之前(最好是在创建依赖关系之前),有没有一种好的方法可以检查这个无限循环。

我很乐意使用纯 SQL 或 ruby 来做到这一点,只是想知道是否有人对此有一个干净的解决方案。

好的,我想我已经弄明白了。当记录验证时,它会调用一个新的验证方法,该方法运行一个递归查询,该方法会提取其下方层次结构中的所有 ID,如果您尝试建立依赖关系的任务的 ID 在依赖列表中找到, 然后它验证失败。对此的查询如下:

WITH RECURSIVE dependencies(task_id, path) AS (
  SELECT task_id, ARRAY[task_id]
  FROM task_dependencies
  WHERE dependency_id = #{self.id}

  UNION ALL

  SELECT task_dependencies.task_id, path || task_dependencies.task_id
  FROM dependencies
  JOIN task_dependencies ON dependencies.task_id = task_dependencies.dependency_id
  WHERE NOT task_dependencies.task_id = ANY(path)

)

SELECT task_id id FROM dependencies ORDER BY path

永远不知道,也许这会对一路走来的人有所帮助。 正如 @craig-ringer 所提到的,仍然有可能发生两个并发插入导致竞争条件,但是这个查询也可以反过来锁定 table 以防止在必要时发生这种情况。