Có thể thực hiện khóa ngoại của MySQL vào một trong hai bảng có thể không?


180

Vâng, đây là vấn đề của tôi, tôi có ba bảng; vùng, quốc gia, tiểu bang. Các quốc gia có thể bên trong các khu vực, các quốc gia có thể bên trong các khu vực. Khu vực là hàng đầu của chuỗi thức ăn.

Bây giờ tôi đang thêm một bảng common_areas với hai cột; khu vực_id và phổ biến_place_id. Có thể biến phổ biến_place_id thành khóa ngoại cho các quốc gia HOẶC quốc gia. Có lẽ tôi sẽ phải thêm một cột common_place_type để xác định xem id đang mô tả một quốc gia hay tiểu bang theo cách nào.

Câu trả lời:


282

Những gì bạn đang mô tả được gọi là Hiệp hội đa hình. Nghĩa là, cột "khóa ngoại" chứa một giá trị id phải tồn tại trong một trong các bảng mục tiêu. Thông thường, các bảng đích có liên quan theo một cách nào đó, chẳng hạn như là các thể hiện của một số siêu lớp dữ liệu phổ biến. Bạn cũng cần một cột khác dọc theo cột khóa ngoại, để trên mỗi hàng, bạn có thể chỉ định bảng mục tiêu nào được tham chiếu.

CREATE TABLE popular_places (
  user_id INT NOT NULL,
  place_id INT NOT NULL,
  place_type VARCHAR(10) -- either 'states' or 'countries'
  -- foreign key is not possible
);

Không có cách nào để mô hình hóa các Hiệp hội đa hình bằng các ràng buộc SQL. Một ràng buộc khóa ngoại luôn tham chiếu một bảng mục tiêu.

Hiệp hội đa hình được hỗ trợ bởi các khung như Rails và Hibernate. Nhưng họ nói rõ ràng rằng bạn phải vô hiệu hóa các ràng buộc SQL để sử dụng tính năng này. Thay vào đó, ứng dụng hoặc khung phải làm công việc tương đương để đảm bảo rằng tài liệu tham khảo được thỏa mãn. Đó là, giá trị trong khóa ngoại hiện diện trong một trong các bảng mục tiêu có thể.

Các hiệp hội đa hình là yếu đối với việc thực thi tính nhất quán của cơ sở dữ liệu. Tính toàn vẹn dữ liệu phụ thuộc vào tất cả các máy khách truy cập cơ sở dữ liệu với cùng logic logic toàn vẹn tham chiếu được thi hành và việc thực thi phải không có lỗi.

Dưới đây là một số giải pháp thay thế tận dụng tính toàn vẹn tham chiếu được thi hành bởi cơ sở dữ liệu:

Tạo thêm một bảng cho mỗi mục tiêu. Ví dụ popular_statespopular_countries, tham chiếu statescountriestương ứng. Mỗi bảng "phổ biến" này cũng tham chiếu hồ sơ của người dùng.

CREATE TABLE popular_states (
  state_id INT NOT NULL,
  user_id  INT NOT NULL,
  PRIMARY KEY(state_id, user_id),
  FOREIGN KEY (state_id) REFERENCES states(state_id),
  FOREIGN KEY (user_id) REFERENCES users(user_id),
);

CREATE TABLE popular_countries (
  country_id INT NOT NULL,
  user_id    INT NOT NULL,
  PRIMARY KEY(country_id, user_id),
  FOREIGN KEY (country_id) REFERENCES countries(country_id),
  FOREIGN KEY (user_id) REFERENCES users(user_id),
);

Điều này không có nghĩa là để có được tất cả các địa điểm yêu thích phổ biến của người dùng, bạn cần truy vấn cả hai bảng này. Nhưng nó có nghĩa là bạn có thể dựa vào cơ sở dữ liệu để thực thi tính nhất quán.

Tạo một placesbảng như một supertable. Như Abie đề cập, một lựa chọn thứ hai là các địa điểm phổ biến của bạn tham chiếu một bảng như thế places, đó là cha mẹ của cả hai statescountries. Nghĩa là, cả tiểu bang và quốc gia cũng có khóa ngoại places(bạn thậm chí có thể đặt khóa ngoại này cũng là khóa chính của statescountries).

CREATE TABLE popular_areas (
  user_id INT NOT NULL,
  place_id INT NOT NULL,
  PRIMARY KEY (user_id, place_id),
  FOREIGN KEY (place_id) REFERENCES places(place_id)
);

CREATE TABLE states (
  state_id INT NOT NULL PRIMARY KEY,
  FOREIGN KEY (state_id) REFERENCES places(place_id)
);

CREATE TABLE countries (
  country_id INT NOT NULL PRIMARY KEY,
  FOREIGN KEY (country_id) REFERENCES places(place_id)
);

Sử dụng hai cột. Thay vì một cột có thể tham chiếu một trong hai bảng đích, hãy sử dụng hai cột. Hai cột này có thể là NULL; trong thực tế chỉ có một trong số họ là không NULL.

CREATE TABLE popular_areas (
  place_id SERIAL PRIMARY KEY,
  user_id INT NOT NULL,
  state_id INT,
  country_id INT,
  CONSTRAINT UNIQUE (user_id, state_id, country_id), -- UNIQUE permits NULLs
  CONSTRAINT CHECK (state_id IS NOT NULL OR country_id IS NOT NULL),
  FOREIGN KEY (state_id) REFERENCES places(place_id),
  FOREIGN KEY (country_id) REFERENCES places(place_id)
);

Về mặt lý thuyết quan hệ, các Hiệp hội đa hình vi phạm hình thức bình thường đầu tiên , bởi vì thực popular_place_idtế, một cột có hai ý nghĩa: đó là một quốc gia hoặc một quốc gia. Bạn sẽ không lưu trữ một người agevà của họ phone_numbertrong một cột và vì lý do tương tự, bạn không nên lưu trữ cả hai state_idcountry_idtrong một cột. Thực tế là hai thuộc tính này có kiểu dữ liệu tương thích là ngẫu nhiên; chúng vẫn biểu thị các thực thể logic khác nhau.

Các hiệp hội đa hình cũng vi phạm Mẫu thông thường thứ ba , bởi vì ý nghĩa của cột phụ thuộc vào cột phụ đặt tên bảng mà khóa ngoại đề cập đến. Trong Dạng thông thường thứ ba, một thuộc tính trong bảng phải chỉ phụ thuộc vào khóa chính của bảng đó.


Nhận xét lại từ @SavasVedova:

Tôi không chắc chắn tôi làm theo mô tả của bạn mà không thấy các định nghĩa bảng hoặc truy vấn mẫu, nhưng có vẻ như bạn chỉ đơn giản có nhiều Filtersbảng, mỗi bảng chứa một khóa ngoại tham chiếu một Productsbảng trung tâm .

CREATE TABLE Products (
  product_id INT PRIMARY KEY
);

CREATE TABLE FiltersType1 (
  filter_id INT PRIMARY KEY,
  product_id INT NOT NULL,
  FOREIGN KEY (product_id) REFERENCES Products(product_id)
);

CREATE TABLE FiltersType2 (
  filter_id INT  PRIMARY KEY,
  product_id INT NOT NULL,
  FOREIGN KEY (product_id) REFERENCES Products(product_id)
);

...and other filter tables...

Việc kết hợp các sản phẩm với một loại bộ lọc cụ thể thật dễ dàng nếu bạn biết loại nào bạn muốn tham gia:

SELECT * FROM Products
INNER JOIN FiltersType2 USING (product_id)

Nếu bạn muốn loại bộ lọc là động, bạn phải viết mã ứng dụng để xây dựng truy vấn SQL. SQL yêu cầu bảng được chỉ định và cố định tại thời điểm bạn viết truy vấn. Bạn không thể làm cho bảng đã tham gia được chọn động dựa trên các giá trị được tìm thấy trong các hàng riêng lẻ Products.

Tùy chọn duy nhất khác là tham gia vào tất cả các bảng bộ lọc bằng cách sử dụng các phép nối ngoài. Những sản phẩm không có sản phẩm phù hợp sẽ chỉ được trả về dưới dạng một hàng null. Nhưng bạn vẫn phải mã hóa tất cả các bảng đã tham gia và nếu bạn thêm các bảng lọc mới, bạn phải cập nhật mã của mình.

SELECT * FROM Products
LEFT OUTER JOIN FiltersType1 USING (product_id)
LEFT OUTER JOIN FiltersType2 USING (product_id)
LEFT OUTER JOIN FiltersType3 USING (product_id)
...

Một cách khác để tham gia vào tất cả các bảng bộ lọc là thực hiện một cách thanh thản:

SELECT * FROM Product
INNER JOIN FiltersType1 USING (product_id)
UNION ALL
SELECT * FROM Products
INNER JOIN FiltersType2 USING (product_id)
UNION ALL
SELECT * FROM Products
INNER JOIN FiltersType3 USING (product_id)
...

Nhưng định dạng này vẫn yêu cầu bạn viết tham chiếu đến tất cả các bảng. Không có xung quanh đó.


Cái nào bạn muốn đề nghị Bill? Tôi đang trong quá trình thiết kế cơ sở dữ liệu nhưng tôi bị lạc. Về cơ bản tôi cần liên kết các bộ lọc với một sản phẩm và các giá trị của các bộ lọc sẽ được điền vào các bảng khác nhau. Nhưng vấn đề là các bộ lọc sẽ được tạo bởi quản trị viên, do đó tùy thuộc vào loại bộ lọc, dữ liệu có thể thay đổi và do đó joinmục tiêu cũng sẽ thay đổi ...... Tôi có phức tạp quá không? Cứu giúp!
Savas Vedova

+1 cảm ơn bạn vì một giải pháp tuyệt vời. Một câu hỏi tôi có với giải pháp thứ nhất / thứ hai là: có bất kỳ vi phạm chuẩn hóa nào với thực tế là nhiều bảng có thể tham chiếu đến cùng một khóa chính trong bảng meta đó không? Tôi biết bạn có thể giải quyết vấn đề này bằng logic, nhưng tôi không thấy cách nào để cơ sở dữ liệu thực thi nó, trừ khi tôi thiếu một cái gì đó.
Cướp

5
Tôi thực sự thích cách tiếp cận với "KIỂM TRA CONSTRAINT". Nhưng nó có thể được cải thiện nếu chúng ta thay đổi "HOẶC" thành "XOR". Bằng cách đó, chúng tôi đảm bảo rằng chỉ có một cột trong tập hợp KHÔNG phải là NULL
alex_b

1
@alex_b, vâng, điều đó tốt, nhưng XOR logic không phải là SQL chuẩn và không được tất cả các thương hiệu SQL hỗ trợ. MySQL có nó, nhưng PostgreSQL thì không. Oracle có nó, nhưng Microsoft thì không cho đến năm 2016. Và cứ thế.
Bill Karwin

1
"Hai cột này có thể là NULL; trên thực tế chỉ có một trong số chúng không phải là NULL" - điều này sẽ vi phạm 1NF!
onedaywhen

10

Đây không phải là giải pháp thanh lịch nhất trên thế giới, nhưng bạn có thể sử dụng kế thừa bảng cụ thể để thực hiện công việc này.

Về mặt khái niệm, bạn đang đề xuất một khái niệm về một lớp "những thứ có thể là khu vực phổ biến" mà từ đó ba loại địa điểm của bạn được thừa hưởng. Bạn có thể đại diện này như một bảng gọi là, ví dụ, placestrong đó mỗi hàng có một one-to-one mối quan hệ với một hàng trong regions, countrieshoặc states. (Thuộc tính được chia sẻ giữa các vùng, quốc gia hoặc tiểu bang, nếu có, có thể được đẩy vào bảng nơi này.) Của bạn popular_place_idsau đó sẽ là một tài liệu tham khảo chính nước ngoài để một hàng trong bảng nơi mà sau đó sẽ dẫn bạn đến một khu vực, đất nước , hoặc nhà nước.

Giải pháp bạn đề xuất với cột thứ hai để mô tả loại kết hợp xảy ra là cách Rails xử lý các liên kết đa hình, nhưng nói chung tôi không phải là người hâm mộ. Bill giải thích rất chi tiết tại sao các hiệp hội đa hình không phải là bạn của bạn.


1
hay còn gọi là "mô hình siêu kiểu phụ"
ErikE

Ngoài ra bài viết này cũng nêu rõ khái niệm duhallowgreygeek.com/polymorphic-association-bad-sql-smell
Marco Staffoli

5

Đây là một sửa chữa cho cách tiếp cận "có thể xác thực" của Bill Karwin, sử dụng khóa tổng hợp ( place_type, place_id )để giải quyết các vi phạm hình thức thông thường được nhận thức:

CREATE TABLE places (
  place_id INT NOT NULL UNIQUE,
  place_type VARCHAR(10) NOT NULL
     CHECK ( place_type = 'state', 'country' ),
  UNIQUE ( place_type, place_id )
);

CREATE TABLE states (
  place_id INT NOT NULL UNIQUE,
  place_type VARCHAR(10) DEFAULT 'state' NOT NULL
     CHECK ( place_type = 'state' ),
  FOREIGN KEY ( place_type, place_id ) 
     REFERENCES places ( place_type, place_id )
  -- attributes specific to states go here
);

CREATE TABLE countries (
  place_id INT NOT NULL UNIQUE,
  place_type VARCHAR(10) DEFAULT 'country' NOT NULL
     CHECK ( place_type = 'country' ),
  FOREIGN KEY ( place_type, place_id ) 
     REFERENCES places ( place_type, place_id )
  -- attributes specific to country go here
);

CREATE TABLE popular_areas (
  user_id INT NOT NULL,
  place_id INT NOT NULL,
  UNIQUE ( user_id, place_id ),
  FOREIGN KEY ( place_type, place_id ) 
     REFERENCES places ( place_type, place_id )
);

Những gì thiết kế này không thể đảm bảo rằng cho mỗi hàng trong placesđó tồn tại một hàng trong stateshoặc countries(nhưng không phải cả hai). Đây là một hạn chế của khóa ngoại trong SQL. Trong một DBMS tuân thủ đầy đủ các tiêu chuẩn SQL-92, bạn có thể xác định các ràng buộc giữa các bảng có thể bảo vệ sẽ cho phép bạn đạt được điều tương tự nhưng nó rất rắc rối, liên quan đến giao dịch và một DBMS như vậy vẫn chưa được đưa ra thị trường.


0

Tôi nhận ra rằng chủ đề này đã cũ, nhưng tôi đã thấy điều này và một giải pháp xuất hiện trong đầu và tôi nghĩ rằng tôi đã ném nó ra khỏi đó.

Khu vực, quốc gia và quốc gia là các vị trí địa lý sống trong một hệ thống phân cấp.

Bạn có thể tránh vấn đề của mình hoàn toàn bằng cách tạo một bảng miền có tên là Geo_location_type mà bạn sẽ có ba hàng (Vùng, Quốc gia, Bang).

Tiếp theo, thay vì ba bảng vị trí, hãy tạo một bảng vị trí địa lý duy nhất có khóa ngoại là ge_location_type_id (để bạn biết nếu thể hiện là Vùng, Quốc gia hoặc Bang).

Mô hình hóa hệ thống phân cấp bằng cách làm cho bảng này tự tham chiếu sao cho một thể hiện Trạng thái giữ fKey cho đối tượng Quốc gia mẹ của nó, lần lượt giữ fKey cho thể hiện Vùng mẹ của nó. Các thể hiện vùng sẽ giữ NULL trong fKey đó. Điều này không khác gì những gì bạn đã làm với ba bảng (bạn sẽ có 1 - nhiều mối quan hệ giữa vùng và quốc gia và giữa quốc gia và tiểu bang) ngoại trừ bây giờ tất cả chỉ trong một bảng.

Bảng common_user_location sẽ là bảng phân giải phạm vi giữa người dùng và georgraphical_location (vì vậy nhiều người dùng có thể thích nhiều nơi).

Soooo

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

CREATE TABLE [geographical_location_type] (
    [geographical_location_type_id] INTEGER NOT NULL,
    [name] VARCHAR(25) NOT NULL,
    CONSTRAINT [PK_geographical_location_type] PRIMARY KEY ([geographical_location_type_id])
)

-- Add 'Region', 'Country' and 'State' instances to the above table


CREATE TABLE [geographical_location] (
   [geographical_location_id] BIGINT IDENTITY(0,1) NOT NULL,
    [name] VARCHAR(1024) NOT NULL,
    [geographical_location_type_id] INTEGER NOT NULL,
    [geographical_location_parent] BIGINT,  -- self referencing; can be null for top-level instances
    CONSTRAINT [PK_geographical_location] PRIMARY KEY ([geographical_location_id])
)

CREATE TABLE [user] (
    [user_id] BIGINT NOT NULL,
    [login_id] VARCHAR(30) NOT NULL,
    [password] VARCHAR(512) NOT NULL,
    CONSTRAINT [PK_user] PRIMARY KEY ([user_id])
)


CREATE TABLE [popular_user_location] (
    [popular_user_location_id] BIGINT NOT NULL,
    [user_id] BIGINT NOT NULL,
    [geographical_location_id] BIGINT NOT NULL,
    CONSTRAINT [PK_popular_user_location] PRIMARY KEY ([popular_user_location_id])
)

ALTER TABLE [geographical_location] ADD CONSTRAINT [geographical_location_type_geographical_location] 
    FOREIGN KEY ([geographical_location_type_id]) REFERENCES [geographical_location_type] ([geographical_location_type_id])



ALTER TABLE [geographical_location] ADD CONSTRAINT [geographical_location_geographical_location] 
    FOREIGN KEY ([geographical_location_parent]) REFERENCES [geographical_location] ([geographical_location_id])



ALTER TABLE [popular_user_location] ADD CONSTRAINT [user_popular_user_location] 
    FOREIGN KEY ([user_id]) REFERENCES [user] ([user_id])



ALTER TABLE [popular_user_location] ADD CONSTRAINT [geographical_location_popular_user_location] 
    FOREIGN KEY ([geographical_location_id]) REFERENCES [geographical_location] ([geographical_location_id])

Không chắc DB mục tiêu là gì; ở trên là MS SQL Server.


0

Vâng, tôi có hai bảng:

  1. bài hát

a) Số bài hát b) Tên bài hát ....

  1. danh sách phát a) Số danh sách phát b) Tiêu đề danh sách phát ...

và tôi có một phần ba

  1. bài hát_to_playlist_relation

Vấn đề là một số loại danh sách phát có liên kết đến danh sách phát khác. Nhưng trong mysql chúng ta không có khóa ngoại được liên kết với hai bảng.

Giải pháp của tôi: Tôi sẽ đặt một cột thứ ba trong tests_to_playlist_relation. Cột đó sẽ là boolean. Nếu 1 thì bài hát, người khác sẽ liên kết với bảng danh sách phát.

Vì thế:

  1. bài hát_to_playlist_relation

a) Playlist_number (int) b) Là bài hát (boolean) c) Số tương đối (số bài hát hoặc số danh sách phát) (int) ( không phải khóa ngoại đối với bất kỳ bảng nào)

 # tạo bảng  bài hát
    truy vấn . nối thêm ( "SET SQL_MODE = NO_AUTO_VALUE_ON_ZERO;" ) 
    truy vấn . append ( "CREATE TABLE songs( NUMBERint (11) NOT NULL, SONG POSITIONint (11) NOT NULL, PLAY SONGtinyint (1) NOT NULL DEFAULT '1', SONG TITLEvarchar (255) CHARACTER SET utf8 đối chiếu utf8_general_ci NOT NULL, DESCRIPTIONvarchar (SET utf8 1000) CHARACTER đối chiếu utf8_general_ci NOT NULL, ARTISTvarchar (255) CHARACTER SET utf8 đối chiếu utf8_general_ci NOT NULL DEFAULT 'Άγνωστος καλλιτέχνης', AUTHORvarchar (255) CHARACTER SET utf8 đối chiếu utf8_general_ci NOT NULL DEFAULT 'Άγνωστος στιχουργός', COMPOSERvarchar (255) CHARACTER SET utf8 đối chiếu utf8_general_ci NOT NULL DEFAULT 'Άγ,ALBUMvarchar (255) CHARACTER SET utf8 đối chiếu utf8_general_ci NOT NULL DEFAULT 'Άγνωστο άλμπουμ', YEARint (11) NOT NULL DEFAULT '33', RATINGint (11) NOT NULL DEFAULT '5', IMAGEvarchar (600) CHARACTER SET utf8 đối chiếu utf8_general_ci NOT NULL , SONG PATHvarchar (500) CHARACTER SET utf8 đối chiếu utf8_general_ci NOT NULL, SONG REPEATint (11) NOT NULL DEFAULT '0', VOLUMEnổi NOT NULL DEFAULT '1', SPEEDnổi NOT NULL DEFAULT '1') ENGINE = InnoDB DEFAULT CHARSET = utf8;" ) 
    truy vấn . append ( "ALTER TABLE songsADD PRIMARY KEY ( NUMBER), ADD UNIQUE KEY POSITION( SONG POSITION), ADD UNIQUE KEY TITLE( SONG TITLE), ADD UNIQUE KEY PATH( SONG PATH);") 
    truy vấn. chắp thêm ( "ALTER TABLE songsMODIFY NUMBERint (11) KHÔNG NULL AUTO_INCREMENT;" )

#create table playlists
queries.append("CREATE TABLE `playlists` (`NUMBER` int(11) NOT NULL,`PLAYLIST POSITION` int(11) NOT NULL,`PLAYLIST TITLE` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,`PLAYLIST PATH` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8;")
queries.append("ALTER TABLE `playlists` ADD PRIMARY KEY (`NUMBER`),ADD UNIQUE KEY `POSITION` (`PLAYLIST POSITION`),ADD UNIQUE KEY `TITLE` (`PLAYLIST TITLE`),ADD UNIQUE KEY `PATH` (`PLAYLIST PATH`);")
queries.append("ALTER TABLE `playlists` MODIFY `NUMBER` int(11) NOT NULL AUTO_INCREMENT;")

#create table for songs to playlist relation
queries.append("CREATE TABLE `songs of playlist` (`PLAYLIST NUMBER` int(11) NOT NULL,`SONG OR PLAYLIST` tinyint(1) NOT NULL DEFAULT '1',`RELATIVE NUMBER` int(11) NOT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8;")
queries.append("ALTER TABLE `songs of playlist` ADD KEY `PLAYLIST NUMBER` (`PLAYLIST NUMBER`) USING BTREE;")
queries.append("ALTER TABLE `songs of playlist` ADD CONSTRAINT `playlist of playlist_ibfk_1` FOREIGN KEY (`PLAYLIST NUMBER`) REFERENCES `playlists` (`NUMBER`) ON DELETE RESTRICT ON UPDATE RESTRICT")

Đó là tất cả!

playlists_query = "CHỌN s1. *, s3. *, s4. * TỪ các bài hát dưới dạng s1 INNER THAM GIA .`NUMBER` = s2.`PLAYLIST SỐ` THAM GIA `danh sách phát` với tư cách là s4 TRÊN s4.`NUMBER` = s2.
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.