Hubungan banyak-ke-banyak yang saling eksklusif

9

Saya punya tabel containersyang dapat memiliki hubungan banyak-ke-banyak ke beberapa tabel, katakanlah itu plants, animalsdan bacteria. Setiap wadah dapat berisi tanaman, hewan, atau bakteri dalam jumlah yang sewenang-wenang, dan setiap tanaman, hewan, atau bakteri dapat dalam jumlah yang sewenang-wenang.

Sejauh ini ini sangat mudah, tetapi bagian saya memiliki masalah adalah bahwa setiap wadah hanya boleh mengandung unsur-unsur dari jenis yang sama. Wadah campuran yang mis. Berisi tumbuhan dan hewan harus menjadi pelanggaran batasan dalam basis data.

Skema asli saya untuk ini adalah sebagai berikut:

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


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


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


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

Tetapi dengan skema ini, saya tidak dapat menemukan cara mengimplementasikan batasan bahwa kontainer harus homogen.

Apakah ada cara untuk menerapkan ini dengan integritas referensial dan memastikan pada tingkat basis data bahwa wadahnya homogen?

Saya menggunakan Postgres 9.6 untuk ini.

Ilmuwan gila
sumber
1
Apakah wadahnya homogen? Dengan kata lain, dapatkah wadah yang menampung tanaman hari ini dikosongkan dan, tanpa perubahan apa pun, menampung hewan atau bakteri besok?
RDFozz
@RDFozz Saya tidak punya rencana untuk mengizinkan itu di UI, tetapi pada prinsipnya itu akan mungkin. Tidak masuk akal untuk melakukan itu, menghapus wadah dan membuat yang baru akan menjadi tindakan khas. Tetapi jika sebuah wadah mengubah jenis konten, itu tidak akan merusak apa pun
Mad Scientist

Jawaban:

10

Ada cara untuk mengimplementasikan ini secara deklaratif hanya tanpa mengubah pengaturan Anda saat ini, jika Anda setuju untuk memperkenalkan beberapa redundansi padanya. Berikut ini dapat dianggap sebagai pengembangan atas saran RDFozz , meskipun ide tersebut sepenuhnya terbentuk dalam pikiran saya sebelum saya membaca jawabannya (dan itu cukup berbeda untuk menjamin posting jawabannya sendiri).

Penerapan

Inilah yang Anda lakukan, langkah demi langkah:

  1. Buat containerTypestabel di sepanjang garis yang disarankan dalam jawaban RDFozz:

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

    Isi itu dengan ID yang telah ditentukan untuk setiap jenis. Untuk tujuan jawaban ini, biarkan mereka cocok dengan contoh RDFozz: 1 untuk tanaman, 2 untuk hewan, 3 untuk bakteri.

  2. Tambahkan containerType_idkolom ke containersdan buat itu tidak bisa dibatalkan dan kunci asing.

    ALTER TABLE containers
    ADD containerType_id int NOT NULL
      REFERENCES containerTypes (id);
  3. Dengan asumsi idkolom sudah menjadi kunci utama containers, buat batasan unik (id, containerType_id).

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

    Di sinilah redudansi dimulai. Jika iddinyatakan sebagai kunci utama, kami yakin itu unik. Jika unik, kombinasi dari iddan kolom lain pasti unik juga tanpa deklarasi keunikan tambahan - jadi, apa gunanya? Intinya adalah bahwa dengan secara resmi mendeklarasikan pasangan kolom yang unik, kita membiarkannya dapat dirujuk , yaitu menjadi target dari batasan kunci asing, yang merupakan bagian dari bagian ini.

  4. Tambahkan containerType_idkolom untuk masing-masing tabel junction ( containers_animals, containers_plants, containers_bacteria). Menjadikannya kunci asing sepenuhnya opsional. Yang penting adalah memastikan kolom memiliki nilai yang sama untuk semua baris, berbeda untuk setiap tabel: 1 untuk containers_plants, 2 untuk containers_animals, 3 untuk containers_bacteria, sesuai dengan deskripsi di containerTypes. Dalam setiap kasus, Anda juga dapat menjadikan nilai itu sebagai default untuk menyederhanakan pernyataan penyisipan Anda:

    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. Di setiap tabel persimpangan, buat pasangan kolom (container_id, containerType_id)referensi batasan kunci asing 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);

    Jika container_idsudah didefinisikan sebagai referensi containers, jangan ragu untuk menghapus batasan itu dari setiap tabel karena tidak diperlukan lagi.

Bagaimana itu bekerja

Dengan menambahkan kolom jenis kontainer dan membuatnya berpartisipasi dalam batasan kunci asing, Anda menyiapkan mekanisme yang mencegah perubahan jenis kontainer. Mengubah jenis dalam containerstipe hanya dimungkinkan jika kunci asing didefinisikan dengan DEFERRABLEklausa, yang seharusnya tidak ada dalam implementasi ini.

Bahkan jika mereka ditangguhkan, mengubah tipe akan tetap tidak mungkin karena kendala cek di sisi lain containershubungan tabel-persimpangan. Setiap tabel persimpangan hanya memungkinkan satu jenis wadah tertentu. Itu tidak hanya mencegah referensi yang ada mengubah tipe tetapi juga mencegah penambahan referensi tipe yang salah. Artinya, jika Anda memiliki wadah tipe 2 (hewan), Anda hanya dapat menambahkan item ke dalamnya menggunakan tabel di mana tipe 2 diizinkan, yang mana containers_animals, dan tidak akan dapat menambahkan baris yang merujuknya, misalnya containers_bacteria, yang menerima hanya ketik 3 kontainer.

Akhirnya, keputusan Anda sendiri untuk memiliki tabel yang berbeda untuk plants,, animalsdan bacteria, dan tabel persimpangan yang berbeda untuk setiap jenis entitas, sudah membuat wadah tidak mungkin memiliki item lebih dari satu jenis.

Jadi, semua faktor ini digabungkan memastikan, dengan cara deklaratif murni, bahwa semua wadah Anda akan homogen.

Andriy M
sumber
3

Salah satu opsi adalah menambahkan a containertype_idke Containertabel. Jadikan kolom BUKAN NULL, dan kunci asing ke ContainerTypetabel, yang akan memiliki entri untuk setiap jenis item yang bisa dimasukkan ke dalam sebuah wadah:

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

Untuk memastikan jenis wadah tidak dapat diubah, buat pemicu pembaruan yang memeriksa apakah containertype_iddiperbarui, dan memutar kembali perubahan dalam kasus itu.

Kemudian, dalam menyisipkan dan memperbarui pemicu pada tabel tautan kontainer Anda, periksa containertype_id terhadap jenis entitas dalam tabel itu, untuk memastikan mereka cocok.

Jika apa pun yang Anda masukkan ke dalam wadah harus sesuai dengan jenisnya, dan jenisnya tidak dapat diubah, maka semua yang ada di dalam wadah itu akan menjadi jenis yang sama.

CATATAN: Karena pemicu pada tabel tautan adalah apa yang akan memutuskan apa yang cocok, jika Anda perlu memiliki jenis wadah yang dapat memiliki tanaman dan hewan di dalamnya, Anda dapat membuat jenis itu, menetapkannya ke wadah, dan memeriksa untuk itu. . Jadi, Anda mempertahankan fleksibilitas jika beberapa hal berubah pada beberapa titik (misalnya, Anda mendapatkan jenis "majalah" dan "buku" ...).

CATATAN yang kedua: Jika sebagian besar yang terjadi pada wadah adalah sama, terlepas dari apa yang ada di dalamnya, maka ini masuk akal. Jika Anda memiliki hal-hal yang sangat berbeda yang terjadi (dalam sistem, bukan dalam realitas fisik kami) berdasarkan pada isi wadah, maka gagasan Evan Carroll untuk memiliki tabel terpisah untuk jenis wadah terpisah masuk akal dengan sangat baik. Solusi ini menetapkan bahwa wadah memiliki jenis yang berbeda pada saat pembuatan, tetapi menyimpannya dalam tabel yang sama. Jika Anda harus memeriksa jenis setiap kali Anda mengambil tindakan pada sebuah wadah, dan jika tindakan yang Anda lakukan tergantung pada jenisnya, tabel terpisah sebenarnya mungkin lebih cepat dan lebih mudah.

RDFozz
sumber
Ini cara melakukannya tetapi ada banyak kelemahan: melakukan ini membutuhkan tiga pemindaian indeks untuk memasang kembali daftar kontainer / tanaman, memperlambat penyisipan dengan menambahkan pilih di tabel asing, mengurangi integritas menjadi fungsi dari pemicu - kadang-kadang itu berfungsi tetapi saya tidak akan pernah menginginkannya, itu juga memperlambat pembaruan untuk memastikan kolom tidak dimodifikasi. Semua itu mengatakan, saya pikir kita sedang bekerja di sekitar blok mental lebih daripada memenuhi tuntutan aplikasi, tetapi dari suara saya mungkin sendirian dalam hal itu.
Evan Carroll
1
Kami tidak tahu persis apa yang perlu terjadi dari sini; jika sebagian besar aplikasi berfokus pada wadah itu sendiri (mengirimnya, melacaknya, menempatkannya di fasilitas penyimpanan, dll.) maka sebagian besar pertanyaan mungkin tidak berfokus pada isi wadah, hanya pada wadah itu sendiri. Seperti yang saya catat, pasti ada skenario di mana memperlakukan wadah tanaman sebagai entitas yang sama sekali berbeda dari wadah hewan masuk akal. OP harus memutuskan skenario apa yang mereka hadapi.
RDFozz
3

Jika Anda hanya membutuhkan 2 atau 3 kategori (tanaman / metazoa / bakteri) dan Anda ingin membuat model hubungan XOR, mungkin "busur" adalah solusi untuk Anda. Keuntungan: tidak perlu pemicu. Diagram contoh dapat ditemukan [di sini] [1]. Dalam situasi Anda, tabel "wadah" akan memiliki 3 kolom dengan batasan PERIKSA, yang memungkinkan tanaman atau hewan atau bakteri.

Ini mungkin tidak tepat jika akan ada kebutuhan untuk membedakan banyak kategori (misalnya genera, spesies, subspesies) di masa depan. Namun, untuk 2-3 grup / kategori ini dapat melakukan trik.

UPDATE: Terinspirasi oleh saran dan komentar kontributor, solusi berbeda yang memungkinkan banyak taksa (kelompok organisme terkait, diklasifikasikan oleh ahli biologi), dan menghindari nama tabel "spesifik" (PostgreSQL 9.5).

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

Data uji:

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

Pengujian:

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

Terima kasih kepada @RDFozz dan @Evan Carroll dan @ypercube untuk masukan dan kesabaran mereka (membaca / mengoreksi jawaban saya).

Stefan
sumber
1

Pertama, saya setuju dengan @RDFozz pada pembacaan pertanyaan .. Namun dia mengangkat beberapa kekhawatiran pada jawaban Stefan ,

masukkan deskripsi gambar di sini

Untuk mengatasi kekhawatirannya, adil

  1. Hapus PRIMARY KEY
  2. Tambahkan UNIQUEkendala untuk melindungi terhadap entri duplikat.
  3. Tambahkan EXCLUSIONkendala untuk memastikan wadahnya "homogen"
  4. Tambahkan indeks c_iduntuk memastikan kinerja yang layak.
  5. Bunuh siapa pun yang melakukan ini, arahkan mereka ke jawaban saya yang lain untuk kewarasan.

Ini seperti apa,

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

Sekarang Anda dapat memiliki satu wadah dengan banyak hal, tetapi hanya satu jenis benda dalam satu wadah.

# 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).

Dan itu semua diimplementasikan pada indeks GIST.

Piramida Agung Giza tidak memiliki apa pun di PostgreSQL.

Evan Carroll
sumber
0

Saya memiliki wadah meja yang dapat memiliki hubungan banyak ke banyak tabel, katakanlah itu adalah tanaman, hewan dan bakteri.

Itu ide yang buruk.

Tetapi dengan skema ini, saya tidak dapat menemukan cara mengimplementasikan batasan bahwa kontainer harus homogen.

Dan sekarang Anda tahu mengapa. =)

Saya percaya Anda terjebak pada gagasan pewarisan dari pemrograman berorientasi objek (OO). OO Inheritance memecahkan masalah dengan penggunaan kembali kode. Dalam SQL, kode redundan adalah yang paling sedikit dari masalah kita. Integritas adalah yang pertama dan terpenting. Kinerja seringkali kedua. Kami akan menikmati rasa sakit untuk dua yang pertama. Kami tidak memiliki "waktu kompilasi" yang dapat menghilangkan biaya.

Jadi lupakan saja obsesi Anda untuk menggunakan kembali kode. Wadah untuk tanaman, hewan, dan bakteri pada dasarnya berbeda di setiap tempat di dunia nyata. Komponen penggunaan kembali kode "memegang barang" tidak akan melakukannya untuk Anda. Pisahkan mereka. Tidak hanya akan membuat Anda lebih integritas dan lebih banyak kinerja, tetapi di masa depan Anda akan menemukan lebih mudah untuk memperluas skema Anda: setelah semua, dalam skema Anda, Anda sudah harus memecah item yang terkandung (tanaman, hewan, dll) , tampaknya paling tidak mungkin Anda harus memecah wadah. Anda tidak akan ingin mendesain ulang seluruh skema Anda saat itu.

Evan Carroll
sumber
Memisahkan wadah akan memindahkan masalah ke bagian skema yang berbeda, saya masih perlu referensi wadah dari tabel lain dan bagian-bagian itu harus membedakan jenis wadah yang berbeda juga.
Mad Scientist
Mereka akan tahu jenis wadah apa yang mereka dapatkan hanya dari meja tempat mereka menemukan wadah. Saya bingung dengan maksud Anda? Tanaman mereferensikan satu wadah saja plant_containers, dan seterusnya. Hal-hal yang hanya membutuhkan wadah tanaman pilih hanya dari plant_containerstabel. Hal-hal yang memerlukan wadah apa pun (yaitu mencari semua jenis wadah) dapat dilakukan UNION ALLpada ketiga tabel dengan wadah.
Evan Carroll