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

180

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

Теперь я добавляю таблицу Popular_areas с двумя столбцами; region_id и Popular_place_id. Можно ли сделать Popular_place_id внешним ключом для обеих стран или государств. Мне, вероятно, придется добавить столбец Popular_place_type, чтобы определить, описывает ли идентификатор страну или штат в любом случае.

Эндрю Джонсон
источник

Ответы:

282

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

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

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

Но этот формат все еще требует, чтобы вы писали ссылки на все таблицы. Там нет обойти это.

Билл Карвин
источник
Какой из них вы бы предложили Биллу? Я нахожусь в процессе разработки базы данных, но я потерян. Мне в основном нужно связать фильтры с продуктом, и значения фильтров будут заполнены в разных таблицах. Но проблема в том, что администраторы будут генерировать фильтры, поэтому в зависимости от типа фильтра данные могут различаться, и, следовательно, joinцель также будет меняться ...... Я слишком усложняю или как? Помогите!
Савас Ведова
+1 спасибо за отличное решение. Один вопрос, который у меня возник в связи с первым / вторым решением: есть ли какое-либо нарушение нормализации в связи с тем, что несколько таблиц могут ссылаться на один и тот же первичный ключ в этой мета-таблице? Я знаю, что вы можете решить это с помощью логики, но я не вижу возможности для базы данных обеспечить ее выполнение, если я что-то упустил.
Роб
5
Мне очень нравится подход с "КОНСТРАЙНТ-ПРОВЕРКА". Но это может быть улучшено, если мы изменим «ИЛИ» на «XOR». Таким образом мы гарантируем, что только один столбец из набора НЕ НЕДЕЙСТВИТЕЛЕН
alex_b
1
@alex_b, да, это хорошо, но логический XOR не является стандартным SQL и поддерживается не всеми брендами SQL. В MySQL это есть, а в PostgreSQL - нет. У Oracle это есть, а у Microsoft нет до 2016 года. И так далее.
Билл Карвин
1
«Эти два столбца может быть NULL, на самом деле только один из них должен быть не-NULL» - это будет нарушать 1nf!
понедельник,
10

Это не самое элегантное решение в мире, но вы можете использовать конкретное наследование таблиц, чтобы сделать эту работу.

Концептуально вы предлагаете понятие класса «вещей, которые могут быть популярными областями», от которых наследуются ваши три типа мест. Можно представить это в виде таблицы под названием, например, placesгде каждая строка имеет отношение один к одному с подряд в regions, countriesили states. (Атрибуты, которые являются общими для регионов, стран или штатов, если таковые имеются, могут быть помещены в эту таблицу мест.) popular_place_idТогда вы будете ссылаться на внешний ключ для строки в таблице мест, которая затем приведет вас к региону, стране или гос.

Решение, которое вы предлагаете со вторым столбцом для описания типа ассоциации, заключается в том, как Rails обрабатывает полиморфные ассоциации, но я вообще не фанат этого. Билл очень подробно объясняет, почему полиморфные ассоциации не твои друзья.

Abie
источник
1
ака "шаблон подтипа супертипа"
ErikE
Также эта статья хорошо объясняет концепцию duhallowgreygeek.com/polymorphic-association-bad-sql-smell
Марко
5

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

onedaywhen
источник
0

Я понимаю, что эта ветка старая, но я увидела это, и решение пришло мне на ум, и я решила выбросить его туда

Регионы, страны и государства - это географические местоположения, которые живут в иерархии.

Вы могли бы полностью избежать вашей проблемы, создав таблицу домена с именем geometry_location_type, которую вы бы заполнили тремя строками (Регион, Страна, Штат).

Затем, вместо трех таблиц местоположения, создайте одну таблицу географического расположения, которая имеет внешний ключ географического_ географического_ местоположения (так что вы знаете, является ли экземпляр регионом, страной или государством).

Смоделируйте иерархию, сделав эту таблицу самоссылочной, чтобы экземпляр State удерживал fKey в своем родительском экземпляре Country, который, в свою очередь, удерживает fKey в своем родительском экземпляре Region. Экземпляры региона будут содержать значение NULL в этом ключе. Это ничем не отличается от того, что вы сделали бы с тремя таблицами (у вас было бы 1 - много связей между регионом и страной, а также между страной и государством), за исключением того, что теперь все это в одной таблице.

Таблица Popular_user_Location будет таблицей разрешения области действия между пользователем и georgraphical_location (так многим пользователям может понравиться много мест).

Оооочень…

введите описание изображения здесь

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

Не был уверен, какова была целевая БД; выше MS SQL Server.

Toolsmythe
источник
0

Ну, у меня есть две таблицы:

  1. песни

а) номер песни б) название песни ....

  1. плейлисты а) номер плейлиста б) название плейлиста ...

а у меня третий

  1. songs_to_playlist_relation

Проблема в том, что некоторые виды плейлистов имеют ссылки на другие плейлисты. Но в mysql у нас нет внешнего ключа, связанного с двумя таблицами.

Мое решение: я помещу третий столбец в songs_to_playlist_relation. Этот столбец будет логическим. Если 1, то песня, иначе будет ссылка на таблицу списка воспроизведения.

Так:

  1. songs_to_playlist_relation

a) Playlist_number (int) b) Is song (логическое значение) c) Относительный номер (номер песни или номер списка воспроизведения) (int) ( не внешний ключ для какой-либо таблицы)

 # создать таблицу  песни
    запросы . добавить ( "SET SQL_MODE = NO_AUTO_VALUE_ON_ZERO;" ) 
    запросы . 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 COLLATE utf8_general_ci NOT NULL, DESCRIPTIONvarchar (1000) CHARACTER SET utf8 КОПИЯМ utf8_general_ci NOT NULL, ARTISTVARCHAR (255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT 'Άγνωστος καλλιτέχνης', AUTHORVARCHAR (255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT 'Άγνωστος στιχουργός', COMPOSERVARCHAR (255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT 'Άγνωστος συνθέτης',ALBUMvarchar (255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT 'Άγνωστο άλμπουμ', YEARint (11) NOT NULL DEFAULT '33', RATINGint (11) НЕ NULL ПО УМОЛЧАНИЮ '5', В IMAGEНАЛИЧИИ УСТАНОВЛЕНО (600) ОБОЗНАЧЕНИЕ , SONG PATHVARCHAR (500) CHARACTER SET utf8 СОЪЪАТЕ utf8_general_ci NOT NULL, то SONG REPEATINT (11) NOT NULL DEFAULT '0', VOLUMEвсплывают NOT NULL DEFAULT '1', SPEEDвсплывают NOT NULL DEFAULT '1') ДВИГАТЕЛЬ = InnoDB УМОЛЧАНИЮ CHARSET = utf8;» ) 
    запросы . 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);") 
    запросы, ( «ИЗМЕНИТЬ ТАБЛИЦУ ИЗМЕНИТЬappend int (11) NOT NULL AUTO_INCREMENT;"songsNUMBER)

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

Вот и все!

playlists_query = "ВЫБРАТЬ s1. *, s3. *, s4. * ИЗ песен как s1 INNER JOIN` песни из playlist` как s2 ON s1.`NUMBER` = s2.`RELATIVE NUMBER` INNER JOIN `playlists` как s3 ON s3 .`NUMBER` = s2.`PLAYLIST NUMBER` INNER Присоединяйтесь к `плейлистам` как s4 ON s4.`NUMBER` = s2.`RELATIVE NUMBER` ЗАКАЗАТЬ по s3.`PLAYLIST POSITION`,` s1``SONG POSITION` "
Крис П
источник