SQL中,两个表可以相互引用吗?

2021-11-20 00:00:00 database mysql database-design

在这个系统中,我们存储产品、产品图像(一个产品可以有多个图像)和一个产品的默认图像.数据库:

创建表`产品`(`ID` int(10) 无符号 NOT NULL AUTO_INCREMENT,`NAME` varchar(255) 非空,`DESCRIPTION` 文本不为空,`已启用`tinyint(1) NOT NULL 默认值 '1',`DATEADDED` 日期时间非空,`DEFAULT_PICTURE_ID` int(10) 无符号默认 NULL,主键(`ID`),KEY`Index_2`(`DATEADDED`),密钥`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;创建表`products_pictures`(`ID` int(10) 无符号 NOT NULL AUTO_INCREMENT,`IMG_PATH` varchar(255) 非空,`PRODUCT_ID` int(10) 无符号 NOT NULL,主键(`ID`),KEY`FK_products_pictures_1`(`PRODUCT_ID`),CONSTRAINT `FK_products_pictures_1` FOREIGN KEY (`PRODUCT_ID`) REFERENCES `products` (`ID`) 删除级联) ENGINE=InnoDB AUTO_INCREMENT=20 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;

如您所见,products_pictures.PRODUCT_ID ->products.IDproducts.DEFAULT_PICTURE_ID ->products_pictures.ID,所以循环参考.可以吗?

解决方案

不,这不行.表之间的循环引用很乱.请参阅这篇(十年前)文章:SQL 设计:循环参考>

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


选项 1

作为您的设计,使两个 FK 之一可以为空.这使您可以解决先有鸡还是先有蛋的问题(我应该先插入哪个表?).

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

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

CONSTRAINT FK_products_1外键(id,default_picture_id)REFERENCES products_pictures (product_id, id)ON DELETE RESTRICT --- SET NULL 选项将ON UPDATE RESTRICT --- 导致其他问题

这将需要 (product_id, id)products_pictures 中的 UNIQUE 约束/索引才能定义和工作上述 FK正确.


选项 2

另一种方法是从 product 表中删除 Default_Picture_ID 列,并在 picture 表中添加一个 IsDefault BIT 列代码>表.这个解决方案的问题是如何让每个产品只允许一张图片有那个位,而所有其他人都关闭它.在 SQL-Server 中(我认为在 Postgres 中)这可以通过部分索引来完成:

创建唯一索引 is_DefaultPictureON products_pictures (Product_ID)哪里是默认值 = 1 ;

但是 MySQL 没有这样的功能.


选项 3

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

MySQL 中的约束不能延迟.


选项 4

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

product_default_picture---------------product_id 不为空default_picture_id 不为空主键 (product_id)外键(product_id,default_picture_id)REFERENCES products_pictures (product_id, id)

这还需要在 (product_id, id)products_pictures 中的 UNIQUE 约束/索引,如解决方案 1 所示.


总而言之,对于 MySQL,您有两种选择:

  • 选项 1(一个可为空的 FK 列)和上面的更正以正确执行完整性

  • 选项 4(不可为空的 FK 列)

In this system, we store products, images of products (there can be many image for a product), and a default image for a product. The database:

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;

as you can see, products_pictures.PRODUCT_ID -> products.ID and products.DEFAULT_PICTURE_ID -> products_pictures.ID, so a cycle reference. Is it OK?

解决方案

No, it's not OK. Circular references between tables are messy. See this (decade old) article: SQL By Design: The Circular Reference

Some DBMS can handle these, and with special care, but MySQL will have issues.


Option 1

As your design, to make one of the two FKs nullable. This allows you to solve the chicken-and-egg problem (which table should I first Insert into?).

There is a problem though with your code. It will allow a product to have a default picture where that picture will be referencing another product!

To disallow such an error, your FK constraint should be:

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

This will require a UNIQUE constraint/index in table products_pictures on (product_id, id) for the above FK to be defined and work properly.


Option 2

Another approach is to remove the Default_Picture_ID column form the product table and add an IsDefault BIT column in the picture table. The problem with this solution is how to allow only one picture per product to have that bit on and all others to have it off. In SQL-Server (and I think in Postgres) this can be done with a partial index:

CREATE UNIQUE INDEX is_DefaultPicture 
  ON products_pictures (Product_ID)
  WHERE IsDefault = 1 ;

But MySQL has no such feature.


Option 3

This approach, allows you to even have both FK columns defined as NOT NULL is to use deferrable constraints. This works in PostgreSQL and I think in Oracle. Check this question and the answer by @Erwin: Complex foreign key constraint in SQLAlchemy (the All key columns NOT NULL Part).

Constraints in MySQL cannot be deferrable.


Option 4

The approach (which I find cleanest) is to remove the Default_Picture_ID column and add another table. No circular path in the FK constraints and all FK columns will be NOT NULL with this solution:

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)

This will also require a UNIQUE constraint/index in table products_pictures on (product_id, id) as in solution 1.


To summarize, with MySQL you have two options:

  • option 1 (a nullable FK column) with the correction above to enforce integrity correctly

  • option 4 (no nullable FK columns)

相关文章