Можно ли сделать внешний ключ MySQL для одной из двух возможных таблиц?


Ну вот моя проблема у меня есть три таблицы; регионы, страны, государства. Страны могут быть внутри регионов, государства могут быть внутри регионов. Регионы являются вершиной пищевой цепи.

теперь я добавляю таблицу popular_areas с двумя столбцами; region_id и popular_place_id. Можно ли сделать popular_place_id внешним ключом для любой из стран или государства. Вероятно, мне придется добавить столбец popular_place_type для определения независимо от того, описывает ли идентификатор страну или государство в любом случае.

3 153

3 ответа:

то, что вы описываете, называется полиморфными ассоциациями. То есть столбец" внешний ключ " содержит значение идентификатора, которое должно существовать в одной из набора целевых таблиц. Обычно целевые таблицы связаны каким-либо образом, например, являются экземплярами некоторого общего суперкласса данных. Вам также понадобится другой столбец вдоль столбца внешнего ключа, чтобы в каждой строке вы могли указать, на какую целевую таблицу ссылаются.

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 таблица как супертаблица. как упоминает Аби, вторая альтернатива заключается в том, что ваши популярные места ссылаются на таблицу, такую как 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, будут просто возвращены как одна строка нулей. Но вам все равно придется писать все объединенные таблицы, и если вы добавляете новые таблицы фильтров, вы должны обновить свой код.

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 тогда будет ссылка внешнего ключа на строку в таблице places, которая затем приведет вас к региону, стране или государству.

решение, которое вы предлагаете со вторым столбцом для описания типа ассоциации, заключается в том, как 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, вы можете определить переносимые межтабличные ограничения, которые позволят вам достичь того же, но это неуклюже, включает в себя транзакцию, и такая СУБД еще не вышла на рынок.