Tại sao bạn không thể có khóa ngoại trong một kết hợp đa hình?


81

Tại sao bạn không thể có khóa ngoại trong một liên kết đa hình, chẳng hạn như khóa được biểu diễn bên dưới dưới dạng mô hình Rails?

class Comment < ActiveRecord::Base
  belongs_to :commentable, :polymorphic => true
end

class Article < ActiveRecord::Base
  has_many :comments, :as => :commentable
end

class Photo < ActiveRecord::Base
  has_many :comments, :as => :commentable
  #...
end

class Event < ActiveRecord::Base
  has_many :comments, :as => :commentable
end

3
Chỉ để làm rõ những người khác, OP không nói về foreign_keytùy chọn có thể được chuyển cho belongs_to. OP đang nói về "Ràng buộc khóa ngoại" của cơ sở dữ liệu gốc. Điều đó khiến tôi bối rối trong một thời gian.
Joshua Pinter

Câu trả lời:


178

Khóa ngoại chỉ được tham chiếu đến một bảng cha. Đây là điều cơ bản cho cả cú pháp SQL và lý thuyết quan hệ.

Kết hợp đa hình là khi một cột nhất định có thể tham chiếu đến một trong hai hoặc nhiều bảng cha. Không có cách nào bạn có thể khai báo ràng buộc đó trong SQL.

Thiết kế Liên kết đa hình phá vỡ các quy tắc của thiết kế cơ sở dữ liệu quan hệ. Tôi không khuyên bạn nên sử dụng nó.

Có một số lựa chọn thay thế:

  • Vòng cung riêng: Tạo nhiều cột khóa ngoại, mỗi cột tham chiếu đến một cột gốc. Thực thi rằng chính xác một trong các khóa ngoại này có thể không phải là NULL.

  • Đảo ngược mối quan hệ: Sử dụng ba bảng nhiều-nhiều, mỗi bảng tham chiếu Nhận xét và một bảng cha tương ứng.

  • Concrete Supertable: Thay vì lớp cha "có thể nhận xét" ngầm, hãy tạo một bảng thực mà mỗi bảng cha của bạn tham chiếu. Sau đó liên kết Bình luận của bạn với bảng xếp hạng cao đó. Mã Pseudo-rails sẽ giống như sau (Tôi không phải là người dùng Rails, vì vậy hãy coi đây như một hướng dẫn, không phải mã theo nghĩa đen):

    class Commentable < ActiveRecord::Base
      has_many :comments
    end
    
    class Comment < ActiveRecord::Base
      belongs_to :commentable
    end
    
    class Article < ActiveRecord::Base
      belongs_to :commentable
    end
    
    class Photo < ActiveRecord::Base
      belongs_to :commentable
    end
    
    class Event < ActiveRecord::Base
      belongs_to :commentable
    end
    

Tôi cũng đề cập đến các liên kết đa hình trong bài thuyết trình của tôi Mô hình hướng đối tượng thực tế trong SQL và cuốn sách của tôi Phản vật chất SQL: Tránh cạm bẫy của lập trình cơ sở dữ liệu .


Nhận xét của bạn: Có, tôi biết rằng có một cột khác ghi tên của bảng mà khóa ngoại được cho là trỏ đến. Thiết kế này không được hỗ trợ bởi các khóa ngoại trong SQL.

Ví dụ: điều gì sẽ xảy ra nếu bạn chèn một Nhận xét và đặt tên "Video" làm tên của bảng mẹ cho điều đó Comment? Không tồn tại bảng có tên "Video". Có nên hủy bỏ chèn khi có lỗi không? Ràng buộc nào đang bị vi phạm? Làm thế nào để RDBMS biết rằng cột này phải đặt tên cho một bảng hiện có? Làm thế nào nó xử lý các tên bảng không phân biệt chữ hoa chữ thường?

Tương tự như vậy, nếu bạn bỏ Eventsbảng, nhưng bạn có các hàng trong Commentsđó biểu thị Sự kiện là cha của chúng, thì kết quả sẽ là gì? Có nên hủy bỏ drop table? Các hàng trong Commentscó nên bỏ trống không? Họ có nên thay đổi để tham chiếu đến một bảng hiện có khác, chẳng hạn như Articles? Các giá trị id được sử dụng để trỏ đến Eventscó ý nghĩa gì khi trỏ tới Articleskhông?

Những tình huống khó xử này đều là do các Hiệp hội đa hình phụ thuộc vào việc sử dụng dữ liệu (tức là giá trị chuỗi) để tham chiếu đến siêu dữ liệu (tên bảng). Điều này không được hỗ trợ bởi SQL. Dữ liệu và siêu dữ liệu là riêng biệt.


Tôi đang gặp khó khăn khi xoay quanh đề xuất "Concrete Supertable" của bạn.

  • Định nghĩa Commentablenhư một bảng SQL thực, không chỉ là một tính từ trong định nghĩa mô hình Rails của bạn. Không có cột nào khác là cần thiết.

    CREATE TABLE Commentable (
      id INT AUTO_INCREMENT PRIMARY KEY
    ) TYPE=InnoDB;
    
  • Xác định các bảng Articles, PhotosEventslà "lớp con" của Commentable, bằng cách làm khóa chính của họ cũng là một tham khảo chính nước ngoài Commentable.

    CREATE TABLE Articles (
      id INT PRIMARY KEY, -- not auto-increment
      FOREIGN KEY (id) REFERENCES Commentable(id)
    ) TYPE=InnoDB;
    
    -- similar for Photos and Events.
    
  • Xác định Commentsbảng với một khóa ngoại để Commentable.

    CREATE TABLE Comments (
      id INT PRIMARY KEY AUTO_INCREMENT,
      commentable_id INT NOT NULL,
      FOREIGN KEY (commentable_id) REFERENCES Commentable(id)
    ) TYPE=InnoDB;
    
  • Khi bạn muốn tạo một Article(ví dụ), bạn cũng phải tạo một hàng mới Commentable. Vì vậy, quá cho PhotosEvents.

    INSERT INTO Commentable (id) VALUES (DEFAULT); -- generate a new id 1
    INSERT INTO Articles (id, ...) VALUES ( LAST_INSERT_ID(), ... );
    
    INSERT INTO Commentable (id) VALUES (DEFAULT); -- generate a new id 2
    INSERT INTO Photos (id, ...) VALUES ( LAST_INSERT_ID(), ... );
    
    INSERT INTO Commentable (id) VALUES (DEFAULT); -- generate a new id 3
    INSERT INTO Events (id, ...) VALUES ( LAST_INSERT_ID(), ... );
    
  • Khi bạn muốn tạo một Comment, hãy sử dụng một giá trị tồn tại trong Commentable.

    INSERT INTO Comments (id, commentable_id, ...)
    VALUES (DEFAULT, 2, ...);
    
  • Khi bạn muốn truy vấn nhận xét của một đối tượng nhất định Photo, hãy thực hiện một số phép nối:

    SELECT * FROM Photos p JOIN Commentable t ON (p.id = t.id)
    LEFT OUTER JOIN Comments c ON (t.id = c.commentable_id)
    WHERE p.id = 2;
    
  • Khi bạn chỉ có id của một bình luận và bạn muốn tìm tài nguyên có thể bình luận đó là bình luận. Đối với điều này, bạn có thể thấy rằng bảng có thể nhận xét hữu ích để chỉ định tài nguyên nào mà nó tham chiếu.

    SELECT commentable_id, commentable_type FROM Commentable t
    JOIN Comments c ON (t.id = c.commentable_id)
    WHERE c.id = 42;
    

    Sau đó, bạn cần chạy một truy vấn thứ hai để lấy dữ liệu từ bảng tài nguyên tương ứng (Ảnh, Bài viết, v.v.), sau khi khám phá ra commentable_typebảng nào để tham gia. Bạn không thể làm điều đó trong cùng một truy vấn, vì SQL yêu cầu các bảng phải được đặt tên rõ ràng; bạn không thể tham gia vào một bảng được xác định bởi kết quả dữ liệu trong cùng một truy vấn.

Phải thừa nhận rằng một số bước này phá vỡ các quy ước được sử dụng bởi Rails. Nhưng các quy ước Rails là sai đối với thiết kế cơ sở dữ liệu quan hệ thích hợp.


2
Cảm ơn đã theo dõi. Vì vậy, chúng ta đang ở trên cùng một trang, trong Rails các kết hợp đa hình sử dụng hai cột trong Nhận xét của chúng ta cho khóa ngoại. Một cột giữ id của hàng mục tiêu và cột thứ hai cho Active Record biết khóa đó nằm trong mô hình nào (Bài viết, Ảnh hoặc Sự kiện). Biết được điều này, bạn vẫn đề xuất ba lựa chọn thay thế mà bạn đã đề xuất chứ? Tôi đang gặp khó khăn khi xoay quanh đề xuất "Concrete Supertable" của bạn. Ý bạn là gì khi bạn nói "link your Comments to that supertable" (Có thể bình luận)?
giọt trứng

1
Cảm ơn bạn đã giải thích. Tôi nghĩ rằng tôi hiểu tại sao bạn nói các quy ước Rails là sai đối với thiết kế cơ sở dữ liệu quan hệ phù hợp - mô hình theo một số cách giống như việc sử dụng tệp phẳng làm cơ chế lưu trữ ở chỗ nó mất khả năng thực thi các ràng buộc quan hệ khác nhau.
giọt trứng

6
Chính xác. Chắc hẳn sẽ có một "mùi mã" mạnh rằng nó không phải là thiết kế cơ sở dữ liệu quan hệ đúng khi bản thân tài liệu về Hiệp hội đa hình nói rằng bạn không thể sử dụng các ràng buộc khóa ngoại!
Bill Karwin

1
Một nhược điểm của giải pháp Concrete Supertable là nó không thực thi tính toàn vẹn tham chiếu trên bảng con. Ví dụ: có thể hàng Sự kiện và hàng Ảnh có cùng commentable_id. Tuy nhiên, việc sử dụng thủ tục tốt để tạo commentable_id và gán nó vào bảng con sẽ tránh được tình huống này, nhưng khả năng vẫn tồn tại.
Jason Martens

1
@Mohamad, STI sẽ hoạt động tốt. Bạn vẫn có thể xác định khóa ngoại nếu bảng mẹ của bạn sử dụng STI. Hoặc ngay cả khi bảng con đã sử dụng STI.
Bill Karwin

3

Bill Karwin đúng rằng không thể sử dụng khóa ngoại với các mối quan hệ đa hình do SQL không thực sự có mối quan hệ đa hình khái niệm gốc. Nhưng nếu mục tiêu của bạn có khóa ngoại là để thực thi tính toàn vẹn của tham chiếu, bạn có thể mô phỏng nó thông qua trình kích hoạt. Điều này được DB cụ thể nhưng dưới đây là một số kích hoạt gần đây tôi đã tạo để mô phỏng hành vi xóa theo tầng của khóa ngoại trên mối quan hệ đa hình:

CREATE FUNCTION delete_related_brokerage_subscribers() RETURNS trigger AS $$
  BEGIN
    DELETE FROM subscribers
    WHERE referrer_type = 'Brokerage' AND referrer_id = OLD.id;
    RETURN NULL;
  END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER cascade_brokerage_subscriber_delete
AFTER DELETE ON brokerages
FOR EACH ROW EXECUTE PROCEDURE delete_related_brokerage_subscribers();


CREATE FUNCTION delete_related_agent_subscribers() RETURNS trigger AS $$
  BEGIN
    DELETE FROM subscribers
    WHERE referrer_type = 'Agent' AND referrer_id = OLD.id;
    RETURN NULL;
  END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER cascade_agent_subscriber_delete
AFTER DELETE ON agents
FOR EACH ROW EXECUTE PROCEDURE delete_related_agent_subscribers();

Trong mã của tôi, một bản ghi trong brokeragesbảng hoặc một bản ghi trong agentsbảng có thể liên quan đến một bản ghi trong subscribersbảng.


Điều đó thật tuyệt. Bất kỳ ý tưởng nào về cách người ta có thể tạo một trình kích hoạt tương tự để đảm bảo rằng các liên kết đa hình mới được tạo trỏ đến một loại và ID hợp lệ?
cayblood
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.