允许用户对bug记录进行评论,我们很容易设计出一个一对多的关系。但是随着需求变更,可以进行评论的可能是多张表,比如bug和feature_request都可以进行评论,就会有类似以下的关系。

1
2
3
4
5
6
7
8
-- 错误的语句,仅用于表现关系
create table comment(
comment_id serial primary key,
issue_id bigint unsigned not null,
.......
foreign key(issue_id)
reference bug(issue_id) or reference feature_request(issue_id)
);

目的:关联多个父表

    本章目的很明确,就是要处理多个父表关联同一子表的情况。

反模式:使用多用途外键

    这个解决方案已经正式命名了,那就是多态关联,或者叫杂乱关联。

定义多态关联

    为了使用多态关联,你需要多添加一列issue_type用来标识issue_id属于bug还是feature_request,如下

1
2
3
4
5
6
7
8
create table comment(
comment_id serial primary key,
issue_id bigint unsigned not null,
issue_type varchar(20), -- BUG or FEATURE_REQUEST
author bigint unsigned not null,
....
foreign key(author) reference account(account_id)
);

    你会发现使用该设计,issue_id外键没有了,因此就没有任何保障数据完整性的手段来确保issue_id是否存在其父表中。同样的也没有一个约束来保证issue_type是否为一个有效值。

使用多态关联进行查询
1
2
3
select * from comment c
left join bug b on c.issue_id = b.issue_id and c.issue_type = 'BUG'
left join feature_request f on c.issue_id = f.issue_id and c.issue_type = 'FEATURE_REQUEST'

如何识别反模式:当出现以下情况时,可能是反模式

  1. 这种标记框架可以让你将标记(或者其他属性)和数据库中的任何其他资源联系起来。就想EAV的设计一样,应该怀疑任何生成有无线扩展性的设计。
  2. 不能在数据库中声明外键。
  3. 有一列,用来说明这条记录的其他列是和什么相关的。任何外键都强制一张表中所有的行引用同一张表。

合理使用反模式:

    应该尽量避免使用多态关联,应该使用外键约束等来确保引用完整性。
    因为:多态关联通常过度依赖上层程序设计而不是数据库的元数据。

解决方案:让关系变得简单

    既要避免使用多态关联的缺点,又要支持你需要的数据模型,最好就是重新设计数据库,接下来就是要帮你解决这个难题。

反向引用

    当你看清本质时,解决方案异常简单:多态关联是一个反向关联。

创建交叉表

    comment无法创建外键,是因为多个外键引用comment表,那么我们为每个外键创建一张独立的交叉表。如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
create table bug_comment(
issue_id bigint unsigned not null,
comment_id bigint unsigned not null,
primary key (issue_id, comment_id),
foreign key (issue_id) references bug(issue_id),
foreign key (comment_id) references comment(comment_id)
);

create table feature_comment(
issue_id bigint unsigned not null,
comment_id bigint unsigned not null,
primary key (issue_id, comment_id),
foreign key (issue_id) references feature_request(issue_id),
foreign key (comment_id) references comment(comment_id)
);

    这个设计模式,元数据可以确保数据完整性。
    缺点:如果有更多的类型,你需要创建更多这样的交叉表。联结查询需要多关联一张表。

共用超级表

    创建一个基类表,并让所有的父表都继承该基类表,关系图如下
关系示意图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
create table issue(
issue_id serial primary key
);
create table bug(
issue_id bigint unsigned not null,
foreign key (issue_id) references issue(issue_id),
....
);
create table feature_request(
issue_id bigint unsigned not null,
foreign key (issue_id) references issue(issue_id),
....
);
create table comment(
comment_id serial primary key,
issue_id bigint unsigned not null,
author bigint unsigned not null,
comment_date datetime,
foreign key (issue_id) references issue(issue_id),
foreign key (author) references account(account_id)
);

    给定一个评论,你可以通过一个相对简单的查询就获取对应的bug记录或者feature记录,而不再需要在查询中包含issue表,除非你将一些属性定义在那张表,同样的,由于bug表的主键和他的祖先issue表中的值时一样的,你可以直接对bug和comment表进行关联查询。也可以对两张没有外键约束直接关联的表进行联结查询,只要对应的列的信息是可比较的即可。

1
2
3
4
select * from comment as c 
left join bug as b using(issue_id)
left join feature as f using(issue_id)
where c.comment_id = 1234

对于一个指定bug,你同样可以轻易的查出他的评论

1
2
3
select * from bug as b
join comment as c using(issue_id)
where b.issue_id = 1234;

更重要的是如果你是用了像issue这样的祖先表,就可以依赖外键来确保数据完整性。
在每一个表与表的关系中,都有一个引用表和一个被引用表。

总结

    看完这章,总感觉有点过度设计,我觉得学习以下作者思考的思路就好了,或者以后也用得上这一章的内容。