外键约束可能导致循环或多个级联路径?

时间:2009-05-12 07:47:59

标签: sql sql-server constraints

当我尝试向表格添加约束时遇到问题。我收到错误:

  

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

我的约束是在Code表和employee表之间。 Code表包含IdNameFriendlyNameTypeValueemployee有许多引用代码的字段,因此可以为每种类型的代码提供引用。

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

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

8 个答案:

答案 0 :(得分:166)

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

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

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

答案 1 :(得分:84)

具有多个级联路径的典型情况是: 一个包含两个细节的主表,让我们说“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记录永远不属于两个细节,现在也不可能为这两个细节制作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记录(实际上是先创建它以获得正确且唯一的id)。

答案 2 :(得分:12)

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

由于RDB中的大多数明显周期都发生在层次结构(组织结构图,部分,子部分等)中,因此不幸的是SQL Server假设最糟糕;即,模式周期==数据周期。实际上,如果您使用RI约束,则无法在数据中实际构建循环!

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

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

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

Celko提出了一种“更好”的方式来表示不引入周期的层次结构,但存在权衡。

答案 3 :(得分:5)

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

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

答案 4 :(得分:3)

通过它的声音,您在其中一个现有的外键上有一个OnDelete / OnUpdate操作,它将修改您的代码表。

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

E.g。更新员工,导致代码由On Update Action更改,导致Employees被On Update Action更改......等等......

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

答案 5 :(得分:1)

触发器是解决此问题的方法:

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

答案 6 :(得分:0)

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

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

或完全关闭此功能:

modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();

答案 7 :(得分:-2)

使用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

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

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

    祝你好运!