Batasan kunci asing dapat menyebabkan siklus atau beberapa jalur kaskade?

176

Saya punya masalah ketika saya mencoba menambahkan kendala ke tabel saya. Saya mendapatkan kesalahan:

Memperkenalkan kendala KUNCI ASING 'FK74988DB24B3C886' di atas meja 'Karyawan' dapat menyebabkan siklus atau banyak jalur kaskade. Tentukan HAPUS TANPA TINDAKAN atau DIPERBARUI TANPA TINDAKAN, atau ubah batasan KUNCI ASING lainnya.

Batasan saya adalah antara Codetabel dan employeetabel. The Codetabel berisi Id, Name, FriendlyName, Typedan Value. The employeememiliki sejumlah bidang yang kode referensi, sehingga ada dapat menjadi acuan untuk setiap jenis kode.

Saya perlu kolom diatur ke nol jika kode yang direferensikan dihapus.

Ada ide bagaimana saya bisa melakukan ini?

Ricardo Altamirano
sumber
Salah satu solusinya ada di sini
IsmailS

Jawaban:

180

SQL Server melakukan penghitungan sederhana jalur kaskade dan, alih-alih mencoba mencari tahu apakah ada siklus yang benar-benar ada, ia mengasumsikan yang terburuk dan menolak untuk membuat tindakan referensial (CASCADE): Anda bisa dan masih harus membuat kendala tanpa tindakan referensial. Jika Anda tidak dapat mengubah desain Anda (atau melakukan hal itu akan membahayakan hal-hal) maka Anda harus mempertimbangkan menggunakan pemicu sebagai upaya terakhir.

Menyelesaikan jalur kaskade FWIW adalah masalah yang kompleks. Produk SQL lainnya hanya akan mengabaikan masalah dan memungkinkan Anda untuk membuat siklus, dalam hal ini akan menjadi perlombaan untuk melihat yang akan menimpa nilai terakhir, mungkin karena ketidaktahuan desainer (mis. ACE / Jet melakukan ini). Saya mengerti beberapa produk SQL akan berusaha menyelesaikan kasus sederhana. Faktanya tetap, SQL Server bahkan tidak mencoba, memainkannya sangat aman dengan melarang lebih dari satu jalur dan setidaknya itu memberitahu Anda begitu.

Microsoft sendiri menyarankan penggunaan pemicu bukannya kendala FK.

suatu hari nanti
sumber
2
satu hal yang saya masih tidak mengerti adalah bahwa, jika "masalah" ini dapat diselesaikan dengan menggunakan pemicu, lalu mengapa pemicu tidak akan "menyebabkan siklus atau beberapa jalur kaskade ..."?
armen
5
@armen: karena pemicu Anda akan secara eksplisit menyediakan logika bahwa sistem tidak dapat secara implisit mencari tahu sendiri, misalnya jika ada beberapa jalur untuk tindakan referensi yang dihapus maka kode pemicu Anda akan menentukan tabel mana yang dihapus dan urutannya.
onedaywhen
6
Dan juga pelatuk dijalankan setelah operasi pertama selesai sehingga tidak ada perlombaan yang terjadi.
Bon
2
@dumbledad: Maksud saya, hanya menggunakan pemicu ketika kendala (mungkin kombinasi) tidak bisa menyelesaikan pekerjaan. Kendala bersifat deklaratif dan implementasinya merupakan tanggung jawab sistem. Pemicu adalah kode prosedural dan Anda harus memberi kode (dan men-debug) implementasi dan menanggung kerugiannya (kinerja yang lebih buruk, dll.).
onedaywhen
1
Masalah dengan ini adalah bahwa pemicu hanya berfungsi selama Anda menghapus batasan kunci asing, yang berarti Anda kemudian tidak memiliki pemeriksaan integritas referensial pada sisipan basis data dan karenanya Anda perlu lebih banyak pemicu untuk mengatasinya. Solusi pemicunya adalah lubang kelinci yang mengarah ke desain database yang merosot.
Neutrino
99

Situasi khas dengan beberapa jalur cascasing adalah sebagai berikut: Tabel master dengan dua detail, katakanlah "Master" dan "Detail1" dan "Detail2". Kedua detail adalah penghapusan kaskade. Sejauh ini tidak ada masalah. Tetapi bagaimana jika kedua detail memiliki hubungan satu-ke-banyak dengan beberapa tabel lain (katakanlah "SomeOtherTable"). SomeOtherTable memiliki kolom Detail1ID DAN kolom Detail2ID.

Master { ID, masterfields }

Detail1 { ID, MasterID, detail1fields }

Detail2 { ID, MasterID, detail2fields }

SomeOtherTable {ID, Detail1ID, Detail2ID, someothertablefields }

Dengan kata lain: beberapa catatan di SomeOtherTable terkait dengan Detail1-catatan dan beberapa catatan di SomeOtherTable terkait dengan catatan Detail2. Bahkan jika dijamin SomeOtherTable-records tidak pernah menjadi milik kedua Details, sekarang mustahil untuk membuat catatan SomeOhterTable menghapus semua rincian, karena ada beberapa jalur cascading dari Master ke SomeOtherTable (satu via Detail1 dan satu via Detail2). Sekarang Anda mungkin sudah memahami ini. Berikut ini adalah solusi yang mungkin:

Master { ID, masterfields }

DetailMain { ID, MasterID }

Detail1 { DetailMainID, detail1fields }

Detail2 { DetailMainID, detail2fields }

SomeOtherTable {ID, DetailMainID, someothertablefields }

Semua bidang ID adalah bidang kunci dan penambahan otomatis. Intinya terletak pada bidang DetailMainId dari tabel Detail. Bidang-bidang ini adalah kunci dan referensial yang bertentangan. Sekarang dimungkinkan untuk menghapus semuanya dengan hanya menghapus catatan master. Kelemahannya adalah bahwa untuk setiap detail1-record DAN untuk setiap detail2 record, harus juga ada DetailMain-record (yang sebenarnya dibuat terlebih dahulu untuk mendapatkan id yang benar dan unik).

hans riesebo
sumber
1
Komentar Anda banyak membantu saya untuk memahami masalah yang saya hadapi. Terima kasih! Saya lebih suka mematikan penghapusan kaskade untuk salah satu jalur, kemudian menangani penghapusan catatan lain dengan beberapa cara lain (prosedur tersimpan; pemicu; dengan kode, dll). Tetapi saya tetap mengingat solusi Anda (pengelompokan dalam satu jalur) untuk kemungkinan aplikasi yang berbeda dari masalah yang sama ...
bebas
1
Satu untuk penggunaan kata crux (dan juga untuk menjelaskan)
masterwok
Apakah ini lebih baik daripada pemicu penulisan? Tampaknya aneh untuk menambahkan tabel tambahan hanya agar kaskade berfungsi.
dumbledad
Apa pun lebih baik daripada pemicu penulisan. Logikanya buram dan tidak efisien dibandingkan dengan yang lain. Memecah tabel besar menjadi yang lebih kecil untuk kontrol yang lebih baik hanyalah konsekuensi alami dari database yang dinormalisasi lebih baik dan tidak dengan sendirinya sesuatu yang perlu diperhatikan.
Neutrino
12

Saya akan menunjukkan bahwa (secara fungsional) ada perbedaan BESAR antara siklus dan / atau beberapa jalur dalam SCHEMA dan DATA. Sementara siklus dan mungkin multipath dalam DATA tentu dapat mempersulit pemrosesan dan menyebabkan masalah kinerja (biaya penanganan yang "benar"), biaya karakteristik ini dalam skema harus mendekati nol.

Karena siklus yang paling jelas dalam RDB terjadi dalam struktur hierarkis (bagan org, bagian, sub bagian, dll.) Sangat disayangkan bahwa SQL Server mengasumsikan yang terburuk; yaitu, siklus skema == siklus data. Bahkan, jika Anda menggunakan batasan RI, Anda tidak dapat benar-benar membangun siklus dalam data!

Saya menduga masalah multipath serupa; yaitu, beberapa jalur dalam skema tidak selalu menyiratkan beberapa jalur dalam data, tapi saya kurang berpengalaman dengan masalah multipath.

Tentu saja jika SQL Server tidak memungkinkan siklus masih akan tunduk pada kedalaman 32, tapi itu mungkin cukup untuk sebagian besar kasus. (Sayang sekali itu bukan pengaturan database!)

Pemicu "Alih-alih Hapus" juga tidak berfungsi. Kali kedua tabel dikunjungi, pemicunya diabaikan. Jadi, jika Anda benar-benar ingin mensimulasikan kaskade Anda harus menggunakan prosedur tersimpan di hadapan siklus. Pemicu Alih-dari-Hapus akan bekerja untuk kasus multi-kasus.

Celko menyarankan cara "lebih baik" untuk mewakili hierarki yang tidak memperkenalkan siklus, tetapi ada pengorbanan.

Bill Cohagan
sumber
"Jika Anda menggunakan batasan RI, Anda tidak dapat benar-benar membangun siklus dalam data!" - Poin bagus!
onedaywhen
Tentu Anda bisa membangun sirkular data, tetapi dengan MSSQL hanya menggunakan UPDATE. RDBM lainnya mendukung kendala yang ditangguhkan (integritas dipastikan pada saat komitmen, bukan pada saat memasukkan / memperbarui / menghapus).
Carl Krig
3

Dengan suara Anda memiliki tindakan OnDelete / OnUpdate pada salah satu Kunci Asing Anda yang ada, yang akan mengubah tabel kode Anda.

Jadi dengan membuat Kunci Asing ini, Anda akan menciptakan masalah siklik,

Misalnya Memperbarui Karyawan, menyebabkan Kode diubah oleh Tindakan Pembaruan, menyebabkan Karyawan diubah oleh Tindakan Pembaruan ... dll ...

Jika Anda memposting Definisi Tabel Anda untuk kedua tabel, & definisi Foreign Key / constraint Anda, kami harus dapat memberi tahu Anda di mana masalahnya ...

Eoin Campbell
sumber
1
Mereka cukup panjang, jadi saya rasa saya tidak bisa mempostingnya di sini, tapi saya akan sangat menghargai bantuan Anda - tidak tahu apakah ada cara saya dapat mengirimkannya kepada Anda? Saya akan coba dan jelaskan: Satu-satunya kendala yang ada adalah dari 3 tabel yang semuanya memiliki bidang yang merujuk kode dengan kunci INT Id sederhana. Masalahnya tampaknya bahwa Karyawan memiliki beberapa bidang yang mereferensikan tabel kode dan bahwa saya ingin mereka semua mengalir ke SET NULL. Yang saya butuhkan adalah ketika kode dihapus, referensi ke mereka harus disetel ke nol di mana-mana.
posting mereka pula ... Saya tidak berpikir siapa pun di sini akan keberatan, dan jendela kode akan memformat mereka dengan baik di blok bergulir :)
Eoin Campbell
2

Ini karena Emplyee mungkin memiliki Koleksi entitas lain mengatakan Kualifikasi dan Kualifikasi mungkin memiliki beberapa koleksi Universitas lain misalnya

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

}

Pada DataContext bisa jadi seperti di bawah ini

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

}

dalam hal ini ada rantai dari Karyawan ke Kualifikasi dan Dari Kualifikasi ke Universitas. Jadi itu melempar pengecualian yang sama bagi saya.

Itu bekerja untuk saya ketika saya berubah

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

Untuk

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

Pemicu adalah solusi untuk masalah ini:

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
Nada Soda
sumber
0

Ini adalah kesalahan kebijakan pemicu basis data jenis. Pemicu adalah kode dan dapat menambahkan beberapa kecerdasan atau kondisi ke relasi Cascade seperti Penghapusan Cascade. Anda mungkin perlu mengkhususkan opsi tabel terkait di sekitar ini seperti Mematikan CascadeOnDelete :

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

Atau Matikan fitur ini sepenuhnya:

modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();
Amirhossein Mehrvarzi
sumber
-2

Solusi saya untuk masalah ini yang dihadapi menggunakan ASP.NET Core 2.0 dan EF Core 2.0 adalah dengan melakukan hal berikut secara berurutan:

  1. Jalankan update-databaseperintah di Package Management Console (PMC) untuk membuat database (ini menghasilkan "Memperkenalkan kendala KUNCI ASING ... dapat menyebabkan siklus atau beberapa jalur kaskade.")

  2. Jalankan script-migration -Idempotentperintah di PMC untuk membuat skrip yang dapat dijalankan terlepas dari tabel / batasan yang ada

  3. Ambil skrip yang dihasilkan dan temukan ON DELETE CASCADEserta ganti denganON DELETE NO ACTION

  4. Jalankan SQL yang dimodifikasi terhadap database

Sekarang, migrasi Anda harus mutakhir dan penghapusan berjenjang tidak boleh terjadi.

Sayang sekali saya tidak dapat menemukan cara untuk melakukan ini di Entity Framework Core 2.0.

Semoga berhasil!

pengguna1477388
sumber
Anda dapat mengubah file migrasi Anda untuk melakukannya (tanpa mengubah skrip sql), yaitu dalam file migrasi Anda, Anda dapat mengatur aksi onDelete ke Restrict from Cascade
Rushi Soni
Lebih baik untuk menentukan ini menggunakan anotasi yang lancar sehingga Anda tidak harus ingat untuk melakukan ini jika Anda akhirnya menghapus dan membuat ulang folder migrasi Anda.
Allen Wang
Dalam pengalaman saya, anotasi yang lancar dapat digunakan dan harus digunakan (saya menggunakannya) tetapi mereka sering sangat bermasalah. Cukup menentukan mereka dalam kode tidak selalu bekerja menghasilkan hasil yang diharapkan.
user1477388