外部キー制約により、サイクルまたは複数のカスケードパスが発生する可能性がありますか?


176

テーブルに制約を追加しようとすると問題が発生します。エラーが発生します:

テーブル 'Employee'にFOREIGN KEY制約 'FK74988DB24B3C886'を導入すると、サイクルまたは複数のカスケードパスが発生する可能性があります。ON DELETE NO ACTIONまたはON UPDATE NO ACTIONを指定するか、他のFOREIGN KEY制約を変更します。

私の制約は、Codeテーブルとテーブルの間にありemployeeます。Code表には含まれていIdNameFriendlyNameTypeValue。にemployeeはコードを参照するいくつかのフィールドがあるため、コードのタイプごとに参照を作成できます。

参照されているコードが削除された場合、フィールドをnullに設定する必要があります。

どうすればこれを行うことができますか?


ソリューションの1
つがここに

回答:


180

SQL Serverはカスケードパスの単純なカウントを行い、実際にサイクルが存在するかどうかを判断しようとするのではなく、最悪の場合を想定して参照アクション(CASCADE)の作成を拒否します。参照アクションなしで制約を作成できます。デザインを変更できない場合(または変更すると事態が悪化する場合)、最後の手段としてトリガーの使用を検討する必要があります。

FWIWカスケードパスの解決は複雑な問題です。他のSQL製品は単に問題を無視し、サイクルを作成できるようにします。その場合、おそらくデザイナーの無知により、最後に値を上書きするかどうかを確認するのは競争になります(たとえば、ACE / Jetはこれを行います)。一部のSQL製品が単純なケースを解決しようとすることを理解しています。事実は残っていますが、SQL Serverは複数のパスを許可しないことで、安全性を最大限に発揮し、少なくともそのように指示します。

マイクロソフト自身、FK制約の代わりにトリガーを使用することを推奨しています。


2
私がまだ理解できないことの1つは、この「問題」がトリガーを使用して解決できる場合、トリガーが「サイクルまたは複数のカスケードパスを引き起こす」ことがないのはなぜですか?
アーメン2014

5
@armen:トリガーは、システムがそれ自体で暗黙的に把握できなかったロジックを明示的に提供するため、たとえば参照削除アクションのパスが複数ある場合、トリガーコードは削除されるテーブルとその順序を定義します。
2014

6
また、最初の操作が完了した後にトリガーが実行されるため、競合は発生しません。
Bon

2
@dumbledad:つまり、制約(おそらく組み合わせ)が仕事を完了できない場合にのみトリガーを使用します。制約は宣言的であり、その実装はシステムの責任です。トリガーは手続き型コードであり、実装をコーディング(およびデバッグ)して、その不利な点(パフォーマンスの低下など)に耐える必要があります。
2016

1
これの問題は、外部キー制約を削除する限りトリガーが機能することです。つまり、データベースの挿入に対する参照整合性チェックが行われず、それを処理するためにさらに多くのトリガーが必要になります。トリガーソリューションは、退化したデータベース設計につながるうさぎの穴です。
ニュートリノ

99

複数のカスケードパスを持つ典型的な状況は次のようになります。2つの詳細を持つマスターテーブル、たとえば「マスター」と「詳細1」と「詳細2」としましょう。両方の詳細はカスケード削除です。これまでのところ問題はありません。しかし、両方の詳細に他のテーブル(「SomeOtherTable」など)との1対多の関係がある場合はどうでしょうか。SomeOtherTableには、Detail1ID列とDetail2ID列があります。

Master { ID, masterfields }

Detail1 { ID, MasterID, detail1fields }

Detail2 { ID, MasterID, detail2fields }

SomeOtherTable {ID, Detail1ID, Detail2ID, someothertablefields }

つまり、SomeOtherTableの一部のレコードはDetail1レコードにリンクされ、SomeOtherTableの一部のレコードはDetail2レコードにリンクされます。SomeOtherTableレコードが両方の詳細に属さないことが保証されている場合でも、マスターからSomeOtherTable(Detail1を介して1つとDetail2を介して1つ)へのカスケードパスが複数あるため、SomeOhterTableのレコードを両方の詳細に対してカスケード削除することは不可能になりました。今、あなたはすでにこれを理解しているかもしれません。これが可能な解決策です:

Master { ID, masterfields }

DetailMain { ID, MasterID }

Detail1 { DetailMainID, detail1fields }

Detail2 { DetailMainID, detail2fields }

SomeOtherTable {ID, DetailMainID, someothertablefields }

すべてのIDフィールドはキーフィールドと自動インクリメントです。重要な部分は、詳細テーブルのDetailMainIdフィールドにあります。これらのフィールドは、キーと参照制約の両方です。マスターレコードを削除するだけで、すべてをカスケード削除できるようになりました。欠点は、detail1レコードごと、およびdetail2レコードごとに、DetailMainレコード(実際には、正しい一意のIDを取得するために最初に作成される)も必要であるということです。


1
あなたのコメントは私が直面している問題を理解するのに大いに役立ちました。ありがとうございました!パスの1つでカスケード削除をオフにしてから、他のレコードの削除をいくつかの他の方法で処理します(ストアドプロシージャ、トリガー、コードなど)。しかし、私は同じ問題の考えられるさまざまなアプリケーションについてあなたの解決策(1つのパスでグループ化)を念頭に置いています...
freewill

1
cruxという単語の使用のために(そして説明のためにも)
masterwok

これはトリガーを記述するよりも優れていますか?カスケードを機能させるためだけにテーブルを追加するのは奇妙に思えます。
dumbledad 2016

トリガーを記述するよりも優れています。それらのロジックは不透明であり、他のものと比較して非効率的です。細かい制御のために大きなテーブルを小さなテーブルに分割することは、より良い正規化されたデータベースの自然な帰結であり、それ自体は懸念事項ではありません。
ニュートリノ

12

(機能的に)SCHEMAとDATAのサイクルや複数のパスの間に大きな違いがあることを指摘します。DATAのサイクルおよびおそらくマルチパスは処理を複雑にし、パフォーマンスの問題(「適切に」処理するコスト)を引き起こす可能性がありますが、スキーマのこれらの特性のコストはゼロに近いはずです。

RDBの見かけのサイクルのほとんどは階層構造(組織図、部品、サブ部品など)で発生するため、SQL Serverが最悪の事態を想定するのは残念です。つまり、スキーマサイクル==データサイクルです。実際、RI制約を使用している場合、実際にデータのサイクルを構築することはできません。

マルチパスの問題も同様だと思います。つまり、スキーマ内の複数のパスが必ずしもデータ内の複数のパスを意味するわけではありませんが、マルチパスの問題の経験は少ないです。

もちろん、SQL Server サイクルを許可した場合でも、深度は32になりますが、ほとんどの場合はそれで十分でしょう。(残念ながら、それはデータベース設定ではありません!)

「代わりに削除」トリガーも機能しません。2回目にテーブルにアクセスすると、トリガーは無視されます。したがって、カスケードを本当にシミュレートしたい場合は、サイクルの存在下でストアドプロシージャを使用する必要があります。ただし、代わりに削除トリガーはマルチパスの場合に機能します。

Celkoは、循環を導入しない階層を表現するための「より良い」方法を提案していますが、トレードオフがあります。


「RI制約を使用している場合、実際にデータにサイクルを構築することはできません!」 - いい視点ね!
onedaywhen

確かにデータの循環性を構築できますが、MSSQLではUPDATEのみを使用します。他のRDBMは遅延制約をサポートします(整合性は、挿入/更新/削除時ではなく、コミット時に保証されます)。
Carl Krig


3

その音によって、既存の外部キーの1つにOnDelete / OnUpdateアクションがあり、コードテーブルが変更されます。

この外部キーを作成することで、循環的な問題が発生します。

たとえば、従業員を更新すると、更新時アクションによってコードが変更され、更新時アクションによって従業員が変更されます...など...

両方のテーブルのテーブル定義と外部キー/制約定義を投稿すると、問題がどこにあるかがわかります...


1
それらはかなり長いので、私はそれらをここに投稿することはできないと思いますが、私はあなたの助けに感謝します-私があなたに送ることができる方法があるかどうかわかりませんか?試して説明してみましょう。存在する唯一の制約は、単純なINT Idキーによってコードを参照するフィールドを持つすべての3つのテーブルからのものです。問題は、従業員がコードテーブルを参照するいくつかのフィールドを持っていることと、それらすべてをSET NULLにカスケードすることです。私が必要なのは、コードが削除されたときに、それらへの参照をどこでもnullに設定することです。

とにかくそれらを投稿します...ここではだれも気にしないと思います。コードウィンドウは、スクロールブロックでそれらを適切にフォーマットします:)
Eoin Campbell

2

これは、Emplyeeが他のエンティティのコレクションを持っている可能性があるためです。

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

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

0

これは、データベーストリガーポリシータイプのエラーです。トリガーはコードであり、カスケード削除などのカスケード関係にインテリジェンスまたは条件を追加できます。CascadeOnDeleteをオフにするなど、関連するテーブルオプションを特殊化する必要がある場合があります。

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

または、この機能を完全にオフにします。

modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();

-2

ASP.NET Core 2.0およびEF Core 2.0を使用して発生したこの問題に対する私の解決策は、以下を順番に実行することでした。

  1. update-databaseパッケージ管理コンソール(PMC)でコマンドを実行してデータベースを作成します(これにより、「FOREIGN KEY制約の導入...サイクルまたは複数のカスケードパスが発生する可能性があります。」エラーが発生します)。

  2. script-migration -IdempotentPMCでコマンドを実行して、既存のテーブル/制約に関係なく実行できるスクリプトを作成します

  3. 結果のスクリプトを取得し、検索ON DELETE CASCADEして置き換えますON DELETE NO ACTION

  4. 変更されたSQLをデータベースに対して実行する

これで、移行は最新の状態になり、連鎖的な削除は発生しなくなります。

Entity Framework Core 2.0でこれを行う方法を見つけることができませんでした。

幸運を!


(SQLスクリプトを変更せずに)移行ファイルを変更できます。つまり、移行ファイルでonDeleteアクションをカスケードから制限に設定できます
Rushi Soni

流暢な注釈を使用してこれを指定することをお勧めします。これにより、移行フォルダーを削除して再作成することになった場合に、忘れずにこれを行う必要があります。
アレン王

私の経験では、流暢な注釈を使用でき、使用する必要があります(私はそれらを使用します)が、それらはしばしばかなりバグが多いものです。コードでそれらを指定するだけでは、必ずしも期待どおりの結果が得られるとは限りません。
user1477388
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.