【问题标题】:How to prevent a self-referencing table from becoming circular如何防止自引用表变成圆形
【发布时间】:2014-04-11 17:52:05
【问题描述】:

这是一个很常见的问题,但我还没有找到我正在寻找的确切问题和答案。

我有一个表,它的 FK 指向它自己的 PK,以启用任意深度的层次结构,例如经典的 tblEmployee,它有一个列 Manager,它是一个具有 PK tblEmployee.EmployeeID 的 FK。

假设在我的应用中,用户

  1. 创建没有经理的新员工 Alice 和 Dave,因为他们是 CEO 和总裁。所以 tblEmployee.Manager 对于这两条记录来说是 NULL。
  2. 创建新员工 Bob,Alice 作为经理。然后创建以 Bob 为经理的 Charles。他们的 Manager 字段包含 tblEmployee 中另一条记录的主键值。
  3. 编辑 Alice 的员工记录,这意味着为 Dave 分配了她的经理(这很好),但意外地将 Alice 的经理设置为 Charles,他在树中 Alice 的下方两层。

现在该表位于循环引用而不是正确的树中。

在应用程序中确保步骤 3无法完成最佳方法是什么?我只需要确保它会拒绝执行最后一次 SQL 更新,而是显示一些错误消息。

我并不挑剔它是 SQL Server 中的数据库约束(必须在 2008 年或 2012 年工作)还是我的 C# 应用程序的业务逻辑层中的某种验证例程。

【问题讨论】:

  • 业务逻辑可能非常简单:一直寻找员工的经理,直到它为空,或者它是您已经在循环中遇到的员工。
  • 您是否考虑过使用hierarchyid 来建模层次结构而不是指针模型?由于其对层次结构建模的不同方式的本质,您无法构建循环。

标签: c# sql sql-server circular-reference


【解决方案1】:

您可以使用 CHECK CONSTRAINT 来验证经理 ID 不是循环。您不能在检查约束中进行复杂查询,但如果您先将其包装在一个函数中,您可以:

create function CheckManagerCycle( @managerID int )
returns int
as
begin

    declare @cycleExists bit
    set @cycleExists = 0

    ;with cte as (
        select E.* from tblEmployee E where ID = @managerID
        union all
        select E.* from tblEmployee E join cte on cte.ManagerID = E.ID and E.ID <> @managerID
    )
    select @cycleExists = count(*) from cte E where E.ManagerID = @managerID

    return @cycleExists;

end

然后你可以使用这样的约束:

alter table tblEmployee
ADD CONSTRAINT chkManagerRecursive CHECK ( dbo.CheckManagerCycle(ManagerID) = 0 )

这将防止添加或更新记录以从任何来源创建循环。


编辑:重要提示:检查约束在它们引用的列上进行验证。我最初编写此代码是为了检查员工 ID 而不是经理 ID 上的周期。但是,这不起作用,因为它仅在更改 ID 列时触发。此版本确实有效,因为它会在 ManagerID 更改的任何时候触发。

【讨论】:

  • cte 的递归定义让我头晕目眩。非常感谢你给我看这个;我没有足够的 SQL 知道可以通过这种方式完成(我是一名 C# 开发人员,只是因为我们没有足够大的员工来拥有一个常驻 DB 大师)
  • “这将阻止添加或更新记录以从任何来源创建循环。” 您确定吗?我测试了它(postgresql),但chkManagerRecursive 评估为 1,循环引用必须在表中(即约束只捕获更新,但不捕获插入)。无论如何,我调整了您的函数以在 postgresql 中的插入后或更新行级触发器中运行并且运行良好。谢了。
  • 在 c# 中这个逻辑等价物是什么?
【解决方案2】:

您可以添加“级别”整数列。

Alice 和 Dave 的等级 == 0 如果您为员工设置经理,他的(员工)级别将是其经理的级别+1。

更新过程中你应该检查经理级别是否小于员工级别...

这将比使用过程更快...

【讨论】:

    【解决方案3】:

    您可以在UPDATE 声明中包含支票:

    DECLARE @Employee INT = 2
           ,@NewManager INT = 4
    ;WITH cte AS (SELECT *
                  FROM tblEmployee
                  WHERE Manager = @Employee
                  UNION  ALL
                  SELECT a.*
                  FROM tblEmployee a
                  JOIN cte b
                    ON a.manager = b.EmployeeID)
    UPDATE a
    SET a.Manager = @NewManager
    FROM tblEmployee a
    WHERE EmployeeID = @Employee
        AND NOT EXISTS (SELECT *
                        FROM cte b
                        WHERE a.EmployeeID = b.Manager)
    

    演示:SQL Fiddle

    【讨论】:

    • Woot 我不知道你可以用 CTE 做一些递归的事情!谢谢。
    【解决方案4】:

    我认为最好的方法是:

    1. 在 t-sql 中创建 2 个递归函数(性能优于脏循环),这将完成返回“给定员工的 N+x 个经理”和“给定经理的 Nx 员工
    2. 阻止第 3 步,使用 GET_MANAGERS_OFGET_EMPLOYEES_OF 函数将在两者中使用:

      • 签入您的 C# 应用程序
      • 签入员工表TRIGGER (最好的安全原因是您不知道是否每个开发人员都会在更新员工之前检查以及是否有人直接进行sql更新)

    如果您分配给员工 Y经理 X 不是 Y 的员工 N-x

    无论如何,这些递归函数将在您的 SQL 查询和 C# 应用程序中很有用

    仅供参考,有一种方法可以在 C# 应用程序中处理 SQL ERROR TRANSACTION(“您可以这样做,因为...”)。

    【讨论】:

      猜你喜欢
      • 2014-04-22
      • 1970-01-01
      • 2018-09-02
      • 2012-02-15
      • 2014-05-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多