Немного поздно, но все существующие ответы, похоже, не соответствуют действительности:
- Владелец веб-сайта является
1:Many
отношением
- Веб-сайт к владельцу является
1:1
отношением
- Таблицы «Пользователи» и «Компании» не должны иметь внешнего ключа в таблице «Веб-сайты».
- Никакие данные веб-сайта, общие для пользователей и компаний или нет, не должны находиться в таблицах «Пользователи» или «Компании».
- Никакая информация о владельце, общая или нет, не должна быть в таблице веб-сайтов.
- MySQL молча игнорирует
CHECK
ограничения для таблиц (без обеспечения ссылочной целостности)
- СУБД должна обрабатывать логику «отношений», а не приложение, использующее базу данных.
Частично это отражено в ответе onedaywhen, но этот ответ по-прежнему упускает возможность сделать MySQL выполняет тяжелую работу и обеспечивает ссылочную целостность.
В любом случае по закону у веб-сайта может быть только один владелец. У человека или компании может быть любое количество веб-сайтов, в том числе ни одного. Ссылка в базе от владельца на сайт может быть только 1:1
на любом уровне нормализации. На самом деле это отношение 1:Many
, и для каждого владельца, который владеет более чем одним веб-сайтом, потребуется несколько записей в таблице. Ссылка с веб-сайта на владельца 1:1
как в терминах базы данных, так и в реальности. Наличие ссылки с веб-сайта на владельца лучше представляет модель. С индексом в таблице веб-сайтов выполнение 1:Many
поиска данного владельца становится достаточно эффективным.
Атрибут CHECK
в SQL был бы отличным решением, если бы MySQL не проигнорировал его молча.
Документация по MySQL 13.1.20 Синтаксис CREATE TABLE
Предложение CHECK
анализируется, но игнорируется всеми механизмами хранения.
Функциональность MySQL предлагает два решения в качестве обходных путей для реализации поведения CHECK
и сохранения ссылочной целостности данных. Триггеры с хранимыми процедурами — это одно из них, и оно хорошо работает со всеми типами ограничений. Легче реализовать, хотя и менее универсально, использовать VIEW
с предложением WITH CHECK OPTION
, которое MySQL будет реализовывать.
Документация по MySQL 24.5 .4 Предложение View WITH CHECK OPTION
Предложение WITH CHECK OPTION
можно указать для обновляемого представления, чтобы предотвратить вставку в строки, для которых предложение WHERE
в select_statement
неверно. Это также предотвращает обновление строк, для которых условие WHERE
истинно, но обновление приведет к тому, что оно не будет истинным (другими словами, оно предотвращает обновление видимых строк до невидимых строк).
Сайт MySQLTUTORIAL дает хороший пример обоих вариантов в их Введение в SQL ПРОВЕРИТЬ ограничение учебник. (Вы должны подумать об опечатках, но в остальном хорошо.)
Обнаружив этот вопрос при попытке разрешить аналогичное взаимоисключающее разделение внешнего ключа и разработать решение с подсказками, генерируемыми ответами, кажется правильным поделиться своим решением взамен.
Рекомендуемое решение
Для минимального воздействия на существующую схему и приложение, обращающееся к данным, оставьте таблицы Users
и Companies
такими, какие они есть. Переименуйте таблицу Websites
и замените ее представлением с именем Websites
, к которому приложение сможет по-прежнему обращаться. Все старые запросы к Websites
должны по-прежнему работать, за исключением случаев, когда требуется информация о владельце. Так:
Установка
-- Keep the `Users` table about "users"
CREATE TABLE `Users` (
`id` INT SERIAL PRIMARY KEY,
`name` VARCHAR(180),
-- user_attributes
);
-- Keep the `Companies` table about "companies"
CREATE TABLE `Companies` (
`id` SERIAL PRIMARY KEY,
`name` VARCHAR(180),
-- company_attributes
);
-- Attach ownership information about the website to the website's record in the `Websites` table, renamed to `WebsitesData`
CREATE TABLE `WebsitesData` (
`id` SERIAL PRIMARY KEY,
`name` VARCHAR(255),
`is_personal` BOOL,
`owner_user` BIGINT UNSIGNED DEFAULT NULL,
`owner_company` BIGINT UNSIGNED DEFAULT NULL,
website_attributes,
FOREIGN KEY `WebsiteOwner_User` (`owner_user`)
REFERENCES `Users` (`id`)
ON DELETE RESTRICT ON UPDATE CASCADE,
FOREIGN KEY `WebsiteOwner_Company` (`owner_company`)
REFERENCES `Companies` (`id`)
ON DELETE RESTRICT ON UPDATE CASCADE,
);
-- Create a new `VIEW` with the original name of `Websites` as the gateway to the website records which can enforce the constraints you need
CREATE VIEW `Websites` AS
SELECT * FROM `WebsitesData` WHERE
(`is_personal`=TRUE AND `owner_user` IS NOT NULL AND `owner_company` IS NULL) OR
(`is_personal`=FALSE AND `owner_user` IS NULL AND `owner_company` IS NOT NULL)
WITH CHECK OPTION;
использование
-- Use the Websites VIEW for the INSERT, UPDATE, and SELECT operations as you normally would and leave the WebsitesData table in the background.
INSERT INTO `Websites` SET
`is_personal`=TRUE,
`owner_user`=$userID;
INSERT INTO `Websites` SET
`is_personal`=FALSE,
`owner_company`=$companyID;
-- Or, using different field lists based on the type of owner
INSERT INTO `Websites` (`is_personal`,`owner_user`, ...)
VALUES (TRUE, $userID, ...);
INSERT INTO `Websites` (`is_personal`,`owner_company`, ...)
VALUES (FALSE, $companyID, ...);
-- Or, using a common field list, and placing NULL in the proper place
INSERT INTO `Websites` (`is_personal`,`owner_user`,`owner_company`,...)
VALUES (TRUE, $userID, NULL, ...);
INSERT INTO `Websites` (`is_personal`,`owner_user`,`owner_company`,...)
VALUES (FALSE, NULL, $companyID, ...);
-- Change the company that owns a website
-- Will ERROR if the site was owned by a User.
UPDATE `Websites` SET `owner_company`=$new_companyID;
-- Force change the ownership from a User to a Company
UPDATE `Websites` SET
`owner_company`=$new_companyID,
`owner_user`=NULL,
`is_personal`=FALSE;
-- Force change the ownership from a Company to a User
UPDATE `Websites` SET
`owner_user`=$new_userID,
`owner_company`=NULL,
`is_personal`=TRUE;
-- Selecting the owner of a site without needing to know if it is personal or not
(SELECT `Users`.`name` AS `Owner`
FROM `Websites`
JOIN `Users` ON `Websites`.`owner_user`=`Users`.`id`
WHERE `is_personal`=TRUE AND `Websites`.`id`=$siteID)
UNION
(SELECT `Companies`.`name` AS `Owner`
FROM `Websites`
JOIN `Companies` ON `Websites`.`owner_company`=`Companies`.`id`
WHERE `is_personal`=FALSE AND `Websites`.`id`=$siteID);
-- Selecting the sites owned by a User
SELECT `name` FROM `Websites`
WHERE `is_personal`=TRUE AND `id`=$userID;
SELECT `Websites`.`name`
FROM `Websites`
JOIN `Users` ON `Websites`.`owner_user`=`Users`.$userID
WHERE `is_personal`=TRUE AND `Users`.`name`="$user_name";
-- Selecting the sites owned by a Company
SELECT `name` FROM `Websites` WHERE `is_personal`=FALSE AND `id`=$companyID;
SELECT `Websites`.`name`
FROM `Websites`
JOIN `Comnpanies` ON `Websites`.`owner_company`=`Companies`.$userID
WHERE `is_personal`=FALSE AND `Companies`.`name`="$company_name";
-- Listing all websites and their owners
(SELECT `Websites`.`name` AS `Website`,`Users`.`name` AS `Owner`
FROM `Websites`
JOIN `Users` ON `Websites`.`owner_user`=`Users`.`id`
WHERE `is_personal`=TRUE)
UNION ALL
(SELECT `Websites`.`name` AS `Website`,`Companies`.`name` AS `Owner`
FROM `Websites`
JOIN `Companies` ON `Websites`.`owner_company`=`Companies`.`id`
WHERE `is_personal`=FALSE)
ORDER BY Website, Owner;
-- Listing all users or companies which own at least one website
(SELECT `Websites`.`name` AS `Website`,`Users`.`name` AS `Owner`
FROM `Websites`
JOIN `Users` ON `Websites`.`owner_user`=`Users`.`id`
WHERE `is_personal`=TRUE)
UNION DISTINCT
(SELECT `Websites`.`name` AS `Website`,`Companies`.`name` AS `Owner`
FROM `Websites`
JOIN `Companies` ON `Websites`.`owner_company`=`Companies`.`id`
WHERE `is_personal`=FALSE)
GROUP BY `Owner` ORDER BY `Owner`;
Повышение уровня нормализации
В качестве технического примечания по нормализации информация о владельце может быть исключена из таблицы Websites и создана новая таблица для хранения данных о владельце, включая столбец is_normal.
CREATE TABLE `Websites` (
`id` SERIAL PRIMARY KEY,
`name` VARCHAR(255),
`owner` BIGINT UNSIGNED DEFAULT NULL,
website_attributes,
FOREIGN KEY `Website_Owner` (`owner`)
REFERENCES `WebOwners` (id`)
ON DELETE RESTRICT ON UPDATE CASCADE
);
CREATE TABLE `WebOwnersData` (
`id` SERIAL PRIMARY KEY,
`is_personal` BOOL,
`user` BIGINT UNSIGNED DEFAULT NULL,
`company` BIGINT UNSIGNED DEFAULT NULL,
FOREIGN KEY `WebOwners_User` (`user`)
REFERENCES `Users` (`id`)
ON DELETE RESTRICT ON UPDATE CASCADE,
FOREIGN KEY `WebOwners_Company` (`company`)
REFERENCES `Companies` (`id`)
ON DELETE RESTRICT ON UPDATE CASCADE,
);
CREATE VIEW `WebOwners` AS
SELECT * FROM WebsitesData WHERE
(`is_personal`=TRUE AND `user` IS NOT NULL AND `company` IS NULL) OR
(`is_personal`=FALSE AND `user` IS NULL AND `company` IS NOT NULL)
WITH CHECK OPTION;
Я считаю, однако, что созданное VIEW с его ограничениями предотвращает любую из аномалий, на устранение которых направлена нормализация, и добавляет сложности, которые не нужны в данной ситуации. В любом случае процесс нормализации всегда является компромиссом.
person
Chindraba
schedule
31.12.2018