Ràng buộc khóa ngoài có thể gây ra chu kỳ hoặc nhiều đường dẫn tầng?


176

Tôi có một vấn đề khi tôi cố gắng thêm các ràng buộc vào các bảng của mình. Tôi nhận được lỗi:

Giới thiệu ràng buộc FOREIGN KEY 'FK74988DB24B3C886' trên bảng 'Nhân viên' có thể gây ra chu kỳ hoặc nhiều đường dẫn tầng. Chỉ định TRÊN XÓA KHÔNG CÓ HÀNH ĐỘNG hoặc CẬP NHẬT KHÔNG CÓ HÀNH ĐỘNG, hoặc sửa đổi các ràng buộc khác NGOẠI TỪ.

Ràng buộc của tôi là giữa một Codebảng và một employeebảng. Các Codebảng chứa Id, Name, FriendlyName, TypeValue. Có employeemột số trường tham chiếu mã, để có thể có một tham chiếu cho từng loại mã.

Tôi cần cho các trường được đặt thành null nếu mã được tham chiếu bị xóa.

Bất kỳ ý tưởng làm thế nào tôi có thể làm điều này?


Một trong những giải pháp là đây
IsmailS

Câu trả lời:


180

SQL Server thực hiện đơn giản việc đếm các đường dẫn theo tầng và, thay vì cố gắng tìm hiểu xem có bất kỳ chu kỳ nào thực sự tồn tại hay không, nó giả định điều tồi tệ nhất và từ chối tạo các hành động tham chiếu (CASCADE): bạn vẫn có thể và vẫn nên tạo các ràng buộc mà không cần các hành động tham chiếu. Nếu bạn không thể thay đổi thiết kế của mình (hoặc làm như vậy sẽ thỏa hiệp mọi thứ) thì bạn nên xem xét sử dụng kích hoạt như là phương sách cuối cùng.

FWIW giải quyết các đường dẫn tầng là một vấn đề phức tạp. Các sản phẩm SQL khác sẽ đơn giản bỏ qua vấn đề và cho phép bạn tạo các chu kỳ, trong trường hợp đó sẽ là một cuộc đua để xem cái nào sẽ ghi đè lên giá trị cuối cùng, có thể là do sự thiếu hiểu biết của nhà thiết kế (ví dụ: ACE / Jet làm điều này). Tôi hiểu một số sản phẩm SQL sẽ cố gắng giải quyết các trường hợp đơn giản. Sự thật vẫn còn, SQL Server thậm chí không thử, chơi nó cực kỳ an toàn bằng cách không cho phép nhiều hơn một đường dẫn và ít nhất nó cho bạn biết như vậy.

Chính Microsoft khuyên nên sử dụng các kích hoạt thay vì các ràng buộc FK.


2
một điều mà tôi vẫn không thể hiểu là, nếu "vấn đề" này có thể được giải quyết bằng cách sử dụng một trình kích hoạt, thì tại sao một trình kích hoạt sẽ không "gây ra chu kỳ hoặc nhiều đường dẫn tầng ..."?
armen

5
@armen: bởi vì trình kích hoạt của bạn sẽ cung cấp logic một cách rõ ràng rằng hệ thống không thể tự tìm ra chính nó, ví dụ nếu có nhiều đường dẫn cho một hành động tham chiếu xóa thì mã kích hoạt của bạn sẽ xác định các bảng sẽ bị xóa và theo thứ tự nào.
onedaywhen

6
Và cũng kích hoạt thực thi sau khi hoạt động đầu tiên hoàn thành để không có cuộc đua đang diễn ra.
Bon

2
@dumbledad: Ý tôi là, chỉ sử dụng kích hoạt khi các ràng buộc (có thể kết hợp) không thể hoàn thành công việc. Các ràng buộc là khai báo và việc thực hiện chúng là trách nhiệm của hệ thống. Kích hoạt là mã thủ tục và bạn phải mã hóa (và gỡ lỗi) việc thực hiện và chịu đựng những nhược điểm của chúng (hiệu suất kém hơn, v.v.).
onedaywhen

1
Vấn đề với điều này là trình kích hoạt chỉ hoạt động miễn là bạn loại bỏ ràng buộc khóa ngoại, điều đó có nghĩa là sau đó bạn không kiểm tra tính toàn vẹn tham chiếu trên các chèn cơ sở dữ liệu và do đó bạn cần nhiều trình kích hoạt hơn để xử lý điều đó. Giải pháp kích hoạt là một lỗ thỏ dẫn đến thiết kế cơ sở dữ liệu suy biến.
Neutrino

99

Một tình huống điển hình với nhiều đường dẫn xếp tầng sẽ là: Bảng chính có hai chi tiết, giả sử "Chính" và "Chi tiết1" và "Chi tiết 2". Cả hai chi tiết được xóa tầng. Cho đến nay không có vấn đề. Nhưng điều gì sẽ xảy ra nếu cả hai chi tiết có mối quan hệ một-nhiều với một số bảng khác (giả sử "someOtherTable"). Một sốOtherTable có cột Chi tiết1ID VÀ cột Chi tiết2ID.

Master { ID, masterfields }

Detail1 { ID, MasterID, detail1fields }

Detail2 { ID, MasterID, detail2fields }

SomeOtherTable {ID, Detail1ID, Detail2ID, someothertablefields }

Nói cách khác: một số bản ghi trong someOtherTable được liên kết với bản ghi chi tiết1 và một số bản ghi trong someOtherTable được liên kết với bản ghi chi tiết2. Ngay cả khi được đảm bảo rằng các bản ghi của someOtherTable không bao giờ thuộc cả hai Chi tiết, thì hiện tại không thể xóa các bản ghi tầng của someOhterTable cho cả hai chi tiết, bởi vì có nhiều đường dẫn xếp tầng từ Master sang someOtherTable (một qua Chi tiết1 và một qua Chi tiết 2). Bây giờ bạn có thể đã hiểu điều này. Đây là một giải pháp có thể:

Master { ID, masterfields }

DetailMain { ID, MasterID }

Detail1 { DetailMainID, detail1fields }

Detail2 { DetailMainID, detail2fields }

SomeOtherTable {ID, DetailMainID, someothertablefields }

Tất cả các trường ID là trường khóa và tăng tự động. Điểm mấu chốt nằm trong các trường Chi tiếtMainId của các bảng Chi tiết. Các lĩnh vực này là cả chống chỉ định và tham chiếu. Bây giờ có thể xếp tầng xóa mọi thứ bằng cách chỉ xóa các bản ghi chính. Nhược điểm là đối với mỗi bản ghi chi tiết1 và cho mỗi bản ghi chi tiết2, cũng phải có bản ghi chi tiết (thực sự được tạo trước để có id chính xác và duy nhất).


1
Nhận xét của bạn đã giúp tôi rất nhiều để hiểu vấn đề mà tôi đang phải đối mặt. Cảm ơn bạn! Tôi muốn tắt xóa tầng theo một trong các đường dẫn, sau đó xử lý xóa các bản ghi khác theo một số cách khác (thủ tục được lưu trữ; kích hoạt; bằng mã, v.v.). Nhưng tôi giữ giải pháp của bạn (nhóm trong một đường dẫn) trong tâm trí cho các ứng dụng khác nhau có thể xảy ra cùng một vấn đề ...
ý chí tự do

1
Một trong những sử dụng từ khóa (và cũng để giải thích)
masterwok

Điều này có tốt hơn so với việc kích hoạt bằng văn bản không? Có vẻ kỳ lạ khi thêm một bảng bổ sung chỉ để làm cho tầng hoạt động.
dumbledad 2/2/2016

Bất cứ điều gì tốt hơn so với viết kích hoạt. Logic của họ mờ đục và họ không hiệu quả so với bất cứ điều gì khác. Việc chia các bảng lớn thành các bảng nhỏ hơn để kiểm soát tốt hơn chỉ là kết quả tự nhiên của cơ sở dữ liệu được chuẩn hóa tốt hơn và bản thân nó không phải là điều cần quan tâm.
Neutrino

12

Tôi sẽ chỉ ra rằng (về mặt chức năng) có sự khác biệt LỚN giữa các chu kỳ và / hoặc nhiều đường dẫn trong SCHema và DATA. Trong khi các chu kỳ và có lẽ là đa đường trong DATA chắc chắn có thể xử lý phức tạp và gây ra các vấn đề về hiệu năng (chi phí xử lý "đúng"), chi phí của các đặc điểm này trong lược đồ phải gần bằng không.

Vì hầu hết các chu kỳ rõ ràng trong RDB xảy ra trong các cấu trúc phân cấp (biểu đồ tổ chức, phần, phần phụ, v.v.), thật không may là SQL Server giả định điều tồi tệ nhất; tức là chu trình lược đồ == chu kỳ dữ liệu. Thực tế, nếu bạn đang sử dụng các ràng buộc RI, bạn thực sự không thể xây dựng một chu kỳ trong dữ liệu!

Tôi nghi ngờ vấn đề đa đường là tương tự; tức là, nhiều đường dẫn trong lược đồ không nhất thiết ngụ ý nhiều đường dẫn trong dữ liệu, nhưng tôi có ít kinh nghiệm hơn với vấn đề đa đường.

Tất nhiên nếu SQL Server đã làm cho phép các chu kỳ thì nó vẫn phải chịu độ sâu 32, nhưng điều đó có lẽ phù hợp với hầu hết các trường hợp. (Quá tệ đó không phải là một thiết lập cơ sở dữ liệu!)

Kích hoạt "Thay vì xóa" cũng không hoạt động. Lần thứ hai một bảng được truy cập, kích hoạt được bỏ qua. Vì vậy, nếu bạn thực sự muốn mô phỏng một tầng, bạn sẽ phải sử dụng các thủ tục được lưu trữ với sự có mặt của các chu kỳ. Tuy nhiên, Bộ kích hoạt thay thế sẽ hoạt động cho nhiều trường hợp.

Celko gợi ý một cách "tốt hơn" để thể hiện các hệ thống phân cấp không giới thiệu chu kỳ, nhưng có sự đánh đổi.


"nếu bạn đang sử dụng các ràng buộc RI, bạn thực sự không thể xây dựng một chu kỳ trong dữ liệu!" -- điểm tốt!
onedaywhen

Chắc chắn bạn có thể xây dựng tính tuần hoàn dữ liệu, nhưng với MSSQL chỉ sử dụng CẬP NHẬT. Các RDBM khác hỗ trợ các ràng buộc hoãn lại (đảm bảo tính toàn vẹn tại thời điểm cam kết, không phải tại thời điểm chèn / cập nhật / xóa).
Carl Krig


3

Bằng âm thanh của nó, bạn có một hành động OnDelete / OnUpdate trên một trong các Khóa ngoại hiện tại của bạn, điều đó sẽ sửa đổi bảng mã của bạn.

Vì vậy, bằng cách tạo Khóa ngoại này, bạn sẽ tạo ra một vấn đề theo chu kỳ,

Ví dụ: Cập nhật nhân viên, khiến các mã bị thay đổi bởi một hành động cập nhật, khiến nhân viên bị thay đổi bởi một hành động cập nhật ... vv ...

Nếu bạn đăng Định nghĩa bảng cho cả hai bảng và định nghĩa ràng buộc / Khóa ngoài của bạn, chúng tôi sẽ có thể cho bạn biết vấn đề ở đâu ...


1
Chúng khá dài, vì vậy tôi không nghĩ rằng tôi có thể đăng chúng ở đây, nhưng tôi sẽ đánh giá cao sự giúp đỡ của bạn - không biết có cách nào tôi có thể gửi chúng cho bạn không? Hãy thử và mô tả nó: Các ràng buộc duy nhất tồn tại là từ 3 bảng mà tất cả đều có các trường tham chiếu mã bằng một khóa INT Id đơn giản. Vấn đề dường như là Nhân viên có một số trường tham chiếu bảng mã và tôi muốn tất cả chúng xếp tầng thành SET NULL. Tất cả những gì tôi cần là khi mã bị xóa, các tham chiếu đến chúng phải được đặt thành null ở mọi nơi.

dù sao thì hãy đăng chúng lên ... Tôi không nghĩ có ai ở đây sẽ bận tâm và cửa sổ mã sẽ định dạng chúng đúng cách trong một khối cuộn :)
Eoin Campbell

2

Điều này là do Emplyee có thể có Bộ sưu tập của các thực thể khác nói Trình độ và Chứng chỉ có thể có một số trường Đại học sưu tập khác, vd

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

}

Trên DataContext nó có thể như dưới đây

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

}

trong trường hợp này, có chuỗi từ Nhân viên đến Trình độ chuyên môn và Từ Trình độ đến Đại học. Vì vậy, nó đã ném ngoại lệ tương tự với tôi.

Nó làm việc cho tôi khi tôi thay đổi

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

Đến

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

1

Kích hoạt là giải pháp cho vấn đề này:

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

Đây là một lỗi của các chính sách kích hoạt cơ sở dữ liệu. Một trình kích hoạt là mã và có thể thêm một số thông tin hoặc điều kiện vào mối quan hệ Cascade như Cascade Deletion. Bạn có thể cần chuyên môn hóa các tùy chọn bảng liên quan xung quanh điều này như Tắt CascadeOnDelete :

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

Hoặc Tắt hoàn toàn tính năng này:

modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();

-2

Giải pháp của tôi cho vấn đề này gặp phải khi sử dụng ASP.NET Core 2.0 và EF Core 2.0 là thực hiện theo thứ tự sau:

  1. Chạy update-databaselệnh trong Bảng điều khiển quản lý gói (PMC) để tạo cơ sở dữ liệu (điều này dẫn đến "Giới thiệu ràng buộc FOREIGN KEY ... có thể gây ra chu kỳ hoặc nhiều đường dẫn tầng.")

  2. Chạy script-migration -Idempotentlệnh trong PMC để tạo tập lệnh có thể chạy bất kể các bảng / ràng buộc hiện có

  3. Lấy kịch bản kết quả và tìm ON DELETE CASCADEvà thay thế bằngON DELETE NO ACTION

  4. Thực thi SQL đã sửa đổi đối với cơ sở dữ liệu

Bây giờ, việc di chuyển của bạn phải được cập nhật và việc xóa tầng sẽ không xảy ra.

Quá tệ, tôi không thể tìm thấy bất kỳ cách nào để làm điều này trong Entity Framework Core 2.0.

Chúc may mắn!


Bạn có thể thay đổi tệp di chuyển của mình để làm như vậy (không thay đổi tập lệnh sql), tức là trong tệp di chuyển của bạn, bạn có thể đặt hành động onDelete thành Hạn chế từ Cascade
Rushi Soni 17/03/18

Tốt hơn là chỉ định điều này bằng cách sử dụng các chú thích lưu loát để bạn không phải nhớ làm điều này nếu cuối cùng bạn xóa và tạo lại thư mục di chuyển của mình.
Allen Wang

Theo kinh nghiệm của tôi, các chú thích lưu loát có thể được sử dụng và nên được sử dụng (tôi sử dụng chúng) nhưng chúng thường khá lỗi. Chỉ cần chỉ định chúng trong mã không phải lúc nào cũng hoạt động tạo ra kết quả mong đợi.
user1477388
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.