Това, което описвате, се нарича полиморфни асоциации. Тоест, колоната "чужд ключ" съдържа стойност на идентификатор, която трябва да съществува в една от набор от целеви таблици. Обикновено целевите таблици са свързани по някакъв начин, като например екземпляри на някакъв общ суперклас данни. Ще ви трябва и друга колона отстрани на колоната с външния ключ, така че на всеки ред да можете да посочите коя целева таблица е препратка.
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
);
Няма начин за моделиране на полиморфни асоциации с помощта на SQL ограничения. Ограничението за външен ключ винаги препраща към един целева таблица.
Полиморфните асоциации се поддържат от рамки като Rails и Hibernate. Но те изрично казват, че трябва да деактивирате SQL ограниченията, за да използвате тази функция. Вместо това приложението или рамката трябва да извършват еквивалентна работа, за да гарантират, че препратката е удовлетворена. Тоест стойността във външния ключ присъства в една от възможните целеви таблици.
Полиморфните асоциации са слаби по отношение на налагането на последователност на базата данни. Целостта на данните зависи от това, че всички клиенти имат достъп до базата данни с наложена една и съща логика на референтна цялост, а също така прилагането трябва да е без грешки.
Ето някои алтернативни решения, които се възползват от наложената от база данни референтна цялост:
Създайте една допълнителна таблица на цел. Например popular_states
и popular_countries
, който препраща към states
и countries
съответно. Всяка от тези „популярни“ таблици също препраща към потребителския профил.
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),
);
Това означава, че за да получите всички популярни любими места на потребителя, трябва да направите заявка и за двете таблици. Но това означава, че можете да разчитате на базата данни, за да наложите последователност.
Създайте places
маса като супермаса. Както Abie споменава, втора алтернатива е популярните ви места да препращат към таблица като places
, който е родител на двете states
и countries
. Тоест и щатите, и държавите също имат външен ключ към places
(можете дори да направите този външен ключ също така първичен ключ на states
и countries
).
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)
);
Използвайте две колони. Вместо една колона, която може да препраща към двете целеви таблици, използвайте две колони. Тези две колони може да са NULL
; всъщност само един от тях трябва да е не-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)
);
От гледна точка на релационната теория, Полиморфните асоциации нарушават Първа нормална форма
, тъй като popular_place_id
всъщност е колона с две значения:това е или държава, или държава. Не бихте съхранили age
на човек и техния phone_number
в една колона и поради същата причина не трябва да съхранявате и двата state_id
и country_id
в една колона. Фактът, че тези два атрибута имат съвместими типове данни, е случаен; те все още означават различни логически обекти.
Полиморфните асоциации също нарушава Трета нормална форма , тъй като значението на колоната зависи от допълнителната колона, която назовава таблицата, към която се отнася външният ключ. В трета нормална форма атрибутът в таблица трябва да зависи само от първичния ключ на тази таблица.
Повторен коментар от @SavasVedova:
Не съм сигурен, че следвам описанието ви, без да видя дефинициите на таблицата или примерна заявка, но звучи сякаш просто имате няколко Filters
таблици, всяка от които съдържа външен ключ, който препраща към централен Products
маса.
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...
Обединяването на продуктите към конкретен тип филтър е лесно, ако знаете към кой тип искате да се присъедините:
SELECT * FROM Products
INNER JOIN FiltersType2 USING (product_id)
Ако искате типът на филтъра да бъде динамичен, трябва да напишете код на приложението, за да създадете SQL заявката. SQL изисква таблицата да бъде посочена и фиксирана в момента, в който пишете заявката. Не можете да накарате обединената таблица да бъде избирана динамично въз основа на стойностите, намерени в отделните редове на Products
.
Единствената друга опция е да се присъедините към всички филтриращи таблици с помощта на външни съединения. Тези, които нямат съответстващ product_id, просто ще бъдат върнати като един ред нулеви стойности. Но все пак трябва да кодирате всички обединените таблици и ако добавите нови филтриращи таблици, трябва да актуализирате кода си.
SELECT * FROM Products
LEFT OUTER JOIN FiltersType1 USING (product_id)
LEFT OUTER JOIN FiltersType2 USING (product_id)
LEFT OUTER JOIN FiltersType3 USING (product_id)
...
Друг начин да се присъедините към всички филтриращи таблици е да го направите последователно:
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)
...
Но този формат все още изисква от вас да пишете препратки към всички таблици. Няма как да се заобиколи.