可能做一个MySQL外键到两个可能的表之一?
那么这是我的问题,我有三个表; 地区,国家,州。 国家可以在区域内部,国家可以在区域内部。 地区是食物链的顶端。
现在我要添加一个有两列的popular_areas表; region_id和popular_place_id。 是否有可能使popular_place_id成为任何国家或国家的外部关键。 我可能将不得不添加一个popular_place_type列来确定id是否描述一个国家或国家的任何一种方式。
你所描述的叫做Polymorphic Associations。 也就是说,“外键”列包含一个必须存在于一组目标表中的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约束对Polymorphic关联进行建模。 外键约束总是引用一个目标表。
多态关联由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
表作为超级。 正如Abie所提到的,第二个选择是,你的热门地点引用一个像places
一样的桌子,这是states
和countries
的父母。 也就是说,无论是国家还是国家,都有外地的钥匙(你甚至可以把这个外键也作为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
和state_id
存储在一个列中,出于同样的原因,您不应将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) ...
但是这种格式仍然要求您写入所有表的引用。 没有得到解决。
这不是世界上最优雅的解决方案,但是您可以使用具体的表继承来完成这个工作。
从概念上讲,你提出了一类“可以成为热门地区的东西”的概念,从这三类地方继承下来。 您可以将其表示为一个表格,例如,每个行与regions
, countries
或states
某一行具有一对一关系的地方。 (区域,国家或州之间共享的属性(如果有的话)可以被推入到这个位置表中)。然后,您的popular_place_id
将成为位置表中的行的外键引用,然后将引导您到区域,国家或州。
你提出的解决方案,第二列描述的关联类型恰好是Rails如何处理多态关联,但我不是一般的粉丝。 比尔详细解释了为什么多态协会不是你的朋友。
这是对比尔·卡尔文(Bill Karwin)的“可超越” ( place_type, place_id )
方法的修正,使用复合键( 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 ) );
这种设计不能确保在每个places
的行中都存在一个states
或countries
(但不是两个)的行。 这是SQL中外键的限制。 在完全符合SQL-92标准的DBMS中,您可以定义可延迟的表间约束,这将允许您实现相同的但却笨重的交易,而这样的DBMS尚未将其推向市场。