Thực hiện mối quan hệ nhiều-nhiều với các ràng buộc tham gia trong SQL


17

Tôi nên triển khai SQL như thế nào trong kịch bản được mô tả trong sơ đồ Mối quan hệ thực thể sau đây?

Mối quan hệ nhiều-nhiều với tổng hạn chế tham gia

Như được chỉ ra, mọi Asự xuất hiện của loại thực thể phải liên quan đến ít nhất một B đối tác (được biểu thị bằng các đường kết nối kép) và ngược lại . Tôi biết tôi nên tạo ba bảng theo sau:

    CREATE TABLE A
    (
        a INT NOT NULL,
        CONSTRAINT A_PK PRIMARY KEY (a)
    );

    CREATE TABLE B
    (
        b INT NOT NULL,
        CONSTRAINT B_PK PRIMARY KEY (b)
    );

    CREATE TABLE R
    (
        a INT NOT NULL,
        b INT NOT NULL,
        CONSTRAINT R_PK      PRIMARY KEY (a, b),
        CONSTRAINT R_to_A_FK FOREIGN KEY (a)
            REFERENCES A (a),
        CONSTRAINT R_to_B_FK FOREIGN KEY (b)
            REFERENCES B (b)
    );

Nhưng, điều gì về việc thực hiện các ràng buộc tham gia tổng thể (nghĩa là thực thi rằng mỗi trường hợp của một trong hai Ahoặc Bcó liên quan đến tối thiểu một lần xảy ra mối quan hệ với nhau)?

Câu trả lời:


16

Nó không dễ thực hiện trong SQL nhưng nó không phải là không thể. Nếu bạn muốn điều này được thi hành thông qua DDL một mình, DBMS phải thực hiện các DEFERRABLEràng buộc. Điều này có thể được thực hiện (và có thể được kiểm tra để hoạt động trong Postgres, đã triển khai chúng):

-- lets create first the 2 tables, A and B:
CREATE TABLE a 
( aid INT NOT NULL,
  bid INT NOT NULL,
  CONSTRAINT a_pk PRIMARY KEY (aid) 
 );

CREATE TABLE b 
( bid INT NOT NULL,
  aid INT NOT NULL,
  CONSTRAINT b_pk PRIMARY KEY (bid) 
 );

-- then table R:
CREATE TABLE r 
( aid INT NOT NULL,
  bid INT NOT NULL,
  CONSTRAINT r_pk PRIMARY KEY (aid, bid),
  CONSTRAINT a_r_fk FOREIGN KEY (aid) REFERENCES a,  
  CONSTRAINT b_r_fk FOREIGN KEY (bid) REFERENCES b
 );

Cho đến đây là thiết kế "bình thường", trong đó mọi thứ Acó thể liên quan đến số không, một hoặc nhiều Bvà mọi thứ Bcó thể liên quan đến số không, một hoặc nhiều A.

Hạn chế "tổng tham gia" cần các ràng buộc theo thứ tự ngược lại (từ ABtương ứng, tham chiếu R). Có các FOREIGN KEYràng buộc theo hướng ngược lại (từ X đến Y và từ Y đến X) đang tạo thành một vòng tròn (một vấn đề "gà và trứng") và đó là lý do tại sao chúng ta cần ít nhất một trong số chúng DEFERRABLE. Trong trường hợp này, chúng tôi có hai vòng tròn ( A -> R -> AB -> R -> Bvì vậy chúng tôi cần hai ràng buộc có thể bảo vệ:

-- then we add the 2 constraints that enforce the "total participation":
ALTER TABLE a
  ADD CONSTRAINT r_a_fk FOREIGN KEY (aid, bid) REFERENCES r 
    DEFERRABLE INITIALLY DEFERRED ;

ALTER TABLE b
  ADD CONSTRAINT r_b_fk FOREIGN KEY (aid, bid) REFERENCES r 
    DEFERRABLE INITIALLY DEFERRED ;

Sau đó chúng tôi có thể kiểm tra rằng chúng tôi có thể chèn dữ liệu. Lưu ý rằng INITIALLY DEFERREDkhông cần thiết. Chúng tôi có thể đã xác định các ràng buộc là DEFERRABLE INITIALLY IMMEDIATEnhưng sau đó chúng tôi phải sử dụng SET CONSTRAINTScâu lệnh để trì hoãn chúng trong quá trình giao dịch. Trong mọi trường hợp, chúng ta cần phải chèn vào các bảng trong một giao dịch:

-- insert data 
BEGIN TRANSACTION ;
    INSERT INTO a (aid, bid)
    VALUES
      (1, 1),    (2, 5),
      (3, 7),    (4, 1) ;

    INSERT INTO b (aid, bid)
    VALUES
      (1, 1),    (1, 2),
      (2, 3),    (2, 4),
      (2, 5),    (3, 6),
      (3, 7) ;

    INSERT INTO r (aid, bid)
    VALUES
      (1, 1),    (1, 2),
      (2, 3),    (2, 4),
      (2, 5),    (3, 6),
      (3, 7),    (4, 1),
      (4, 2),    (4, 7) ; 
 END ;

Đã thử nghiệm tại SQLfiddle .


Nếu DBMS không có các DEFERRABLEràng buộc, một cách giải quyết là xác định các cột A (bid)B (aid)cột là NULL. Các INSERTthủ tục / câu lệnh sau đó sẽ phải chèn vào trước AB(đặt null vào bidaidtương ứng), sau đó chèn vào Rvà sau đó cập nhật các giá trị null ở trên thành các giá trị null không liên quan từ đó R.

Với phương pháp này, các DBMS không thi hành các yêu cầu của DDL một mình nhưng mỗi INSERT(và UPDATEDELETEMERGE) thủ tục phải được xem xét và điều chỉnh cho phù hợp và người sử dụng phải được giới hạn chỉ sử dụng chúng và không phải trực tiếp ghi vào các bảng.

Có các vòng tròn trong các FOREIGN KEYràng buộc không được xem xét bởi nhiều thực tiễn tốt nhất và vì lý do chính đáng, sự phức tạp là một trong số đó. Ví dụ, với cách tiếp cận thứ hai (với các cột không thể), việc cập nhật và xóa các hàng sẽ vẫn phải được thực hiện với mã bổ sung, tùy thuộc vào DBMS. Ví dụ, trong SQL Server, bạn không thể đặt ON DELETE CASCADEvì các bản cập nhật xếp tầng và xóa không được phép khi có vòng tròn FK.

Xin vui lòng đọc các câu trả lời tại câu hỏi liên quan này:
Làm thế nào để có mối quan hệ một-nhiều với một đứa trẻ đặc quyền?


Một cách tiếp cận thứ 3 khác (xem câu trả lời của tôi trong câu hỏi đã đề cập ở trên) là loại bỏ hoàn toàn các FK tròn. Vì vậy, giữ cho phần đầu tiên của mã (với các bảng A, B, Rvà các phím nước ngoài chỉ từ R đến A và B) gần như nguyên vẹn (trên thực tế đơn giản hóa nó), chúng tôi thêm một bảng cho Ađể lưu trữ các "phải có một" tài liệu liên quan từ B. Vì vậy, A (bid)cột di chuyển đến A_one (bid)Tương tự được thực hiện cho mối quan hệ ngược từ B sang A:

CREATE TABLE a 
( aid INT NOT NULL,
  CONSTRAINT a_pk PRIMARY KEY (aid) 
 );

CREATE TABLE b 
( bid INT NOT NULL,
  CONSTRAINT b_pk PRIMARY KEY (bid) 
 );

-- then table R:
CREATE TABLE r 
( aid INT NOT NULL,
  bid INT NOT NULL,
  CONSTRAINT r_pk PRIMARY KEY (aid, bid),
  CONSTRAINT a_r_fk FOREIGN KEY (aid) REFERENCES a,  
  CONSTRAINT b_r_fk FOREIGN KEY (bid) REFERENCES b
 );

CREATE TABLE a_one 
( aid INT NOT NULL,
  bid INT NOT NULL,
  CONSTRAINT a_one_pk PRIMARY KEY (aid),
  CONSTRAINT r_a_fk FOREIGN KEY (aid, bid) REFERENCES r
 );

CREATE TABLE b_one
( bid INT NOT NULL,
  aid INT NOT NULL,
  CONSTRAINT b_one_pk PRIMARY KEY (bid),
  CONSTRAINT r_b_fk FOREIGN KEY (aid, bid) REFERENCES r
 );

Sự khác biệt so với cách tiếp cận thứ 1 và thứ 2 là không có FK tròn, do đó các cập nhật xếp tầng và xóa sẽ hoạt động tốt. Việc thực thi "tổng tham gia" không chỉ bằng DDL, như trong cách tiếp cận thứ 2, và phải được thực hiện bằng các thủ tục thích hợp ( INSERT/UPDATE/DELETE/MERGE). Một sự khác biệt nhỏ với cách tiếp cận thứ 2 là tất cả các cột có thể được xác định không phải là null.


Một cách tiếp cận thứ 4 khác (xem câu trả lời của @Aaron Bertrand trong câu hỏi đã đề cập ở trên) là sử dụng các chỉ mục duy nhất được lọc / một phần , nếu chúng có sẵn trong DBMS của bạn ( Rtrong trường hợp này, bạn cần hai trong số chúng, trong trường hợp này). Điều này rất giống với cách tiếp cận thứ 3, ngoại trừ việc bạn sẽ không cần thêm 2 bảng. Ràng buộc "tổng tham gia" vẫn phải được áp dụng theo mã.


Cách tiếp cận thứ 4 (một chút ẩn đi) thực sự hoàn hảo. Ví dụ, xem postgresql.org/docs/9.6/static/indexes-partial.html Ví dụ 11-3 cho postgres.
Danilo

@Danilo Tôi thấy nó hoàn hảo đến mức nào để đảm bảo có tối đa 1 tổng số người tham gia (dựa trên một số lĩnh vực bổ sung - thành công trong ví dụ về hậu kỳ). Tôi không thể thấy nó hữu ích như thế nào để đảm bảo rằng có ít nhất một thành công - câu hỏi thực tế trong chủ đề này. Bạn có thể vui lòng giải thích?
Alexander Mihailov

3

Bạn không thể trực tiếp. Để bắt đầu, bạn sẽ không thể chèn bản ghi cho A mà không có B, nhưng bạn không thể tạo bản ghi B nếu không có bản ghi A cho bản ghi đó. Có một số cách để thực thi nó bằng cách sử dụng những thứ như kích hoạt - bạn sẽ phải kiểm tra trên mỗi lần chèn và xóa rằng ít nhất một bản ghi tương ứng vẫn còn trong bảng liên kết AB.

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.