可以为两个可能的表之一做一个MySQL外键吗?

时间:2009-01-13 22:02:17

标签: mysql polymorphic-associations

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

现在我正在添加一个包含两列的popular_areas表; region_id和popular_place_id。是否可以使popular_place_id成为国家 OR 州的外键。我可能不得不添加一个popular_place_type列来确定id是否描述了一个国家或州。

5 个答案:

答案 0 :(得分:249)

您所描述的内容称为多态关联。也就是说,“外键”列包含必须存在于一组目标表之一中的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_statespopular_countries,分别引用statescountries。这些“热门”表格中的每一个也都引用了用户的个人资料。

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这样的表,这是两者的父表statescountries。也就是说,州和国家/地区也都有places的外键(您甚至可以将此外键设为statescountries的主键。)

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)
);

就关系理论而言,多态关联违反First Normal Form,因为popular_place_id实际上是一个有两个含义的列:它是一个州或一个国家。您不会将某个人的age及其phone_number存储在一个列中,出于同样的原因,您不应将state_idcountry_id存储在一个列中。这两个属性具有兼容的数据类型的事实是巧合的;它们仍然表示不同的逻辑实体。

多态关联也违反Third Normal Form,因为列的含义取决于为外键引用的表命名的额外列。在第三范式中,表中的属性必须仅依赖于该表的主键。


来自@SavasVedova的评论:

我不确定在没有看到表定义或示例查询的情况下遵循您的描述,但听起来您只是有多个Filters表,每个表都包含一个引用中心{{1}的外键}表。

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...

如果希望过滤器类型是动态的,则必须编写应用程序代码来构造SQL查询。 SQL要求在编写查询时指定并修复表。您无法根据SELECT * FROM Products INNER JOIN FiltersType2 USING (product_id) 的各个行中找到的值动态选择联接表。

唯一的另一个选择是使用外连接加入所有过滤器表。那些没有匹配的product_id的东西只会作为一行空值返回。但是你仍然必须硬编码所有连接表,如果你添加新的过滤表,你必须更新你的代码。

Products

加入所有过滤表的另一种方法是串行执行:

SELECT * FROM Products
LEFT OUTER JOIN FiltersType1 USING (product_id)
LEFT OUTER JOIN FiltersType2 USING (product_id)
LEFT OUTER JOIN FiltersType3 USING (product_id)
...

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

答案 1 :(得分:10)

这不是世界上最优雅的解决方案,但您可以使用concrete table inheritance来完成这项工作。

从概念上讲,你提出的是一类“可以成为流行区域的东西”的概念,你的三种类型的地方都会继承这种东西。您可以将此表示为一个名为places的表格,其中每一行与regionscountriesstates中的行具有一对一的关系。 (区域,国家或州之间共享的属性(如果有)可以推送到此places表中。)然后,您的popular_place_id将成为places表中行的外键引用,这将引导您到某个地区,国家或地区。

你建议使用第二列描述关联类型的解决方案恰好是Rails如何处理多态关联,但我不是一般的粉丝。 Bill详细解释了为什么多态关联不是你的朋友。

答案 2 :(得分:5)

这是对Bill Karwin的“超级”方法的修正,使用复合键( 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中的每一行都有statescountries中的行(但不是两者)。这是SQL中外键的限制。在完全符合SQL-92标准的DBMS中,您可以定义可延迟的表间约束,这些约束允许您实现相同但却很笨重,涉及事务,并且这样的DBMS尚未将其推向市场。

答案 3 :(得分:0)

我意识到这个线程很旧,但是我看到了这个想法,想到了一个解决方案,我想我应该把它扔在那里。

地区,国家和州是生活在层次结构中的地理位置。

您可以通过创建一个称为geo_location_type的域表来完全避免问题,该域表将填充三行(Region,Country,State)。

接下来,代替三个位置表,创建一个具有geo_location_type_id外键的单一geo_location表(这样您就知道实例是Region,Country还是State)。

通过使该表具有自引用性来对层次结构建模,以便State实例将fKey保留为其父Country实例,而Country实例又将fKey保留为其父Region实例。区域实例在该fKey中将保留NULL。这与您对这三个表(您将拥有1 –区域与国家之间以及国家与州之间的许多关系)所做的操作没有什么不同,除了现在全部都在一个表中。

popular_user_location表将是用户和georgraphicallocation之间的范围解析表(因此,许多用户可能喜欢很多地方)。

嘘...

enter image description here

CREATE TABLE [geographical_location_type] (
    [geographical_location_type_id] INTEGER NOT NULL,
    [name] VARCHAR(25) NOT NULL,
    CONSTRAINT [PK_geographical_location_type] PRIMARY KEY ([geographical_location_type_id])
)

-- Add 'Region', 'Country' and 'State' instances to the above table


CREATE TABLE [geographical_location] (
   [geographical_location_id] BIGINT IDENTITY(0,1) NOT NULL,
    [name] VARCHAR(1024) NOT NULL,
    [geographical_location_type_id] INTEGER NOT NULL,
    [geographical_location_parent] BIGINT,  -- self referencing; can be null for top-level instances
    CONSTRAINT [PK_geographical_location] PRIMARY KEY ([geographical_location_id])
)

CREATE TABLE [user] (
    [user_id] BIGINT NOT NULL,
    [login_id] VARCHAR(30) NOT NULL,
    [password] VARCHAR(512) NOT NULL,
    CONSTRAINT [PK_user] PRIMARY KEY ([user_id])
)


CREATE TABLE [popular_user_location] (
    [popular_user_location_id] BIGINT NOT NULL,
    [user_id] BIGINT NOT NULL,
    [geographical_location_id] BIGINT NOT NULL,
    CONSTRAINT [PK_popular_user_location] PRIMARY KEY ([popular_user_location_id])
)

ALTER TABLE [geographical_location] ADD CONSTRAINT [geographical_location_type_geographical_location] 
    FOREIGN KEY ([geographical_location_type_id]) REFERENCES [geographical_location_type] ([geographical_location_type_id])



ALTER TABLE [geographical_location] ADD CONSTRAINT [geographical_location_geographical_location] 
    FOREIGN KEY ([geographical_location_parent]) REFERENCES [geographical_location] ([geographical_location_id])



ALTER TABLE [popular_user_location] ADD CONSTRAINT [user_popular_user_location] 
    FOREIGN KEY ([user_id]) REFERENCES [user] ([user_id])



ALTER TABLE [popular_user_location] ADD CONSTRAINT [geographical_location_popular_user_location] 
    FOREIGN KEY ([geographical_location_id]) REFERENCES [geographical_location] ([geographical_location_id])

不确定目标数据库是什么;上面是MS SQL Server。

答案 4 :(得分:0)

好吧,我有两个桌子:

  1. 歌曲

a)歌曲编号 b)歌曲标题 ....

  1. 播放列表 a)播放列表号 b)播放列表标题 ...

我有第三个

  1. songs_to_playlist_relation

问题在于某些播放列表具有指向其他播放列表的链接。但是在mysql中,我们没有与两个表关联的外键。

我的解决方案:我将在songs_to_playlist_relation中放置第三列。该列将为布尔值。如果为1,则为歌曲,否则将链接至播放列表。

所以:

  1. songs_to_playlist_relation

a)播放列表编号(int) b)是歌曲(布尔值) c)相对编号(歌曲编号或播放列表编号)(int)(不是任何表的外键)

 #create table songs
    queries.append("SET SQL_MODE = NO_AUTO_VALUE_ON_ZERO;")
    queries.append("CREATE TABLE songs (NUMBER int(11) NOT NULL,SONG POSITION int(11) NOT NULL,PLAY SONG tinyint(1) NOT NULL DEFAULT '1',SONG TITLE varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,DESCRIPTION varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,ARTIST varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT 'Άγνωστος καλλιτέχνης',AUTHOR varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT 'Άγνωστος στιχουργός',COMPOSER varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT 'Άγνωστος συνθέτης',ALBUM varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT 'Άγνωστο άλμπουμ',YEAR int(11) NOT NULL DEFAULT '33',RATING int(11) NOT NULL DEFAULT '5',IMAGE varchar(600) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,SONG PATH varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,SONG REPEAT int(11) NOT NULL DEFAULT '0',VOLUME float NOT NULL DEFAULT '1',SPEED float NOT NULL DEFAULT '1') ENGINE=InnoDB DEFAULT CHARSET=utf8;")
    queries.append("ALTER TABLE songs ADD PRIMARY KEY (NUMBER), ADD UNIQUE KEY POSITION (SONG POSITION), ADD UNIQUE KEY TITLE (SONG TITLE), ADD UNIQUE KEY PATH (SONG PATH);")
    queries.append("ALTER TABLE songs MODIFY NUMBER int(11) NOT NULL AUTO_INCREMENT;")

#create table playlists
queries.append("CREATE TABLE `playlists` (`NUMBER` int(11) NOT NULL,`PLAYLIST POSITION` int(11) NOT NULL,`PLAYLIST TITLE` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,`PLAYLIST PATH` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8;")
queries.append("ALTER TABLE `playlists` ADD PRIMARY KEY (`NUMBER`),ADD UNIQUE KEY `POSITION` (`PLAYLIST POSITION`),ADD UNIQUE KEY `TITLE` (`PLAYLIST TITLE`),ADD UNIQUE KEY `PATH` (`PLAYLIST PATH`);")
queries.append("ALTER TABLE `playlists` MODIFY `NUMBER` int(11) NOT NULL AUTO_INCREMENT;")

#create table for songs to playlist relation
queries.append("CREATE TABLE `songs of playlist` (`PLAYLIST NUMBER` int(11) NOT NULL,`SONG OR PLAYLIST` tinyint(1) NOT NULL DEFAULT '1',`RELATIVE NUMBER` int(11) NOT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8;")
queries.append("ALTER TABLE `songs of playlist` ADD KEY `PLAYLIST NUMBER` (`PLAYLIST NUMBER`) USING BTREE;")
queries.append("ALTER TABLE `songs of playlist` ADD CONSTRAINT `playlist of playlist_ibfk_1` FOREIGN KEY (`PLAYLIST NUMBER`) REFERENCES `playlists` (`NUMBER`) ON DELETE RESTRICT ON UPDATE RESTRICT")

仅此而已!

[DllImport("kernel32.dll", SetLastError = true)]
static extern SafeFileHandle CreateFile(string lpFileName, uint dwDesiredAccess, FileShare dwShareMode, IntPtr securityAttrs, FileMode dwCreationDisposition, FileOptions dwFlagsAndAttributes, IntPtr hTemplateFile);