小编典典

可以对两个可能的表之一执行 MySQL 外键吗?

all

好吧,这是我的问题,我有三张表;地区、国家、州。国家可以在区域内,州可以在区域内。地区是食物链的顶端。

现在我要添加一个包含两列的 Popular_areas 表;region_id 和 Popular_place_id。是否有可能使
popular_place_id 成为国家 州的外键。我可能必须添加一个 popular_place_type 列来确定 id
是描述一个国家还是一个州。


阅读 64

收藏
2022-07-01

共1个答案

小编典典

您所描述的称为多态关联。也就是说,“外键”列包含必须存在于一组目标表中的一个 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_statesand popular_countries,分别引用statesand
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表作为超级表。 正如 Abie 提到的,第二种选择是您的热门地点引用了一个类似
的表格places,它是states和的父级countries。也就是说,州和国家都有一个外键places(你甚至可以使这个外键也成为statesand的主键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_idcountry_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)
...

但是这种格式仍然需要您编写对所有表的引用。没有办法解决这个问题。

2022-07-01