Можна зробити зовнішній ключ MySQL до однієї з двох можливих таблиць?


180

Ну ось моя проблема: у мене три таблиці; регіони, країни, держави. Країни можуть бути всередині регіонів, держави можуть бути всередині регіонів. Регіони - це вершина харчового ланцюга.

Тепер я додаю таблицю popular_areas з двома стовпцями; region_id та popular_place_id. Чи можна зробити popular_place_id іноземним ключем для будь-яких країн АБО держав. Можливо, мені доведеться додати стовпчик popular_place_type, щоб визначити, чи ідентифікатор в будь-якому випадку описує країну чи державу.

Відповіді:


282

Те, що ви описуєте, називається Поліморфними асоціаціями. Тобто, стовпець "зовнішній ключ" містить значення 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таблицю як суперфабрикат. Як згадує Ебі, другою альтернативою є те, що ваші популярні місця посилаються на таку таблицю 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в одному стовпчику. Той факт, що ці два атрибути мають сумісні типи даних, є випадковим; вони все ще означають різні логічні сутності.

Поліморфні асоціації також порушують Третю нормальну форму , тому що значення стовпця залежить від додаткового стовпчика, який називає таблицю, на яку посилається іноземний ключ. У третій звичайній формі атрибут у таблиці повинен залежати лише від первинного ключа цієї таблиці.


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

Але цей формат все ще вимагає, щоб ви писали посилання на всі таблиці. Цього не обійти.


Який би ти запропонував Білл? Я в середині розробки бази даних, але я втрачений. В основному мені потрібно пов’язати фільтри з продуктом, і значення фільтрів будуть заповнені в різних таблицях. Але проблема полягає в тому, що фільтри будуть генеруватися адміністраторами, тому залежно від типу фільтра дані можуть змінюватися, а значить, і joinцільові показники також зміняться ...... Я занадто сильно ускладнюю чи що? Довідка!
Савас Ведова

+1 дякую за приголомшливе рішення. Одне питання, яке виникає у мене з першим / другим рішенням: чи є порушення нормалізації тим фактом, що кілька таблиць можуть посилатися на один і той же первинний ключ у цій мета-таблиці? Я знаю, що ви можете вирішити це логікою, але я не бачу ніякого способу, щоб база даних її застосувала, якщо я щось не пропускаю.
Роб

5
Мені дуже подобається підхід із "КОНСТРУКЦІЙНОЇ ПЕРЕВІРКИ". Але це можна покращити, якщо ми змінимо "АБО" на "XOR". Таким чином ми гарантуємо, що лише один стовпець із набору НЕ
НУЛЬНИЙ

1
@alex_b, так, це добре, але логічний XOR не є стандартним SQL і підтримується не всіма брендами SQL. У MySQL є, але PostgreSQL - ні. У Oracle це є, але Microsoft - до 2016 року.
Білл Карвін

1
"Ці два стовпці можуть бути NULL; насправді лише один з них повинен бути не-NULL" - це порушило б 1NF!
день, коли

10

Це не найелегантніше рішення у світі, але ви можете використати спадщину конкретного столу, щоб зробити цю роботу.

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

Рішення, яке ви пропонуєте у другому стовпчику для опису типу асоціацій, трапляється таким, як Rails обробляє поліморфні асоціації, але я взагалі не прихильник цього. Білл докладно пояснює, чому поліморфні асоціації не є вашими друзями.


1
aka "шаблон
супертипу і

Також ця стаття добре розкриває
Марко Персолілі

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


0

Я розумію, що ця нитка стара, але я побачив це, і рішення прийшло на думку, і я подумав, що викину її туди.

Регіони, країни та держави - це географічні місця, які живуть в ієрархії.

Ви можете повністю уникнути своєї проблеми, створивши таблицю домену під назвою Geo_location_type, яку ви б заповнили трьома рядами (регіон, країна, штат).

Далі, замість трьох таблиць розташування, створіть єдину таблицю географічного розташування, яка має зовнішній ключ Geo_location_type_id (щоб ви знали, чи примірник є регіоном, країною чи державою).

Моделюйте ієрархію, зробивши цю таблицю самовідсилаючою, щоб державний екземпляр тримав fKey для свого екземпляра материнської країни, який, у свою чергу, тримає fKey до свого екземпляра батьківського регіону. Екземпляри регіону міститимуть NULL у цій fKey. Це не відрізняється від того, що ви зробили б з трьома таблицями (у вас було б 1 - багато стосунків між регіоном та країною, а також між країною та державою), за винятком того, що зараз це все в одній таблиці.

Таблиця popular_user_location була б таблицею роздільної здатності між користувачем та georgraphical_location (тому багатьом користувачам могло подобатися багато місць).

Soooo…

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

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.


0

Ну, у мене є дві таблиці:

  1. пісні

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

  1. списки відтворення a) Номер списку відтворення b) Назва списку відтворення ...

і у мене є третя

  1. song_to_playlist_relation

Проблема полягає в тому, що деякі види списків відтворення мають посилання на інші списки відтворення. Але в mysql у нас немає зовнішнього ключа, пов'язаного з двома таблицями.

Моє рішення: Я поставлю третій стовпець у співвідношенні songs_to_playlist_relation. Цей стовпець буде логічним. Якщо 1 - пісня, інакше буде посилання на таблицю списку відтворення.

Так:

  1. song_to_playlist_relation

а) Плейлист_ номер (int) b) - це пісня (булева); c) відносна кількість (номер пісні або номер списку відтворення) (int) ( не зовнішній ключ до будь-якої таблиці)

 # створити пісні на столі 
    запити . додавати ( "SET SQL_MODE = NO_AUTO_VALUE_ON_ZERO;" ) 
    запити . додати ( "СТВОРИТИ ТАБЛИЦЮ 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 ЗАМОВЛЕННЯ 'Άγνωστος συνθέτης',ALBUMvarchar (255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT 'Άγνωστο άλμπουμ', YEARint (11) NOT NULL DEFAULT '33', RATINGint (11) NOT NULL DEFAULT '5', IMAGEvarchar (600) CHARACL SET utci8 , SONG PATHvarchar (500) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, SONG REPEATint (11) NOT NULL DEFAULT '0', VOLUMEfloat NOT NULL DEFAULT '1', SPEEDfloat NOT NULL DEFAULT '1') ENGINE = InnoDB DEFAULT CHARSET = utf8; " ) 
    запити . додавати ( "ALTER TABLE songsADD PRIMARY KEY ( NUMBER), ADD UNIQUE KEY POSITION( SONG POSITION), ADD UNIQUE KEY TITLE( SONG TITLE), ADD UNIQUE KEY PATH( SONG PATH);") 
    запити. додати ( "ALTER TABLE songsMODIFY NUMBERint (11) NOT 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")

Це все!

playlists_query = "SELECT s1. *, s3. *, s4. * З пісні як s1 INNER JOIN` пісні списку відтворення` як s2 ON s1.`NUMBER` = s2.`RELATIVE NUMBER` INNER JOIN `playlists` як s3 ON s3 .`NUMBER` = s2.`PLAYLIST NUMBER` INNER ПРИЄДНАЙТЕСЬ до `playlists` як s4 ON s4.`NUMBER` = s2.` RELATIVE NUMBER` ORDER BY s3.`PLAYLIST POSITION`, `s1`.`SONG POSITION`"
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.