web-dev-qa-db-ja.com

相互に排他的な多対多の関係

テーブルcontainersがあり、複数のテーブルとの多対多の関係を設定できます。たとえば、plantsanimalsbacteriaとしましょう。各容器は、任意の数の植物、動物、または細菌を含むことができ、各植物、動物、または細菌は、任意の数の容器に入れることができる。

これまでのところ、これは非常に簡単ですが、私が問題を抱えているのは、各コンテナには同じタイプの要素のみを含める必要があるということです。たとえば、植物と動物の両方が含まれていると、データベースで制約違反になります。

このための私の元のスキーマは次のとおりでした:

containers
----------
id
...
...


containers_plants
-----------------
container_id
plant_id


containers_animals
------------------
container_id
animal_id


containers_bacteria
-------------------
container_id
bacterium_id

しかし、このスキーマでは、コンテナーを均一にする必要があるという制約を実装する方法を思いつきません。

これを参照整合性で実装し、データベースレベルでコンテナが同種であることを保証する方法はありますか?

これにはPostgres 9.6を使用しています。

8
Mad Scientist

冗長性を導入することに同意する場合は、現在の設定をあまり変更せずに、これを宣言的に実装する方法があります。以下の内容は RDFozzの提案 の発展と見なすことができますが、彼の回答を読む前に、その考えは完全に私の頭の中で形成されました(とにかく、独自の回答投稿を正当化するのに十分なほど異なります)。

実装

ここでは、ステップバイステップであなたが何をすべきかです:

  1. RDFozzの回答で提案されているテーブルの行に沿ってcontainerTypesテーブルを作成します。

    CREATE TABLE containerTypes
    (
      id int PRIMARY KEY,
      description varchar(30)
    );
    

    タイプごとに事前定義されたIDを入力します。この回答の目的のために、RDFozzの例と一致させてください。1は植物、2は動物、3は細菌です。

  2. containerType_id列をcontainersに追加し、null不可および外部キーにします。

    ALTER TABLE containers
    ADD containerType_id int NOT NULL
      REFERENCES containerTypes (id);
    
  3. id列がすでにcontainersの主キーであると想定して、(id, containerType_id)に一意の制約を作成します。

    ALTER TABLE containers
    ADD CONSTRAINT UQ_containers_id_containerTypeId
      UNIQUE (id, containerType_id);
    

    これが冗長性の始まりです。 idが主キーであると宣言されている場合は、一意であることを確認できます。一意である場合、idと別の列の任意の組み合わせも一意性の宣言なしで一意にバインドされます。つまり、ポイントは何ですか?重要なのは、列のペアを一意に正式に宣言することにより、それらをreferableにする、つまり、これが外部キー制約のターゲットになることです。一部についてです。

  4. 各junctionテーブル(containerType_idcontainers_animalscontainers_plants)にcontainers_bacteria列を追加します。外部キーにすることは完全にオプションです。説明に従って、列がすべての行で同じ値を持ち、テーブルごとに異なることを確認することが重要です。1はcontainers_plants、2はcontainers_animals、3はcontainers_bacteriaです。 containerTypesにあります。どちらの場合も、その値をデフォルトにして、挿入ステートメントを簡略化することもできます。

    ALTER TABLE containers_plants
    ADD containerType_id NOT NULL
      DEFAULT (1)
      CHECK (containerType_id = 1);
    
    ALTER TABLE containers_animals
    ADD containerType_id NOT NULL
      DEFAULT (2)
      CHECK (containerType_id = 2);
    
    ALTER TABLE containers_bacteria
    ADD containerType_id NOT NULL
      DEFAULT (3)
      CHECK (containerType_id = 3);
    
  5. 各ジャンクションテーブルで、列のペア(container_id, containerType_id)containersを参照する外部キー制約にします。

    ALTER TABLE containers_plants
    ADD CONSTRAINT FK_containersPlants_containers
      FOREIGN KEY (container_id, containerType_id)
      REFERENCES containers (id, containerType_id);
    
    ALTER TABLE containers_animals
    ADD CONSTRAINT FK_containersAnimals_containers
      FOREIGN KEY (container_id, containerType_id)
      REFERENCES containers (id, containerType_id);
    
    ALTER TABLE containers_bacteria
    ADD CONSTRAINT FK_containersBacteria_containers
      FOREIGN KEY (container_id, containerType_id)
      REFERENCES containers (id, containerType_id);
    

    container_idcontainersへの参照として既に定義されている場合は、不要になったので、各テーブルからその制約を自由に削除してください。

使い方

コンテナータイプの列を追加し、それを外部キー制約に参加させることで、コンテナータイプが変更されないようにするメカニズムを準備します。 containersタイプのタイプを変更できるのは、外部キーがDEFERRABLE句で定義されている場合のみです。外部キーは、この実装では想定されていません。

遅延可能であったとしても、containers—ジャンクションテーブルの関係の反対側にあるチェック制約のため、タイプを変更することはまだ不可能です。各ジャンクションテーブルでは、特定のコンテナタイプを1つだけ使用できます。 既存の参照がタイプを変更するのを防ぐだけでなく、additionも防ぐ間違った型参照。つまり、タイプ2(動物)のコンテナがある場合、タイプ2が許可されているテーブル(containers_animals)を使用して項目を追加するだけで、それを参照する行を追加できません。たとえば、containers_bacteriaはタイプ3のコンテナのみを受け入れます。

最後に、plantsanimals、およびbacteriaの異なるテーブル、およびエンティティタイプごとに異なるジャンクションテーブルを作成するという独自の決定により、コンテナが複数のタイプのアイテム。

したがって、これらすべての要素を組み合わせることにより、純粋に宣言的な方法で、すべてのコンテナが均質になることが保証されます。

9
Andriy M

2つまたは3つのカテゴリ(植物/後生動物/バクテリア)だけが必要で、XOR関係をモデル化する場合は、おそらく「弧」がソリューションです。利点:トリガーが不要。図の例は[こちら] [1]にあります。この状況では、「コンテナ」テーブルには3つの列がCHECK制約付きであり、植物、動物、または細菌のいずれかを許可します。

将来、多くのカテゴリー(例えば、属、種、亜種)を区別する必要がある場合、これはおそらく適切ではありません。ただし、2〜3つのグループ/カテゴリの場合、これでうまくいく場合があります。

更新:寄稿者の提案とコメントに触発され、多くの分類群(関連する生物のグループ、生物学者によって分類)を許可し、「特定の」テーブル名を回避する別のソリューション(PostgreSQL 9.5)。

DDLコード:

-- containers: may have more columns eg for temperature, humidity etc
create table containers ( 
  ctr_name varchar(64) unique
);

-- taxonomy - have as many taxa as needed (not just plants/animals/bacteria)
create table taxa ( 
  t_name varchar(64) unique
);

create table organisms (
  o_id integer primary key
, o_name varchar(64)
, t_name varchar(64) references taxa(t_name)
, unique (o_id, t_name) 
);

-- table for mapping containers to organisms and (their) taxon, 
-- each container contains organisms of one and the same taxon
create table collection ( 
  ctr_name varchar(64) references containers(ctr_name)
, o_id integer 
, t_name varchar(64) 
, unique (ctr_name, o_id)
);

--  exclude : taxa that are different from those already in a container
alter table collection
add exclude using Gist (ctr_name with =, t_name with <>);

--  FK : is the o_id <-> t_name (organism-taxon) mapping correct?
alter table collection
add constraint taxon_fkey
foreign key (o_id, t_name) references organisms (o_id, t_name) ;

テストデータ:

insert into containers values ('container_a'),('container_b'),('container_c');
insert into taxa values('t:plant'),('t:animal'),('t:bacterium');
insert into organisms values 
(1, 'p1', 't:plant'),(2, 'p2', 't:plant'),(3, 'p3', 't:plant'),
(11, 'a1', 't:animal'),(22, 'a1', 't:animal'),(33, 'a1', 't:animal'),
(111, 'b1', 't:bacterium'),(222, 'b1', 't:bacterium'),(333, 'b1', 't:bacterium');

テスト:

-- several plants can be in one and the same container (3 inserts succeed)
insert into collection values ('container_a', 1, 't:plant');
insert into collection values ('container_a', 2, 't:plant');
insert into collection values ('container_a', 3, 't:plant');
-- 3 inserts that fail:
-- organism id in a container must be UNIQUE
insert into collection values ('container_a', 1, 't:plant');
-- bacteria not allowed in container_a, populated by plants (EXCLUSION at work)
insert into collection values ('container_a', 333, 't:bacterium');
-- organism with id 333 is NOT a plant -> insert prevented by FK
insert into collection values ('container_a', 333, 't:plant');

@ RDFozz、@ Evan Carroll、および@ypercubeの入力と忍耐(私の回答の読み取りと修正)に感謝します。

3
stefan

1つのオプションは、Containerテーブルにcontainertype_idを追加することです。カラムをNOT NULLにし、ContainerTypeテーブルへの外部キーを作成します。このテーブルには、コンテナに入れることができるアイテムのタイプごとにエントリがあります。

containertype_id |   type
-----------------+-----------
        1        | plant
        2        | animal
        3        | bacteria

コンテナタイプを変更できないようにするには、containertype_idが更新されたかどうかを確認し、その場合は変更をロールバックする更新トリガーを作成します。

次に、コンテナーリンクテーブルの挿入トリガーと更新トリガーで、containertype_idをそのテーブルのエンティティのタイプと照合し、それらが一致することを確認します。

コンテナに入れたものがタイプと一致する必要があり、タイプを変更できない場合、コンテナ内のすべてが同じタイプになります。

注:リンクテーブルのトリガーが一致するものを決定するものであるため、植物や動物を入れることができるタイプのコンテナーが必要な場合は、そのタイプを作成してコンテナーに割り当て、それを確認できます。そのため、ある時点で状況が変わっても柔軟性が維持されます(たとえば、「雑誌」や「本」などのタイプが得られます)。

2番目に注意:コンテナの内容に関係なく、コンテナで発生することのほとんどが同じである場合、これは理にかなっています。コンテナーの内容に基づいて(物理的な現実ではなくシステムで)非常に異なることが発生する場合、コンテナーの種類ごとに個別のテーブルを作成するというEvan Carrollのアイデアは、完全に理にかなっています。このソリューションは、コンテナーが作成時に異なるタイプを持つことを確立しますが、それらを同じテーブルに保持します。コンテナーに対してアクションを実行するたびにタイプを確認する必要があり、実行するアクションがタイプに依存している場合、個別のテーブルが実際に速くて簡単になる場合があります。

2
RDFozz

最初に、質問の読み方について@RDFozzに同意します。しかし、彼は stefans の回答にいくつかの懸念を提起しています。

enter image description here

彼の懸念に対処するために、

  1. PRIMARY KEYを削除します
  2. UNIQUE制約を追加して、重複したエントリから保護します。
  3. EXCLUSION制約を追加して、コンテナーが「同種」であることを確認します
  4. 適切なパフォーマンスを確保するには、c_idにインデックスを追加します。
  5. これを行う人を殺し、正気のために 私の他の答えを指すようにします。

こんな感じです

CREATE TABLE container ( 
  c_id int NOT NULL,
  p_id int,
  b_id int,
  a_id int,
  UNIQUE (c_id,p_id),
  UNIQUE (c_id,b_id),
  UNIQUE (c_id,a_id),
  EXCLUDE USING Gist(c_id WITH =, (CASE WHEN p_id>0 THEN 1 ELSE 0 END) WITH <>),
  EXCLUDE USING Gist(c_id WITH =, (CASE WHEN b_id>0 THEN 1 ELSE 0 END) WITH <>),
  EXCLUDE USING Gist(c_id WITH =, (CASE WHEN a_id>0 THEN 1 ELSE 0 END) WITH <>),
  CHECK (
    ( p_id IS NOT NULL and b_id IS NULL and a_id IS NULL ) 
    OR ( p_id IS NULL and b_id IS NOT NULL and a_id IS NULL ) 
    OR ( p_id IS NULL and b_id IS NULL and a_id IS NOT NULL ) 
  )
);
CREATE INDEX ON container (c_id);

これで、複数のものを持つ1つのコンテナーを持つことができますが、コンテナー内にあるのはtypeの1つだけです。

# INSERT INTO container (c_id,p_id,b_id) VALUES (1,1,null);
INSERT 0 1
# INSERT INTO container (c_id,p_id,b_id) VALUES (1,null,2);
ERROR:  conflicting key value violates exclusion constraint "container_c_id_case_excl"
DETAIL:  Key (c_id, (
CASE
    WHEN p_id > 0 THEN 1
    ELSE 0
END))=(1, 0) conflicts with existing key (c_id, (
CASE
    WHEN p_id > 0 THEN 1
    ELSE 0
END))=(1, 1).

そして、それはすべてGistインデックスに実装されています。

ギザの大ピラミッドはPostgreSQLには何もありません。

1
Evan Carroll

私はいくつかのテーブルと多対多の関係を持つことができるテーブルコンテナを持っています。たとえば、それらは植物、動物、細菌です。

それは悪い考えです。

しかし、このスキーマでは、コンテナーを均一にする必要があるという制約を実装する方法を思いつきません。

そして今、あなたはその理由を知っています。 =)

オブジェクト指向プログラミング(OO)からの継承という考えにとらわれていると思います。 OO継承はコードの再利用に関する問題を解決します。SQLでは、冗長なコードが問題の最小です。整合性が何よりも重要です。パフォーマンスが2番目です。最初の2つは苦痛を味わいます。コストを削減できる「コンパイル時間」はありません。

したがって、コードの再利用へのこだわりを忘れてください。植物、動物、バクテリア用の容器は、現実世界のあらゆる場所で根本的に異なります。 「保持」のコード再利用コンポーネントは、あなたのためにそれをしません。それらを分解します。整合性とパフォーマンスが向上するだけでなく、将来的にはスキーマを拡張しやすくなります。結局のところ、スキーマでは既に含まれているアイテム(植物、動物など)を分解する必要がありました。 、少なくともコンテナを分解する必要がある可能性があります。その場合、スキーマ全体を再設計する必要はありません。

0
Evan Carroll