다형성 연관에 외래 키가없는 이유는 무엇입니까?


81

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
그냥 다른 사람의 명확성을 위해, 영업 이익은 얘기하지 않습니다 foreign_key에 전달 될 수있는 옵션 belongs_to. OP는 네이티브 데이터베이스의 "외래 키 제약"에 대해 이야기하고 있습니다. 그것은 잠시 나를 혼란스럽게했다.
Joshua Pinter 2018 년

답변:


178

외래 키는 하나의 상위 테이블 만 참조해야합니다. 이것은 SQL 구문과 관계 이론의 기본입니다.

다형성 연관은 주어진 열이 둘 이상의 상위 테이블 중 하나를 참조 할 수있는 경우입니다. SQL에서 제약 조건을 선언 할 수있는 방법은 없습니다.

Polymorphic Associations 디자인은 관계형 데이터베이스 디자인의 규칙을 위반합니다. 나는 그것을 사용하지 않는 것이 좋습니다.

몇 가지 대안이 있습니다.

  • 배타적 호 : 각각 하나의 부모를 참조하는 여러 외래 키 열을 만듭니다. 이러한 외래 키 중 정확히 하나가 NULL이 아닐 수 있도록 강제합니다.

  • 관계 반전 : 3 개의 다 대다 테이블을 사용하고 각 테이블은 주석과 각 상위를 참조합니다.

  • 구체적인 수퍼 테이블 : 암시 적 "주석 가능"수퍼 클래스 대신 각 상위 테이블이 참조하는 실제 테이블을 만듭니다. 그런 다음 주석을 해당 수퍼 테이블에 연결하십시오. Pseudo-rails 코드는 다음과 같습니다 (저는 Rails 사용자가 아니므로 리터럴 코드가 아닌 지침으로 취급하십시오).

    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
    

또한 프레젠테이션 에서 SQL의 Practical Object-Oriented Models 및 저서 SQL Antipatterns : Preventing the Pitfalls of Database Programming 에서 다형성 연관성에 대해서도 다룹니다 .


Re your comment : 예, 외래 키가 가리키는 테이블의 이름을 기록하는 또 다른 열이 있다는 것을 알고 있습니다. 이 디자인은 SQL의 외래 키에서 지원되지 않습니다.

예를 들어 댓글을 삽입하고 해당 상위 테이블의 이름으로 "Video"라는 이름을 입력하면 Comment어떻게됩니까? "비디오"라는 테이블이 없습니다. 삽입이 오류와 함께 중단되어야합니까? 어떤 제약이 위반되고 있습니까? RDBMS는이 열이 기존 테이블의 이름을 지정해야한다는 것을 어떻게 알 수 있습니까? 대소 문자를 구분하지 않는 테이블 이름을 어떻게 처리합니까?

마찬가지로 Events테이블 을 삭제 했지만 Comments이벤트를 상위로 표시하는 행 이있는 경우 결과는 무엇입니까? 삭제 테이블을 중단해야합니까? 의 행 Comments이 분리 되어야합니까 ? 같은 다른 기존 테이블을 참조하도록 변경해야합니까 Articles? Events가리키는 데 사용되는 ID 값이 의미가 Articles있습니까?

이러한 딜레마는 모두 다형성 연관이 메타 데이터 (테이블 이름)를 참조하기 위해 데이터 (예 : 문자열 값)를 사용하는 데 의존한다는 사실 때문입니다. 이것은 SQL에서 지원되지 않습니다. 데이터와 메타 데이터는 분리되어 있습니다.


"콘크리트 수퍼 테이블"제안에 대해 머리를 감싸는 데 어려움을 겪고 있습니다.

  • 정의 Commentable레일스 모델 정의에 실제 SQL 테이블로가 아닌 형용사입니다. 다른 열은 필요하지 않습니다.

    CREATE TABLE Commentable (
      id INT AUTO_INCREMENT PRIMARY KEY
    ) TYPE=InnoDB;
    
  • 테이블 Articles, PhotosEvents의 "하위 클래스"를 정의하여 Commentable기본 키도를 참조하는 외래 키가되도록합니다 Commentable.

    CREATE TABLE Articles (
      id INT PRIMARY KEY, -- not auto-increment
      FOREIGN KEY (id) REFERENCES Commentable(id)
    ) TYPE=InnoDB;
    
    -- similar for Photos and Events.
    
  • Comments에 대한 외래 키를 사용 하여 테이블을 정의합니다 Commentable.

    CREATE TABLE Comments (
      id INT PRIMARY KEY AUTO_INCREMENT,
      commentable_id INT NOT NULL,
      FOREIGN KEY (commentable_id) REFERENCES Commentable(id)
    ) TYPE=InnoDB;
    
  • Article(예를 들어) 생성하려는 경우 새 행도 생성해야합니다 Commentable. 그래서 너무에 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(), ... );
    
  • 을 만들려면에 Comment있는 값을 사용하십시오 Commentable.

    INSERT INTO Comments (id, commentable_id, ...)
    VALUES (DEFAULT, 2, ...);
    
  • 주어진의 주석을 쿼리하려면 Photo몇 가지 조인을 수행하십시오.

    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;
    
  • 댓글의 ID 만 있고 댓글에 해당하는 댓글 리소스를 찾고 싶을 때. 이를 위해 Commentable 테이블이 참조하는 리소스를 지정하는 것이 도움이 될 수 있습니다.

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

    그런 다음 commentable_type조인 할 테이블을 찾은 후 각 리소스 테이블 (사진, 기사 등)에서 데이터를 가져 오기 위해 두 번째 쿼리를 실행해야 합니다. SQL에서는 테이블 이름을 명시 적으로 지정해야하므로 동일한 쿼리에서이를 수행 할 수 없습니다. 동일한 쿼리의 데이터 결과로 결정된 테이블에 조인 할 수 없습니다.

물론 이러한 단계 중 일부는 Rails에서 사용하는 규칙을 위반합니다. 그러나 적절한 관계형 데이터베이스 설계와 관련하여 Rails 규칙이 잘못되었습니다.


2
후속 조치 해 주셔서 감사합니다. 우리가 같은 페이지에있는 것처럼 Rails에서 다형성 연관은 외래 키에 대한 Comment에서 두 개의 열을 사용합니다. 한 열에는 대상 행의 ID가 있고 두 번째 열은 Active Record에 키가있는 모델 (Article, Photo 또는 Event)을 알려줍니다. 이것을 알면서도 당신이 제안한 세 가지 대안을 추천 하시겠습니까? "콘크리트 수퍼 테이블"제안에 대해 머리를 감싸는 데 어려움을 겪고 있습니다. "댓글을 해당 수퍼 테이블에 연결"(코멘트 가능)이라고하면 무슨 의미입니까?
eggdrop

1
설명 해주셔서 감사합니다. 적절한 관계형 데이터베이스 설계와 관련하여 Rails 규칙이 잘못되었다고 말하는 이유를 이해한다고 생각합니다. 패턴은 다양한 관계형 제약 조건을 적용하는 능력을 상실한다는 점에서 어떤면에서 플랫 파일을 저장 메커니즘으로 사용하는 것과 유사합니다.
eggdrop

6
바로 그거죠. Polymorphic Associations 문서 자체에서 외래 키 제약 조건을 사용할 수 없다고 말하면 올바른 관계형 데이터베이스 디자인이 아니라는 것은 강력한 "코드 냄새"여야합니다!
Bill Karwin

1
Concrete Supertable 솔루션의 한 가지 단점은 children 테이블에 참조 무결성을 적용하지 않는다는 것입니다. 예를 들어, 이벤트 행과 사진 행이 동일한 commentable_id를 가질 수 있습니다. 물론 좋은 절차를 사용하여 commentable_id를 만들고 자식 테이블에 할당하면 이러한 상황을 피할 수 있지만 가능성은 여전히 ​​존재합니다.
Jason Martens

1
@Mohamad, STI는 잘 작동합니다. 상위 테이블이 STI를 사용한 경우 여전히 외래 키를 정의 할 수 있습니다. 또는 자식 테이블이 STI를 사용하더라도.
Bill Karwin 2013

3

Bill Karwin은 SQL이 실제로 기본 개념 다형성 관계를 갖지 않기 때문에 외래 키를 다형성 관계와 함께 사용할 수 없다는 것이 옳습니다. 그러나 외래 키를 갖는 목적이 참조 무결성을 적용하는 것이라면 트리거를 통해 시뮬레이션 할 수 있습니다. 이것은 DB에 따라 다르지만 다음은 다형성 관계에서 외래 키의 계단식 삭제 동작을 시뮬레이션하기 위해 만든 최근 트리거입니다.

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

내 코드에서 brokerages테이블의 레코드 또는 테이블의 레코드는 agents테이블의 레코드와 관련 될 수 있습니다 subscribers.


이것은 훌륭합니다. 새로 생성 된 다형성 연결이 유효한 유형과 ID를 가리 키도록하는 유사한 트리거를 만드는 방법에 대한 아이디어가 있습니까?
cayblood
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.