Возможно ли сделать внешний ключ MySQL для одной из двух возможных таблиц?
Ну вот моя проблема у меня есть три таблицы; регионы, страны, государства. Страны могут быть внутри регионов, государства могут быть внутри регионов. Регионы являются вершиной пищевой цепи.
теперь я добавляю таблицу popular_areas с двумя столбцами; region_id и popular_place_id. Можно ли сделать popular_place_id внешним ключом для обеих стран или государства. Вероятно, мне придется добавить столбец popular_place_type для определения независимо от того, описывает ли id страну или государство в любом случае.
3 ответов
то, что вы описываете, называется полиморфными ассоциациями. То есть, в столбце "внешний ключ" содержит значение ID, которое должно существовать в одной из целевых таблиц. Как правило, целевые таблицы каким-то образом связаны, например, являются экземплярами некоторого общего суперкласса данных. Вам также понадобится другой столбец вдоль столбца внешнего ключа, чтобы в каждой строке вы могли указать, на какую целевую таблицу ссылаются.
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
таблица как supertable. как упоминает Аби, вторая альтернатива заключается в том, что ваши популярные места ссылаются на таблицу, такую как 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)
);
In термины реляционной теории, полиморфные Ассоциации нарушают Первая Нормальная Форма, потому что popular_place_id
фактически столбец с двумя значениями: это либо государство, либо страна. Ты не будешь хранить чужие age
и phone_number
в одном столбце, и по той же причине вы не должны хранить как state_id
и country_id
в одной колонке. Тот факт, что эти два атрибута имеют совместимые типы данных, является совпадением; они по-прежнему означают разные логические сущности.
полиморфные Ассоциации также нарушает Третья Нормальная Форма, потому что значение столбца зависит от дополнительного столбца, который называет таблицу, на которую ссылается внешний ключ. В третьей нормальной форме атрибут таблицы должен зависеть только от первичного ключа этой таблицы.
Re комментарий от @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, будут возвращены как одна строка нулей. Но вы все равно должны hardcode все объединенные таблицы, и если вы добавляете новые таблицы фильтров, вам нужно обновить код.
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)
...
но этот формат по-прежнему требует, чтобы вы писали ссылки на все таблицы. С этим ничего не поделаешь.
Это не самое элегантное решение в мире, но вы могли бы использовать наследование конкретной таблицы чтобы сделать эту работу.
концептуально вы предлагаете понятие класса "вещей, которые могут быть популярными областями", от которых наследуются ваши три типа мест. Вы можете представить это как таблицу с именем, например,places
где каждая строка имеет отношение один к одному со строкой в regions
, countries
или states
. (Атрибуты, разделяемые между регионами, страны или государства, если таковые имеются, могут быть помещены в эту таблицу мест.) Ваш popular_place_id
тогда будет ссылка внешнего ключа на строку в таблице мест, которая затем приведет вас к региону, стране или государству.
решение, которое вы предлагаете со вторым столбцом для описания типа ассоциации, заключается в том, как Rails обрабатывает полиморфные ассоциации, но я не поклонник этого в целом. Билл подробно объясняет, почему полиморфные ассоциации не являются вашими друзьями.
вот исправление к подходу Билла Карвина "supertable", используя составной ключ ( place_type, place_id )
для устранения предполагаемых нарушений нормальной формы:
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 )
);
что этот дизайн не может гарантировать, что для каждой строки в places
существует строку states
или countries
(но не оба). Это ограничение внешних ключей в SQL. В полной СУБД, совместимой со стандартами SQL-92, вы можете определить отложенные ограничения между таблицами, которые позволят вам достичь того же, но это неуклюже, включает транзакцию, и такая СУБД еще не вышла на рынок.