【问题标题】:Foreign key constraint may cause cycles or multiple cascade paths?外键约束可能导致循环或多个级联路径?
【发布时间】:2010-10-25 11:54:55
【问题描述】:

我在尝试向表中添加约束时遇到问题。我得到了错误:

在表“Employee”上引入 FOREIGN KEY 约束“FK74988DB24B3C886”可能会导致循环或多个级联路径。指定 ON DELETE NO ACTION 或 ON UPDATE NO ACTION,或修改其他 FOREIGN KEY 约束。

我的约束介于 Code 表和 employee 表之间。 Code 表包含 IdNameFriendlyNameTypeValueemployee 有许多引用代码的字段,因此可以对每种类型的代码进行引用。

如果引用的代码被删除,我需要将字段设置为空。

有什么想法可以做到这一点吗?

【问题讨论】:

  • 其中一个解决方案是here

标签: sql sql-server constraints


【解决方案1】:

听上去,您对现有外键之一执行了 OnDelete/OnUpdate 操作,这将修改您的代码表。

因此,通过创建此外键,您将创建一个循环问题,

例如更新员工,导致代码被更新操作更改,导致员工被更新操作更改......等等......

如果您发布两个表的表定义和外键/约束定义,我们应该能够告诉您问题出在哪里...

【讨论】:

  • 它们相当长,所以我不认为我可以在这里发布它们,但我非常感谢您的帮助 - 不知道是否有什么方法可以将它们发送给您?我会尝试描述它:唯一存在的约束来自 3 个表,这些表都具有通过简单的 INT Id 键引用代码的字段。问题似乎是 Employee 有几个引用代码表的字段,我希望它们都级联到 SET NULL。我需要的是,当代码被删除时,对它们的引用应该在任何地方设置为 null。
  • 无论如何都要发布它们...我认为这里没有人会介意,代码窗口将在滚动块中正确格式化它们:)
【解决方案2】:

SQL Server 对级联路径进行简单计数,而不是尝试确定是否存在任何循环,而是假设最坏的情况并拒绝创建引用操作 (CASCADE):您可以而且应该仍然创建没有参考动作。如果你不能改变你的设计(或者这样做会影响事情),那么你应该考虑使用触发器作为最后的手段。

FWIW 解析级联路径是一个复杂的问题。其他 SQL 产品将简单地忽略该问题并允许您创建循环,在这种情况下,将竞相看哪个会最后覆盖该值,这可能是设计者的无知(例如 ACE/Jet 就是这样做的)。我了解一些 SQL 产品会尝试解决简单的情况。事实仍然存在,SQL Server 甚至没有尝试,通过禁止多个路径来保证它的安全性,至少它告诉你是这样的。

微软自己advises使用触发器而不是FK约束。

【讨论】:

  • 我仍然无法理解的一件事是,如果这个“问题”可以通过使用触发器来解决,那么触发器为什么不会“导致循环或多个级联路径......” ?
  • @armen:因为您的触发器将显式提供系统无法自行隐式计算的逻辑,例如,如果删除引用操作有多个路径,那么您的触发器代码将定义哪些表被删除的顺序。
  • 并且触发器在第一个操作完成后执行,因此没有竞争。
  • @dumbledad:我的意思是,只有在约束(可能是组合)无法完成工作时才使用触发器。约束是声明性的,它们的实现是系统的责任。触发器是过程代码,您必须对实现进行编码(和调试)并忍受它们的缺点(更差的性能等)。
  • 这个问题是触发器只在你删除外键约束时才起作用,这意味着你没有对数据库插入的引用完整性检查,所以你需要更多的触发器来处理它.触发器解决方案是一个导致退化数据库设计的兔子洞。
【解决方案3】:

我会指出(功能上)SCHEMA 和 DATA 中的周期和/或多条路径之间存在很大差异。虽然 DATA 中的循环和多路径肯定会使处理复杂化并导致性能问题(“正确”处理的成本),但模式中这些特征的成本应该接近于零。

由于 RDB 中最明显的循环出现在层次结构(组织结构图、部分、子部分等)中。不幸的是,SQL Server 假设了最坏的情况;即,模式周期 == 数据周期。事实上,如果您使用 RI 约束,您实际上无法在数据中构建循环!

我怀疑多路径问题类似;即,模式中的多条路径不一定意味着数据中的多条路径,但我对多路径问题的经验较少。

当然,如果 SQL Server 确实 允许循环,它仍然会受到 32 的深度的影响,但这对于大多数情况来说可能已经足够了。 (可惜这不是数据库设置!)

“代替删除”触发器也不起作用。第二次访问表时,将忽略触发器。因此,如果您真的想模拟级联,则必须在存在循环的情况下使用存储过程。然而,Instead-of-Delete-Trigger 将适用于多路径情况。

Celko 提出了一种“更好”的方式来表示不引入循环的层次结构,但需要权衡取舍。

【讨论】:

  • "如果您使用 RI 约束,您实际上无法在数据中构建循环!" -- 好点子!
  • 当然你可以建立数据循环,但 MSSQL 只能使用 UPDATE。其他 RDBM 支持延迟约束(在提交时确保完整性,而不是在插入/更新/删除时)。
【解决方案4】:

具有多个级联路径的典型情况如下: 一个有两个细节的主表,比如说“Master”、“Detail1”和“Detail2”。两个细节都是级联删除。到目前为止没有问题。但是,如果这两个细节都与某个其他表(例如“SomeOtherTable”)具有一对多的关系怎么办。 SomeOtherTable 有一个 Detail1ID 列和一个 Detail2ID 列。

Master { ID, masterfields }

Detail1 { ID, MasterID, detail1fields }

Detail2 { ID, MasterID, detail2fields }

SomeOtherTable {ID, Detail1ID, Detail2ID, someothertablefields }

换句话说:SomeOtherTable 中的一些记录与 Detail1-records 相关联,SomeOtherTable 中的一些记录与 Detail2 记录相关联。即使保证 SomeOtherTable-records 永远不属于这两个 Details,现在也不可能对两个 Details 进行 SomeOhterTable 的记录级联删除,因为从 Master 到 SomeOtherTable 有多个级联路径(一个通过 Detail1,一个通过 Detail2)。 现在你可能已经明白了。这是一个可能的解决方案:

Master { ID, masterfields }

DetailMain { ID, MasterID }

Detail1 { DetailMainID, detail1fields }

Detail2 { DetailMainID, detail2fields }

SomeOtherTable {ID, DetailMainID, someothertablefields }

所有 ID 字段都是关键字段和自动增量。关键在于 Detail 表的 DetailMainId 字段。这些字段既是关键又是参考约束。现在可以通过仅删除主记录来级联删除所有内容。不利的一面是,对于每个 detail1-record 和每个 detail2 记录,还必须有一个 DetailMain-record(实际上是首先创建它以获得正确且唯一的 id)。

【讨论】:

  • 您的评论帮助我理解了我所面临的问题。谢谢!我宁愿关闭其中一个路径的级联删除,然后以其他方式处理其他记录的删除(存储过程;触发器;通过代码等)。但我会牢记您的解决方案(在一条路径中分组),以应对同一问题的可能不同应用......
  • 一个用于症结一词的使用(也用于解释)
  • 这比写触发器好吗?只是为了让级联工作而添加一个额外的表似乎很奇怪。
  • 任何事情都比编写触发器好。它们的逻辑是不透明的,与其他任何东西相比它们效率低下。将大表分解成更小的表以进行更精细的控制只是更好的规范化数据库的自然结果,而这本身并不值得关注。
【解决方案5】:

这是因为 Emplyee 可能有其他实体的集合,例如 Qualifications 和 Qualification 可能有一些其他集合 Universities 例如

public class Employee{
public virtual ICollection<Qualification> Qualifications {get;set;}

}

public class Qualification{

public Employee Employee {get;set;}

public virtual ICollection<University> Universities {get;set;}

}

public class University{

public Qualification Qualification {get;set;}

}

在 DataContext 上可能如下所示

protected override void OnModelCreating(DbModelBuilder modelBuilder){

modelBuilder.Entity<Qualification>().HasRequired(x=> x.Employee).WithMany(e => e.Qualifications);
modelBuilder.Entity<University>.HasRequired(x => x.Qualification).WithMany(e => e.Universities);

}

在这种情况下,存在从员工到资格以及从资格到大学的链。所以它向我抛出了同样的异常。

当我改变时它对我有用

    modelBuilder.Entity<Qualification>().**HasRequired**(x=> x.Employee).WithMany(e => e.Qualifications); 

    modelBuilder.Entity<Qualification>().**HasOptional**(x=> x.Employee).WithMany(e => e.Qualifications);

【讨论】:

    【解决方案6】:

    这是数据库触发策略类型的错误。 触发器是代码,可以为级联关系添加一些智能或条件,例如级联删除。您可能需要专门针对此相关的表选项,例如关闭 CascadeOnDelete

    protected override void OnModelCreating( DbModelBuilder modelBuilder )
    {
        modelBuilder.Entity<TableName>().HasMany(i => i.Member).WithRequired().WillCascadeOnDelete(false);
    }
    

    或完全关闭此功能:

    modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();
    

    【讨论】:

      【解决方案7】:

      有一篇文章解释了如何使用触发器执行多个删除路径。也许这对于复杂的场景很有用。

      http://www.mssqltips.com/sqlservertip/2733/solving-the-sql-server-multiple-cascade-path-issue-with-a-trigger/

      【讨论】:

        【解决方案8】:

        触发器是这个问题的解决方案:

        IF OBJECT_ID('dbo.fktest2', 'U') IS NOT NULL
            drop table fktest2
        IF OBJECT_ID('dbo.fktest1', 'U') IS NOT NULL
            drop table fktest1
        IF EXISTS (SELECT name FROM sysobjects WHERE name = 'fkTest1Trigger' AND type = 'TR')
            DROP TRIGGER dbo.fkTest1Trigger
        go
        create table fktest1 (id int primary key, anQId int identity)
        go  
            create table fktest2 (id1 int, id2 int, anQId int identity,
                FOREIGN KEY (id1) REFERENCES fktest1 (id)
                    ON DELETE CASCADE
                    ON UPDATE CASCADE/*,    
                FOREIGN KEY (id2) REFERENCES fktest1 (id) this causes compile error so we have to use triggers
                    ON DELETE CASCADE
                    ON UPDATE CASCADE*/ 
                    )
        go
        
        CREATE TRIGGER fkTest1Trigger
        ON fkTest1
        AFTER INSERT, UPDATE, DELETE
        AS
            if @@ROWCOUNT = 0
                return
            set nocount on
        
            -- This code is replacement for foreign key cascade (auto update of field in destination table when its referenced primary key in source table changes.
            -- Compiler complains only when you use multiple cascased. It throws this compile error:
            -- Rrigger Introducing FOREIGN KEY constraint on table may cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, 
            -- or modify other FOREIGN KEY constraints.
            IF ((UPDATE (id) and exists(select 1 from fktest1 A join deleted B on B.anqid = A.anqid where B.id <> A.id)))
            begin       
                update fktest2 set id2 = i.id
                    from deleted d
                    join fktest2 on d.id = fktest2.id2
                    join inserted i on i.anqid = d.anqid        
            end         
            if exists (select 1 from deleted)       
                DELETE one FROM fktest2 one LEFT JOIN fktest1 two ON two.id = one.id2 where two.id is null -- drop all from dest table which are not in source table
        GO
        
        insert into fktest1 (id) values (1)
        insert into fktest1 (id) values (2)
        insert into fktest1 (id) values (3)
        
        insert into fktest2 (id1, id2) values (1,1)
        insert into fktest2 (id1, id2) values (2,2)
        insert into fktest2 (id1, id2) values (1,3)
        
        select * from fktest1
        select * from fktest2
        
        update fktest1 set id=11 where id=1
        update fktest1 set id=22 where id=2
        update fktest1 set id=33 where id=3
        delete from fktest1 where id > 22
        
        select * from fktest1
        select * from fktest2
        

        【讨论】:

          【解决方案9】:

          我使用 ASP.NET Core 2.0 和 EF Core 2.0 遇到的这个问题的解决方案是按顺序执行以下操作:

          1. 在包管理控制台 (PMC) 中运行 update-database 命令以创建数据库(这会导致“引入 FOREIGN KEY 约束...可能导致循环或多个级联路径。”错误)

          2. 在 PMC 中运行 script-migration -Idempotent 命令以创建一个可以在现有表/约束条件下运行的脚本

          3. 获取生成的脚本并找到ON DELETE CASCADE并替换为ON DELETE NO ACTION

          4. 对数据库执行修改后的 SQL

          现在,您的迁移应该是最新的,并且不应发生级联删除。

          太糟糕了,我在 Entity Framework Core 2.0 中找不到任何方法。

          祝你好运!

          【讨论】:

          • 您可以更改您的迁移文件以执行此操作(无需更改 sql 脚本),即在您的迁移文件中,您可以将 onDelete 操作设置为 Restrict from Cascade
          • 最好使用流利的注释来指定它,这样在您最终删除并重新创建迁移文件夹时就不必记住这样做了。
          • 根据我的经验,可以使用并且应该使用流畅的注释(我使用它们),但它们通常有很多错误。简单地在代码中指定它们并不总是能产生预期的结果。
          【解决方案10】:

          一些数据库,尤其是 SQL Server,对形成循环的级联行为有限制。 有两种方法可以处理这种情况: 1.将一个或多个关系更改为不级联删除。 2. 将数据库配置为没有这些级联删除中的一项或多项,然后确保加载所有依赖实体,以便 EF Core 可以执行级联行为。 请参考此链接:
          Database cascade limitations

          【讨论】:

            【解决方案11】:

            大量数据库更新以抵消 PK:改为制作数据库副本。

            特殊用例:A 公司使用与 B 公司具有相同架构的数据库。由于它们已合并,因此他们希望使用单个数据库。因此,B公司数据库中的许多表必须有它们的主键偏移,以避免与A公司的记录发生冲突。

            一种解决方案可能是将外键定义为 ON UPDATE CASCADE,并偏移具有外键的主键。但是如果你这样做会遇到很多障碍(Msg 1785, Msg 8102, ...)。

            所以我想到的一个更好的想法是简单地制作数据库副本,DROP 并重新创建必须具有 PKs|FKs 偏移量的表,然后复制数据(在这样做的同时,偏移主键和外键)。

            避免所有麻烦。

            【讨论】:

              猜你喜欢
              • 2012-09-21
              • 1970-01-01
              • 2018-12-19
              • 2016-03-26
              • 2015-10-26
              相关资源
              最近更新 更多