web-dev-qa-db-ja.com

SQLの総参加制約による多対多の関係の実装

次のエンティティリレーションシップ図に示されているシナリオをSQLで実装するにはどうすればよいですか?

Many-to-many relationship with total participation constraints

示されているように、すべてのAエンティティタイプの出現は少なくとも1つBの対応物(二重の接続線で示される)に関連している必要があり、その逆。次の3つのテーブルを作成する必要があることを知っています。

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

しかし、総参加制約の実装についてはどうですか(つまり、enforcingそのeachAまたはBのいずれかのインスタンスが関与しています最低1つ他との関係の発生)?

17
John

SQLで行うのは簡単ではありませんが、不可能ではありません。これをDDLのみで実施したい場合、DBMSはDEFERRABLE制約を実装している必要があります。これを行うことができます(Postgresで動作するようにチェックでき、それらを実装しています):

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

ここまでが「通常の」デザインで、すべてのAはゼロ、1つまたは複数のBに関連付けることができ、すべてのBはゼロ、1つまたは多数に関連付けることができますA

「合計参加」制限には、逆の順序での制約が必要です(AおよびBから、Rを参照)。反対方向(XからYおよびYからX)に_FOREIGN KEY_制約があると、円(「鶏と卵」の問題)が形成され、少なくともDEFERRABLE。この場合、2つの円(_A -> R -> A_および_B -> R -> B_)があるため、2つの遅延可能な制約が必要です。

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

次に、データを挿入できることをテストできます。 _INITIALLY DEFERRED_は必要ないことに注意してください。制約を_DEFERRABLE INITIALLY IMMEDIATE_として定義することもできますが、トランザクション中にそれらを延期するには_SET CONSTRAINTS_ステートメントを使用する必要があります。ただし、いずれの場合も、1つのトランザクションでテーブルに挿入する必要があります。

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

SQLfiddleでテストされています。


DBMSにDEFERRABLE制約がない場合、1つの回避策はA (bid)およびB (aid)列をNULLとして定義することです。 INSERTプロシージャ/ステートメントは、最初にABに挿入する必要があります(bidaidにそれぞれnullを入れる)、次にRに挿入し、次に上記のnull値をRからの関連する非null値に更新します。

このアプローチでは、DBMSはDDLだけで要件を強制しませんが、すべてのINSERT(およびUPDATEおよびDELETEおよびMERGE)プロシージャを考慮する必要がありますそれに応じて調整され、ユーザーはそれらのみを使用するように制限され、テーブルへの直接書き込みアクセス権を持たない必要があります。

_FOREIGN KEY_制約に円があることは、多くのベストプラクティスでは考慮されていません。正当な理由により、複雑さもその1つです。たとえば、2番目のアプローチ(null許容列)では、DBMSによっては、行の更新と削除を追加のコードで実行する必要があります。たとえばSQL Serverでは、FKサークルが存在する場合、連鎖的な更新と削除は許可されないため、単に_ON DELETE CASCADE_を置くことはできません。

この関連する質問の回答もお読みください:
特権を持つ子と1対多の関係を持つ方法は?


別の3番目のアプローチ(上記の質問の私の回答を参照)は、循環FKを完全に削除することです。したがって、コードの最初の部分(テーブルABR、およびRからAおよびBへの外部キーのみ)をほとんどそのままにしておく(実際にはコードを単純化する) 、A用の別のテーブルを追加して、Bからの「1つ必要」関連アイテムを格納します。したがって、A (bid)列はA_one (bid)に移動します。Bから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
 );
_

1番目と2番目のアプローチの違いは、循環FKがないため、更新と削除のカスケードが正常に機能することです。 「全体の参加」の実施は、2番目のアプローチのようにDDLのみによるものではなく、適切な手順(_INSERT/UPDATE/DELETE/MERGE_)によって実行する必要があります。 2番目のアプローチとの小さな違いは、すべての列をnull可能ではなく定義できることです。


別の4番目のアプローチ(@ Aaron Bertrandの回答を参照)はfiltered/partial一意のインデックス(DBMSで使用可能な場合)(この場合、Rテーブルに2つ必要です)。これは、2つの追加のテーブルが必要ないことを除いて、3番目のアプローチと非常に似ています。 「合計参加」制約は、コードによって適用される必要があります。

16
ypercubeᵀᴹ

直接はできません。まず、Bが存在しない状態でAのレコードを挿入することはできませんが、Aレコードがない場合はBレコードを作成できません。トリガーなどを使用して強制する方法はいくつかあります。すべての挿入を確認し、少なくとも1つの対応するレコードがABリンクテーブルに残っていることを削除する必要があります。

3
Cylindric