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

Sql 外键约束可能导致循环或多个级联路径?,sql,sql-server,constraints,Sql,Sql Server,Constraints,我在尝试向表添加约束时遇到问题。我得到一个错误: 在表“Employee”上引入外键约束“FK74988DB24B3C886”可能会导致循环或多个级联路径。指定“在删除时不执行操作”或“在更新时不执行操作”,或修改其他外键约束 我的约束位于代码表和员工表之间。代码表包含Id、名称、FriendlyName、类型和值。员工有许多引用代码的字段,因此每种类型的代码都有一个引用 如果引用的代码被删除,我需要将字段设置为null 你知道我该怎么做吗?听上去,你在现有外键上有一个OnDelete/OnUp

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

在表“Employee”上引入外键约束“FK74988DB24B3C886”可能会导致循环或多个级联路径。指定“在删除时不执行操作”或“在更新时不执行操作”,或修改其他外键约束

我的约束位于代码表和员工表之间。代码表包含Id、名称、FriendlyName、类型和值。员工有许多引用代码的字段,因此每种类型的代码都有一个引用

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


你知道我该怎么做吗?

听上去,你在现有外键上有一个OnDelete/OnUpdate操作,它将修改你的代码表

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

例如,更新员工,通过更新操作更改代码,通过更新操作更改员工。。。等等


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

SQL Server只对级联路径进行简单计数,而不是试图确定是否存在任何循环,它假设最坏的情况,并拒绝创建引用操作级联:您可以也应该在没有引用操作的情况下创建约束。如果你不能改变你的设计或者这样做会使事情妥协,那么你应该考虑使用触发器作为最后的手段。 FWIW解析级联路径是一个复杂的问题。其他SQL产品将简单地忽略该问题,并允许您创建周期,在这种情况下,您将竞相查看哪一个将最后覆盖该值,这可能是设计者所不知道的,例如ACE/Jet会这样做。我知道一些SQL产品会试图解决简单的问题。事实仍然是,SQLServer甚至都不尝试,通过禁止多条路径来实现超级安全,至少它告诉您是这样的


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

我要指出的是,模式和数据中的循环和/或多个路径在功能上有很大区别。虽然数据中的周期和可能的多路径肯定会使处理复杂化,并导致性能问题,但正确处理的成本,模式中这些特征的成本应该接近于零


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

我怀疑多路径问题与此类似;i、 例如,模式中的多条路径并不一定意味着数据中有多条路径,但我对多路径问题的经验较少

当然,如果SQLServer允许循环,那么深度仍然是32,但对于大多数情况,这可能已经足够了。可惜这不是数据库设置

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


Celko提出了一种更好的表示不引入循环的层次结构的方法,但存在折衷。

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

Master { ID, masterfields }

Detail1 { ID, MasterID, detail1fields }

Detail2 { ID, MasterID, detail2fields }

SomeOtherTable {ID, Detail1ID, Detail2ID, someothertablefields }
换句话说:SomeOtherTable中的一些记录与Detail1记录链接,SomeOtherTable中的一些记录与Detail2记录链接。即使保证某些OtherTable记录永远不属于这两个详细信息,现在也不可能对这两个详细信息执行SomeOtherTable的记录级联删除,因为从Master到SomeOtherTable有多个级联路径,一个是通过Detail1,另一个是通过Detail2。 现在你可能已经明白了这一点。以下是一个可能的解决方案:

Master { ID, masterfields }

DetailMain { ID, MasterID }

Detail1 { DetailMainID, detail1fields }

Detail2 { DetailMainID, detail2fields }

SomeOtherTable {ID, DetailMainID, someothertablefields }

所有ID字段都是关键字段,并且是自动递增的。关键在于细节表的DetailMainId字段。这些字段既是键又是引用约束。现在可以通过只删除主记录来级联删除所有内容。缺点是,对于每个detail1记录和每个detail2记录,还必须有一个DetailMain记录,该记录实际上是首先创建的,以获得正确且唯一的id。

这是因为Emplyee可能有其他e的集合 ntity表示,学历和资格证书可能会被其他大学收藏 e、 g

}

在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); 


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

或者完全关闭此功能:

modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();

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


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

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

我使用ASP.NET Core 2.0和EF Core 2.0解决此问题的方法是按顺序执行以下操作:

在包管理控制台PMC中运行update database命令以创建数据库这将导致引入外键约束。。。可能导致循环或多个级联路径。错误

在PMC中运行script migration-Idempotent命令以创建一个脚本,该脚本可以在不考虑现有表/约束的情况下运行

获取生成的脚本,在删除级联时查找,并替换为在删除时不执行任何操作

对数据库执行修改后的SQL

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

可惜我在EntityFrameworkCore2.0中找不到任何方法来实现这一点


祝你好运

他们是相当长的,所以我不认为我可以把他们张贴在这里,但我会非常感谢你的帮助-不知道是否有一些方法,我可以把他们给你?我试着描述一下:唯一存在的约束来自3个表,它们都有字段,这些字段通过一个简单的INT-Id键引用代码。问题似乎是Employee有几个字段引用代码表,我希望它们都级联设置为NULL。我所需要的是,当代码被删除时,对它们的引用应该在任何地方都设置为null。。。我想这里的任何人都不会介意,代码窗口将在滚动块中正确设置它们的格式:解决方案之一是您的评论帮助我理解了我所面临的问题。非常感谢。我更愿意关闭其中一个路径的级联删除,然后处理其他记录的删除—一些其他方式存储的过程;触发器;通过代码等,但我将您的解决方案分组在一条路径中,以用于同一问题的可能不同应用程序…一个用于使用单词crux并解释这是否比编写触发器更好?仅仅为了让级联工作而添加一个额外的表似乎很奇怪。任何东西都比编写触发器好。他们的逻辑不透明,与其他任何东西相比效率都很低。为了更好的控制,将大型表拆分为小型表只是更好的规范化数据库的自然结果,而不是需要关注的问题。我仍然无法理解的一点是,如果这个问题可以通过使用触发器来解决,那么,为什么触发器不会导致循环或多个级联路径@armen:因为触发器会显式地提供系统自己无法隐式理解的逻辑,例如,如果删除引用操作有多个路径,那么触发器代码将定义删除哪些表以及删除顺序。并且触发器在第一个操作完成后执行,因此不会出现争用邓布利多:我的意思是,只有当组合上的约束无法完成任务时才使用触发器。约束是声明性的,它们的实现由系统负责。触发器是过程代码,您必须对实现进行编码和调试,并忍受其缺点、性能较差等。问题是,只要您删除外键约束,触发器就可以工作,这意味着在数据库插入上没有引用完整性检查,因此需要更多的触发器来处理。触发解决方案是一个兔子洞,导致数据库设计退化。如果使用RI约束,则实际上无法在数据中构建循环说得好!当然,您可以构建数据循环,但MSSQL只能使用更新。其他RDBMs支持在提交时确保的延迟约束完整性,而不是在插入/更新/删除时。您可以在不更改sql脚本的情况下更改迁移文件,例如,在您的迁移文件中,您可以将onDelete操作设置为从级联限制。最好使用fluent批注指定此操作,这样,如果您最终删除并重新创建迁移文件夹,您就不必记住执行此操作。根据我的经验,fluent批注可以使用,也应该使用我使用的 哼哼,但他们经常是相当童车。在代码中简单地指定它们并不总是能产生预期的结果。
    modelBuilder.Entity<Qualification>().**HasRequired**(x=> x.Employee).WithMany(e => e.Qualifications); 
    modelBuilder.Entity<Qualification>().**HasOptional**(x=> x.Employee).WithMany(e => e.Qualifications);
protected override void OnModelCreating( DbModelBuilder modelBuilder )
{
    modelBuilder.Entity<TableName>().HasMany(i => i.Member).WithRequired().WillCascadeOnDelete(false);
}
modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();
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