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


9

私はテーブル持っている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を使用しています。


1
あるコンテナは、均質な?つまり、今日植物を保持しているコンテナを空にして、明日何も変更せずに動物や細菌を保持することはできますか?
RDFozz 2017年

@RDFozz UIでこれを許可する予定はありませんが、原則として可能です。コンテナを削除して新しいコンテナを作成するのが一般的なアクションです。しかし、コンテナがコンテンツのタイプを変更しても、何も壊れません
Mad Scientist

回答:


10

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

実装

手順は次のとおりです。

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

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

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

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

    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と別の列の任意の組み合わせは、一意性の宣言を追加することなく、同様に一意であることにバインドされます–それで、ポイントは何ですか?重要なのは、列のペアを一意に正式に宣言することで、それらを参照可能にする、つまり、この部分についての外部キー制約のターゲットにすることです。

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

    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_idがへの参照として既に定義されている場合はcontainers、不要になったら各テーブルからその制約を自由に削除してください。

使い方

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

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

最後に、のために別のテーブルを持っているあなた自身の決断plantsanimalsbacteria、と、各エンティティタイプごとに異なるジャンクションテーブルはすでにそれが不可能コンテナは複数のタイプのアイテムを持ってできるようになります。

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


3

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

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

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

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

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

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

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


それはそれを行う方法ですが、多くの欠点があります。これを行うには、コンテナ/プラントのリストを再構成するために3つのインデックススキャンが必要です。外部テーブルに選択を追加することによって挿入を遅くし、整合性をトリガー-時々それは機能しますが、私はそれを望んでいません。また、列が変更されていないことを確認するために更新を遅くします。そうは言っても、私たちはアプリの要求を満たすよりもメンタルブロックに取り組んでいると思いますが、投票からは、私は1人でいる可能性があります。
エヴァンキャロル

1
ここから何が必要かは正確にはわかりません。アプリケーションの大部分がコンテナー自体に焦点を合わせている場合(コンテナーの出荷、追跡、保管施設での配置など)、ほとんどのクエリはコンテナーのコンテンツに焦点を合わせず、コンテナー自体に焦点を当てている可能性があります。既に述べたように、植物のコンテナを動物のコンテナとはまったく異なるエンティティとして扱うことが理にかなっているシナリオは確かにあります。OPは、彼らが直面するシナリオを決定する必要があります。
RDFozz 2017年

3

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

将来、多くのカテゴリー(例えば、属、種、亜種)を区別する必要がある場合、これはおそらく適切ではありません。ただし、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の入力と忍耐(私の回答の読み取りと修正)に感謝します。


1

まず、質問の読み方について@RDFozzに同意します。しかし、彼はステファンの回答にいくつかの懸念を提起しています。

ここに画像の説明を入力してください

彼の懸念に対処するには、

  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つのコンテナに複数のものを含めることができますが、コンテナ内には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には何もありません。


0

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

それは悪い考えです。

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

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

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

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


コンテナーを分割すると、問題がスキーマの別の部分に移動します。それでも、他のテーブルからコンテナーを参照する必要があり、それらの部分でも異なるコンテナータイプを区別する必要があります。
マッドサイエンティスト

彼らは、コンテナを見つけたテーブルだけで、どのようなコンテナの種類を持っているのかを知っているでしょう。植物はの単一のコンテナを参照plant_containersします。植物コンテナだけが必要なものは、plant_containersテーブルからのみ選択してください。コンテナが必要な場合(つまり、すべてのタイプのコンテナを検索する場合)はUNION ALL、コンテナを含む3つのテーブルすべてで実行できます。
エヴァンキャロル
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.