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

StackOverflow https://stackoverflow.com/questions/441001

  •  22-07-2019
  •  | 
  •  

Вопрос

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

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

Это было полезно?

Решение

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

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


Повторный комментарий от @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.(Атрибуты, которые являются общими для регионов, стран или штатов, если таковые имеются, могут быть помещены в эту таблицу places.) Ваш popular_place_id затем это будет ссылка на внешний ключ для строки в таблице places, которая затем приведет вас к региону, стране или штату.

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

Вот исправление к «суперпопулярному» Билла Карвина. подход, используя составной ключ (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 )
);

Что этот дизайн не может гарантировать, что для каждой строки в местах существует строка в states или country (но не в обоих). Это ограничения внешних ключей в SQL. В полной СУБД, соответствующей стандартам SQL-92, вы можете определить отложенные ограничения между таблицами, которые позволят вам достичь того же, но это неуклюже, требует транзакции, и такая СУБД еще не вышла на рынок.

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

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

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

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

Смоделируйте иерархию, сделав для этой таблицы самоссылку так, чтобы экземпляр 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.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top