如何模拟数据库中的标记联合?

时间:2009-11-13 17:15:16

标签: mysql database sqlite postgresql

在数据库中模拟Tagged union的最佳方法是什么? 我在谈论这样的事情:

create table t1 {
  vehicle_id INTEGER NOT NULL REFERENCES car(id) OR motor(id) -- not valid
  ...
}

其中vehicle_id在汽车表或电机表中是id,它会知道哪个。

(假设电动机和汽车表没有任何共同之处

4 个答案:

答案 0 :(得分:9)

有些人使用名为Polymorphic Associations的设计来执行此操作,允许vehicle_id包含carmotor表中存在的值。然后添加vehicle_type,命名t1中给定行引用的表。

问题是,如果执行此操作,则无法声明真正的SQL外键约束。 SQL中不支持具有多个引用目标的外键。还有其他一些问题,但缺乏参照完整性已经成为一种交易障碍。

更好的设计是从OO设计中借用carmotor常见超类型的概念:

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的答案还显示了一种强制不相交的子类型的方法。也就是说,您希望阻止carmotor引用其父超类型表中的同一行。当我执行此操作时,我使用Identifiable.id的单列主键,但也在UNIQUE上声明Identifiable.(id, type)键。 carmotor中的外键可以引用两列唯一键而不是主键。

答案 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)

我认为最简单的解决方案是使用 constraintcheck

例如,考虑 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。这是因为后者本质上意味着所有行都必须具有 typex,这违背了标记联合的目的。

另外,请注意上面的解决方案只检查必需的列,而不检查无关的列。

例如,您可以插入一个定义了 rectangleradius

通过为 constraint_rectangle 添加另一个条件,即 radius is null,可以轻松缓解这种情况。

但是,我不建议这样做,因为它会使添加新的 type 变得乏味。

例如,要添加一个新的 type triangle 和一个新列 base,我们不仅需要添加新的约束,还需要修改现有的约束以确保其 base 为空。