在SQL中,两个表是否可以相互引用?

Joh*_*ith 32 mysql database database-design

在此系统中,我们存储产品,产品图像(产品可能有许多图像),以及产品的默认图像.数据库:

CREATE TABLE  `products` (
  `ID` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `NAME` varchar(255) NOT NULL,
  `DESCRIPTION` text NOT NULL,
  `ENABLED` tinyint(1) NOT NULL DEFAULT '1',
  `DATEADDED` datetime NOT NULL,
  `DEFAULT_PICTURE_ID` int(10) unsigned DEFAULT NULL,
  PRIMARY KEY (`ID`),
  KEY `Index_2` (`DATEADDED`),
  KEY `FK_products_1` (`DEFAULT_PICTURE_ID`),
  CONSTRAINT `FK_products_1` FOREIGN KEY (`DEFAULT_PICTURE_ID`) REFERENCES `products_pictures` (`ID`) ON DELETE SET NULL ON UPDATE SET NULL
) ENGINE=InnoDB AUTO_INCREMENT=30 DEFAULT CHARSET=utf8;


CREATE TABLE  `products_pictures` (
  `ID` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `IMG_PATH` varchar(255) NOT NULL,
  `PRODUCT_ID` int(10) unsigned NOT NULL,
  PRIMARY KEY (`ID`),
  KEY `FK_products_pictures_1` (`PRODUCT_ID`),
  CONSTRAINT `FK_products_pictures_1` FOREIGN KEY (`PRODUCT_ID`) REFERENCES `products` (`ID`) ON DELETE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=20 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
Run Code Online (Sandbox Code Playgroud)

你可以看到,products_pictures.PRODUCT_ID -> products.ID并且products.DEFAULT_PICTURE_ID -> products_pictures.ID,这样一个循环引用.可以吗?

ype*_*eᵀᴹ 45

不,不行.表之间的循环引用很混乱.请参阅这篇(十年前的)文章:SQL By Design:The Circular Reference

有些DBMS可以处理这些问题,并且需要特别小心,但MySQL会遇到问题.


作为您的设计,第一个选择是使两个FK中的一个可以为空.这可以让你解决鸡蛋问题(我应该先将哪个表插入?).

您的代码存在问题.它将允许产品具有默认图片,其中该图片将引用另一个产品!

要禁止此类错误,您的FK约束应为:

CONSTRAINT FK_products_1 
  FOREIGN KEY (id, default_picture_id) 
  REFERENCES products_pictures (product_id, id)
  ON DELETE RESTRICT                            --- the SET NULL options would 
  ON UPDATE RESTRICT                            --- lead to other issues
Run Code Online (Sandbox Code Playgroud)

这将需要一个UNIQUE约束/表的索引products_pictures(product_id, id)的被定义上述FK和正常工作.


另一种方法是Default_Picture_IDproduct表中删除列,并在表中添加一IsDefault BITpicture.这个解决方案的问题是如何只允许每个产品一张图片打开,其他所有产品都关闭.在SQL-Server中(我认为在Postgres中)可以使用部分索引来完成:

CREATE UNIQUE INDEX is_DefaultPicture 
  ON products_pictures (Product_ID)
  WHERE IsDefault = 1 ;
Run Code Online (Sandbox Code Playgroud)

但MySQL没有这样的功能.


第三种方法,它允许您甚至将两个FK列定义为NOT NULL使用可延迟约束.这适用于PostgreSQL,我想在Oracle中.通过@Erwin检查这个问题和答案:SQLAlchemy中的复杂外键约束(所有键列NOT NULL部分).

MySQL中的约束无法推迟.


第四种方法(我认为最干净)是删除Default_Picture_ID列并添加另一个表.FK约束中没有圆形路径,所有FK列都将NOT NULL使用此解决方案:

product_default_picture
----------------------
product_id          NOT NULL
default_picture_id  NOT NULL
PRIMARY KEY (product_id)
FOREIGN KEY (product_id, default_picture_id)
  REFERENCES products_pictures (product_id, id)
Run Code Online (Sandbox Code Playgroud)

这也将需要UNIQUE在表约束/索引products_pictures(product_id, id)在溶液中1.


总而言之,使用MySQL,您有两种选择:

  • 选项1(可空的FK列),具有上述更正,以正确执行完整性

  • 选项4(没有可空的FK列)

  • 这是一个很好的概述 (+1),但我看不出选项 4 比选项 1 有什么优势。使用选项 1,有缺陷的应用程序代码可以通过创建产品而创建无效数据,而从不为其提供 `default_picture_id`。使用选项 4,有缺陷的应用程序代码可以通过创建产品而不为它创建 `product_default_picture` 行来类似地创建无效数据。但是,选择产品及其默认图片现在需要更多的连接,并且架构的自文档化程度较低。与抵消额外复杂性的初始方法相比,我们获得了什么好处? (3认同)
  • @MarkAmery还有一个加入不是问题,是吗?当需要为默认图片存储更多属性时,会出现第4种解决方案的优势.这也可以在第一个解决方案中使用,可以使用可为空的列,但在这种情况下,MySQL无法强制执行`CHECK`约束(以确保所有这些额外属性都是NULL或NOT NULL). (3认同)
  • 第一个解决方案也有一些复杂性问题。行的插入方式要注意(先插入一个产品,然后插入它的图片,然后更新产品以指向默认图片。)当一个人想要更改默认图片或想要删除一个产品。 (2认同)
  • @MarkAmery 如果使用“无效数据”,您的意思是选项(1 和 4)不强制所有产品都有默认图片,您 100% 正确,他们没有。他们不能。我的回答中没有提到这一点。我认为这只能在选项 3 中完成,具有可延迟约束和不可为空的列。在所有其他选项中,仅使用 DDL 无法完成。 (2认同)
  • @ypercube 是的,您已经正确理解了我所说的“无效数据”的含义。我意识到在 MySQL 中没有干净的解决方案。我不确定第一个解决方案是否有第四个没有的复杂性问题;您已经列出了创建产品所需的 3 个步骤,但第四个解决方案下的过程几乎相同(唯一的区别是插入 product_default_picture 行而不是更新产品行),我想这是我的主要观点。+1 对您的第一条评论,尽管想要额外的 NOT NULL 列以及有关关系的信息。 (2认同)
  • @MarkAmery 也许“复杂性问题”不是最好的描述。尽管如此,我认为删除会很混乱。我想这就是为什么我说第 4 个是*“最干净的”*,要插入,你需要 3 个插入。要进行删除,您需要删除(在 3 个表中或在 1 个带有级联的表中。)对我来说似乎最干净。 (2认同)
  • 那么,如果我选择第一种方法,我是否可以期望“FK_products_1”上的“ON DELETE CASCADE”规则起作用,还是必须在删除产品之前明确删除默认图片? (2认同)
  • @RonInbar 我认为这取决于 DBMS。我们在谈论 MySQL 吗? (2认同)
  • @ypercubeᵀᴹ 目前,我正在使用 ADO.NET 断开连接的数据集。我没有意识到它可能取决于 DBMS。 (2认同)
  • 无论如何,如果您希望在删除产品时也删除其所有图片,那么重要的是`FK_products_pictures_1` FK(并且应该是`ON DELETE CASCADE`)。 (2认同)
  • 当您删除默认图片时,`FK_products_1` 很重要(我的建议 1 是使用“ON DELETE RESTRICT”)。 (2认同)
  • 但是对于 SQL Server,您也可以使用建议 2。它可能更简洁。 (2认同)
  • ypercubeᵀᴹ 确实,这就是我想要的;但是,当我尝试删除父行时,我收到此错误消息:“无法删除此行,因为对关系`FK_[DefaultChild]_[Parent]` 强制执行约束,删除此行将搁置子行。” 我没有使用 SQL Server;只是一个保存为 XML 的数据集。 (2认同)
  • @ypercubeᵀᴹ抱歉,我现在看到当我尝试删除默认子项而不是父项时会发生这种情况,因此它与“On Delete Cascade”规则无关。 (2认同)
  • 让我们[在聊天中继续讨论](http://chat.stackoverflow.com/rooms/146302/discussion-between-ron-inbar-and-ypercube)。 (2认同)