Mối quan hệ nhiều-nhiều-nhiều loại trừ lẫn nhau


9

Tôi có một bảng containerscó thể có một nhiều-nhiều mối quan hệ với một số bảng biểu, giả sử đó là những plants, animalsbacteria. Mỗi thùng chứa có thể chứa một số lượng thực vật, động vật hoặc vi khuẩn tùy ý, và mỗi thực vật, động vật hoặc vi khuẩn có thể nằm trong một số lượng tùy ý của các thùng chứa.

Cho đến nay điều này rất đơn giản, nhưng phần tôi gặp vấn đề là mỗi container chỉ nên chứa các phần tử cùng loại. Các thùng chứa hỗn hợp, ví dụ như chứa cả thực vật và động vật nên là một vi phạm ràng buộc trong cơ sở dữ liệu.

Lược đồ ban đầu của tôi cho điều này là như sau:

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


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


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


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

Nhưng với lược đồ này, tôi không thể đưa ra cách thực hiện các ràng buộc mà các thùng chứa phải đồng nhất.

Có cách nào để thực hiện điều này với tính toàn vẹn tham chiếu và đảm bảo ở mức cơ sở dữ liệu rằng các thùng chứa là đồng nhất không?

Tôi đang sử dụng Postgres 9.6 cho việc này.


1
Các container có đồng nhất không? Điều đó có nghĩa là, một container đang giữ thực vật ngày nay có thể được dọn sạch và, mà không có bất kỳ thay đổi nào, giữ động vật hoặc vi khuẩn vào ngày mai?
RDFozz

@RDFozz Tôi không có kế hoạch cho phép điều đó trong UI, nhưng về nguyên tắc thì điều đó là có thể. Nó không thực sự có ý nghĩa để làm điều đó, xóa container và tạo một cái mới sẽ là hành động điển hình. Nhưng nếu một container thay đổi loại nội dung, nó sẽ không phá vỡ bất cứ thứ gì
Nhà khoa học điên

Câu trả lời:


10

Có một cách để thực hiện khai báo này mà không thay đổi nhiều thiết lập hiện tại của bạn, nếu bạn đồng ý giới thiệu một số dự phòng cho nó. Những gì tiếp theo có thể được coi là một sự phát triển theo đề xuất của RDFozz , mặc dù ý tưởng đó hoàn toàn hình thành trong đầu tôi trước khi tôi đọc câu trả lời của anh ấy (và nó đủ khác để đảm bảo bài trả lời của chính nó).

Thực hiện

Đây là những gì bạn làm, từng bước:

  1. Tạo một containerTypesbảng dọc theo dòng của một gợi ý trong câu trả lời của RDFozz:

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

    Nhập nó với ID được xác định trước cho từng loại. Với mục đích của câu trả lời này, hãy để chúng khớp với ví dụ của RDFozz: 1 cho thực vật, 2 cho động vật, 3 cho vi khuẩn.

  2. Thêm một containerType_idcột vào containersvà làm cho nó không thể rỗng và một khóa ngoại.

    ALTER TABLE containers
    ADD containerType_id int NOT NULL
      REFERENCES containerTypes (id);
    
  3. Giả sử idcột đã là khóa chính của containers, tạo một ràng buộc duy nhất trên (id, containerType_id).

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

    Đây là nơi dự phòng bắt đầu. Nếu idđược khai báo là khóa chính, chúng ta có thể yên tâm nó là duy nhất. Nếu nó là duy nhất, bất kỳ sự kết hợp nào idvà một cột khác cũng bị ràng buộc là duy nhất mà không cần khai báo thêm về tính duy nhất - vậy, vấn đề là gì? Vấn đề là bằng cách chính thức khai báo cặp cột duy nhất, chúng ta cho phép chúng được tham chiếu , tức là trở thành mục tiêu của một ràng buộc khóa ngoài, đó là những gì phần này nói về.

  4. Thêm một containerType_idcột cho mỗi bảng ngã ba ( containers_animals, containers_plants, containers_bacteria). Làm cho nó một khóa ngoại là hoàn toàn tùy chọn. Điều quan trọng là đảm bảo cột có cùng giá trị cho tất cả các hàng, khác nhau cho mỗi bảng: 1 cho containers_plants, 2 cho containers_animals, 3 cho containers_bacteria, theo các mô tả trong containerTypes. Trong mỗi trường hợp, bạn cũng có thể đặt giá trị đó thành mặc định để đơn giản hóa các câu lệnh chèn của mình:

    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. Trong mỗi bảng nối, làm cho cặp cột (container_id, containerType_id)tham chiếu ràng buộc khóa ngoài 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);
    

    Nếu container_idđã được xác định là tham chiếu đến containers, vui lòng xóa ràng buộc đó khỏi mỗi bảng vì không còn cần thiết.

Làm thế nào nó hoạt động

Bằng cách thêm cột loại bộ chứa và làm cho nó tham gia vào các ràng buộc khóa ngoài, bạn chuẩn bị một cơ chế ngăn loại bộ chứa thay đổi. Việc thay đổi loại trong containersloại sẽ chỉ có thể nếu các khóa ngoại được xác định bằng DEFERRABLEmệnh đề mà chúng không được phép có trong triển khai này.

Ngay cả khi chúng có thể được bảo vệ, việc thay đổi loại vẫn sẽ là không thể vì ràng buộc kiểm tra ở phía bên kia của containersmối quan hệ bảng Bảng điều khiển. Mỗi bảng nối chỉ cho phép một loại container cụ thể. Điều đó không chỉ ngăn các tham chiếu hiện có thay đổi loại mà còn ngăn thêm các tham chiếu loại sai. Đó là, nếu bạn có một thùng chứa loại 2 (động vật), bạn chỉ có thể thêm các mục vào nó bằng cách sử dụng bảng loại 2 containers_animals, và sẽ không thể thêm các hàng tham chiếu đến nó, ví dụ, containers_bacteriachấp nhận Chỉ loại 3 container.

Cuối cùng, quyết định của riêng bạn để có các bảng khác nhau cho plants, animalsbacteria, và các bảng giao tiếp khác nhau cho từng loại thực thể, khiến cho một container không thể có các mục có nhiều hơn một loại.

Vì vậy, tất cả các yếu tố này kết hợp đảm bảo, theo cách hoàn toàn khai báo, rằng tất cả các container của bạn sẽ đồng nhất.


3

Một tùy chọn là thêm một containertype_idvào Containerbảng. Tạo cột KHÔNG NULL và khóa ngoại cho ContainerTypebảng, trong đó sẽ có các mục nhập cho từng loại mặt hàng có thể đi trong một thùng chứa:

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

Để đảm bảo loại thùng chứa không thể thay đổi, hãy tạo trình kích hoạt cập nhật để kiểm tra xem loại containertype_idđã được cập nhật chưa và khôi phục thay đổi trong trường hợp đó.

Sau đó, khi chèn và cập nhật trình kích hoạt trên các bảng liên kết vùng chứa của bạn, hãy kiểm tra containertype_id so với loại thực thể trong bảng đó, để đảm bảo chúng khớp với nhau.

Nếu bất cứ điều gì bạn đặt trong một thùng chứa phải phù hợp với loại và loại không thể thay đổi, thì mọi thứ trong thùng chứa sẽ cùng loại.

LƯU Ý: Vì trình kích hoạt trên các bảng liên kết là thứ sẽ quyết định loại nào phù hợp, nếu bạn cần có một loại vật chứa có thể có thực vật và động vật trong đó, bạn có thể tạo loại đó, gán nó vào thùng chứa và kiểm tra xem . Vì vậy, bạn giữ được tính linh hoạt nếu mọi thứ thay đổi một số điểm (giả sử, bạn có các loại "tạp chí" và "sách" ...).

LƯU Ý thứ hai: Nếu hầu hết những gì xảy ra với container là như nhau, bất kể những gì trong chúng, thì điều này có ý nghĩa. Nếu bạn có những điều rất khác xảy ra (trong hệ thống, không phải trong thực tế vật lý của chúng tôi) dựa trên nội dung của container, thì ý tưởng của Evan Carroll về việc có các bảng riêng biệt cho các loại container riêng biệt có ý nghĩa hoàn toàn tốt. Giải pháp này thiết lập rằng các container có các loại khác nhau khi tạo, nhưng giữ chúng trong cùng một bảng. Nếu bạn phải kiểm tra loại mỗi lần bạn thực hiện một hành động trên một thùng chứa và nếu hành động bạn thực hiện phụ thuộc vào loại, các bảng riêng biệt thực sự có thể nhanh hơn và dễ dàng hơn.


Đó là một cách làm nhưng có rất nhiều nhược điểm: làm điều này đòi hỏi ba lần quét chỉ mục để ghép lại danh sách các container / nhà máy, nó làm chậm sự chèn vào bằng cách thêm một lựa chọn trong bảng nước ngoài, nó làm giảm tính toàn vẹn thành chức năng của kích hoạt - đôi khi nó hoạt động nhưng tôi sẽ không bao giờ mong muốn nó, nó cũng làm chậm cập nhật để đảm bảo cột không bị sửa đổi. Tất cả những gì đã nói, tôi nghĩ rằng chúng ta đang làm việc xung quanh khối tinh thần nhiều hơn là đáp ứng nhu cầu của một ứng dụng, nhưng từ số phiếu tôi có thể ở một mình trong đó.
Evan Carroll

1
Chúng tôi không biết chính xác những gì cần phải xảy ra từ đây; nếu phần lớn ứng dụng tập trung vào chính các container (vận chuyển chúng, theo dõi chúng, định vị chúng trong các cơ sở lưu trữ, v.v.) thì hầu hết các truy vấn có thể không tập trung vào nội dung của các container, chỉ vào chính các container. Như tôi đã lưu ý, chắc chắn có những kịch bản trong đó đối xử với một container thực vật như một thực thể hoàn toàn khác với một container động vật có ý nghĩa. OP sẽ phải quyết định kịch bản nào họ phải đối mặt.
RDFozz

3

Nếu bạn chỉ cần 2 hoặc 3 loại (thực vật / metazoa / vi khuẩn) và bạn muốn mô hình hóa mối quan hệ XOR, có thể một "vòng cung" là giải pháp cho bạn. Ưu điểm: không cần kích hoạt. Sơ đồ ví dụ có thể được tìm thấy [ở đây] [1]. Trong tình huống của bạn, bảng "thùng chứa" sẽ có 3 cột với ràng buộc KIỂM TRA, cho phép thực vật hoặc động vật hoặc vi khuẩn.

Điều này có lẽ không phù hợp nếu có nhu cầu phân biệt giữa rất nhiều loại (ví dụ: chi, loài, phân loài) trong tương lai. Tuy nhiên, đối với 2-3 nhóm / danh mục, điều này có thể thực hiện thủ thuật.

CẬP NHẬT: Lấy cảm hứng từ các đề xuất và nhận xét của người đóng góp, một giải pháp khác cho phép nhiều đơn vị phân loại (nhóm sinh vật liên quan, được phân loại bởi nhà sinh vật học) và tránh các tên bảng "cụ thể" (PostgreQuery 9.5).

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

Dữ liệu kiểm tra:

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

Kiểm tra:

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

Cảm ơn @RDFozz và @Evan Carroll và @ypercube vì sự đóng góp và kiên nhẫn của họ (đọc / sửa câu trả lời của tôi).


1

Đầu tiên, tôi đồng ý với @RDFozz về việc đọc câu hỏi .. Tuy nhiên, anh ta đặt ra một số lo ngại về câu trả lời của stefans ,

nhập mô tả hình ảnh ở đây

Để giải quyết mối quan tâm của anh ấy, chỉ cần

  1. Gỡ bỏ PRIMARY KEY
  2. Thêm các UNIQUEràng buộc để bảo vệ chống lại các mục trùng lặp.
  3. Thêm các EXCLUSIONràng buộc để đảm bảo các container là "đồng nhất"
  4. Thêm một chỉ số trên c_idđể đảm bảo hiệu suất tốt.
  5. Giết bất cứ ai làm điều này, chỉ cho họ câu trả lời khác cho sự tỉnh táo.

Đây là những gì nó trông giống như,

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

Bây giờ bạn có thể có một container với nhiều thứ, nhưng chỉ có một loại vật trong một container.

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

Và tất cả đều được thực hiện trên các chỉ mục GIST.

Kim tự tháp Giza vĩ đại không có gì trên PostgreSQL.


0

Tôi có một hộp đựng bàn có thể có mối quan hệ nhiều-nhiều với nhiều bảng, giả sử đó là thực vật, động vật và vi khuẩn.

Đó là một ý tưởng tồi.

Nhưng với lược đồ này, tôi không thể đưa ra cách thực hiện các ràng buộc mà các thùng chứa phải đồng nhất.

Và bây giờ bạn biết tại sao. =)

Tôi tin rằng bạn đang mắc kẹt với ý tưởng kế thừa từ lập trình hướng đối tượng (OO). Kế thừa OO giải quyết vấn đề với việc sử dụng lại mã. Trong SQL, mã dự phòng là vấn đề ít nhất của chúng tôi. Liêm chính là trên hết. Hiệu suất thường là thứ hai. Chúng tôi sẽ tận hưởng nỗi đau cho hai người đầu tiên. Chúng tôi không có "thời gian biên dịch" có thể loại bỏ các chi phí.

Vì vậy, hãy từ bỏ nỗi ám ảnh của bạn để sử dụng lại mã. Các thùng chứa cho thực vật, động vật và vi khuẩn về cơ bản là khác nhau ở mọi nơi trong thế giới thực. Thành phần tái sử dụng mã của "giữ công cụ" sẽ không làm điều đó cho bạn. Phá vỡ chúng ra. Không chỉ giúp bạn toàn vẹn hơn và hiệu suất cao hơn, mà trong tương lai bạn sẽ thấy việc mở rộng lược đồ của mình dễ dàng hơn: sau tất cả, trong lược đồ của bạn, bạn đã phải phá vỡ các mục được chứa (thực vật, động vật, v.v.) , dường như ít nhất có thể là bạn sẽ phải phá vỡ các container. Bạn sẽ không muốn thiết kế lại toàn bộ lược đồ của mình.


Việc tách các container sẽ chuyển vấn đề sang một phần khác của lược đồ, tôi vẫn cần tham chiếu các container từ các bảng khác và các phần đó cũng sẽ phải phân biệt các loại container khác nhau.
Nhà khoa học điên

Họ sẽ biết loại container nào họ có chỉ bằng cái bàn họ tìm thấy container. Tôi bối rối không hiểu ý bạn là gì? Thực vật tham khảo một container duy nhất trong plant_containers, và như vậy. Những thứ chỉ cần một thùng chứa thực vật chỉ chọn từ plant_containersbảng. Những thứ cần bất kỳ container nào (tức là tìm kiếm tất cả các loại container) có thể thực hiện UNION ALLtrên cả ba bảng với các container.
Evan Carroll
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.