你所描述的是多态关联。也就是说,“外键”列包含必须存在于一组目标表之一中的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_states和popular_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),
);
这意味着要获取所有用户的受欢迎的最喜欢的地方,您需要查询这两个表。但这意味着你可以依靠数据库来强制一致性。
将位置表创建为可超级。正如Abie所说,第二种选择是,你的受欢迎的地方引用一个类似地方的表格,这是国家和国家的父母。也就是说,州和国家都有一个外键(甚至可以使这个外键也是国家和国家的主键)。
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实际上是一个具有两个意义的列:它是一个状态或一个国家。您不会将某人的年龄和他们的phone_number存储在一个列中,出于同样的原因,您不应将state_id和country_id都存储在一个列中。这两个属性具有兼容的数据类型的事实是巧合的;它们仍然表示不同的逻辑实体。
多态关联也违反Third Normal Form,因为列的含义取决于额外的列,该列命名外键引用的表。在第三个正常表单中,表中的属性必须仅依赖于该表的主键。
评论来自@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要求在写查询时指定和修复表。您不能根据在产品的各行中找到的值动态选择连接表。
唯一的另一个选择是使用外部联接连接到所有筛选表。没有匹配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)
...
但是这种格式仍然需要你写所有表的引用。没有绕过。