在数据库中模拟Tagged union的最佳方法是什么? 我在谈论这样的事情:
create table t1 {
vehicle_id INTEGER NOT NULL REFERENCES car(id) OR motor(id) -- not valid
...
}
其中vehicle_id在汽车表或电机表中是id,它会知道哪个。
(假设电动机和汽车表没有任何共同之处
答案 0 :(得分:9)
有些人使用名为Polymorphic Associations的设计来执行此操作,允许vehicle_id
包含car
或motor
表中存在的值。然后添加vehicle_type
,命名t1
中给定行引用的表。
问题是,如果执行此操作,则无法声明真正的SQL外键约束。 SQL中不支持具有多个引用目标的外键。还有其他一些问题,但缺乏参照完整性已经成为一种交易障碍。
更好的设计是从OO设计中借用car
和motor
的常见超类型的概念:
CREATE TABLE Identifiable (
id SERIAL PRIMARY KEY
);
然后让t1
引用这个超类型表:
CREATE TABLE t1 (
vehicle_id INTEGER NOT NULL,
FOREIGN KEY (vehicle_id) REFERENCES identifiable(id)
...
);
并且还使子类型引用其父类型。请注意,子类型的主键是不自动递增。父超类型负责分配新的id值,子节点只引用该值。
CREATE TABLE car (
id INTEGER NOT NULL,
FOREIGN KEY (id) REFERENCES identifiable(id)
...
);
CREATE TABLE motor (
id INTEGER NOT NULL,
FOREIGN KEY (id) REFERENCES identifiable(id)
...
);
现在,您可以拥有真正的参照完整性,但也支持具有各自属性的多个子类型表。
@Quassnoi的答案还显示了一种强制不相交的子类型的方法。也就是说,您希望阻止car
和motor
引用其父超类型表中的同一行。当我执行此操作时,我使用Identifiable.id
的单列主键,但也在UNIQUE
上声明Identifiable.(id, type)
键。 car
和motor
中的外键可以引用两列唯一键而不是主键。
答案 1 :(得分:5)
CREATE TABLE vehicle (type INT NOT NULL, id INT NOT NULL,
PRIMARY KEY (type, id)
)
CREATE TABLE car (type INT NOT NULL DEFAULT 1, id INT NOT NULL PRIMARY KEY,
CHECK(type = 1),
FOREIGN KEY (type, id) REFERENCES vehicle
)
CREATE TABLE motorcycle (type INT NOT NULL DEFAULT 2, id INT NOT NULL PRIMARY KEY,
CHECK(type = 2),
FOREIGN KEY (type, id) REFERENCES vehicle
)
CREATE TABLE t1 (
...
vehicle_type INT NOT NULL,
vehicle_id INT NOT NULL,
FOREIGN KEY (vehicle_type, vehicle_id) REFERENCES vehicle
...
)
答案 2 :(得分:3)
我认为你可以使用table inheritance in PostgreSQL来建模这样的参考。
如果你真的需要知道查询中某一行的来源,你可以使用一个简单的UNION ALL语句(这种可能与表继承无关):
SELECT car.*, 'car' table_name
UNION ALL
SELECT motor.*, 'motor' table_name
答案 3 :(得分:2)
我认为最简单的解决方案是使用 constraint
和 check
。
例如,考虑 Haskell 中的这个 ADT:
data Shape = Circle {radius::Float} | Rectangle {width::Float, height::Float}
MySQL/MariaDB 中的等效项是(在 10.5.11-MariaDB 上测试):
CREATE TABLE shape (
type ENUM('circle', 'rectangle') NOT NULL,
radius FLOAT,
width FLOAT,
height FLOAT,
CONSTRAINT constraint_circle CHECK
(type <> 'circle' OR radius IS NOT NULL),
CONSTRAINT constraint_rectangle CHECK
(type <> 'rectangle' OR (width IS NOT NULL AND height IS NOT NULL))
);
INSERT INTO shape(type, radius, width, height)
VALUES ('circle', 1, NULL, NULL); -- ok
INSERT INTO shape(type, radius, width, height)
VALUES ('circle', NULL, 1, NULL); -- error, constraint_circle violated
请注意,上面使用了 type <> x OR y
而不是 type = x AND y
。这是因为后者本质上意味着所有行都必须具有 type
的 x
,这违背了标记联合的目的。
另外,请注意上面的解决方案只检查必需的列,而不检查无关的列。
例如,您可以插入一个定义了 rectangle
的 radius
。
通过为 constraint_rectangle
添加另一个条件,即 radius is null
,可以轻松缓解这种情况。
但是,我不建议这样做,因为它会使添加新的 type
变得乏味。
例如,要添加一个新的 type
triangle
和一个新列 base
,我们不仅需要添加新的约束,还需要修改现有的约束以确保其 base
为空。