谈谈数据库设计——避免多义性
避免数据库表或字段具有多种意思。
先来看一个糟糕的设计,以下将分析由数据库字段两意性引发的惨案。
图1
图中两张表,表示入库申请和实际入库出库,由于入库和出库行为非常相似,所以只用了一张表并用单据类别区分入库和出库。
表面没什么问题,只要在程序中判断单据类别就能使用。
但实际开发过程当中问题远不止这些。
1.看申请单据ID
由于入库时需要申请,所以这里有个申请单据ID。但出库不需要申请,入库时有特殊情况不需要申请直接入库的,所以这两种情况是没有申请单据ID的。
怎么处理?有申请单据ID就插入ID这个没有问题,不需要申请时呢?将这列设置成允许为空,并设默认值为0。
这时看似问题解决了,其实还差一点。如果程序中出现BUG,比如连续按多次保存,将可能再入库时插入多次重复的记录。
这破坏了一张申请单据只能对应一张实际入库单据的规则。
由于出库单据的申请单据ID列都为默认值0,或者是null,所以没办法给该列添加唯一约束,结论是没办法在数据库的设计中就表现出两单据的1对1关系,必须要在程序中,所有涉及的地方都明确地判断,不然可能会造成数据的不正确性。
虽然无法保证程序中所有用到的地方都体现两张单据1对1的关系,可以退一步求其次,用另一种方法来保证这个入库操作插入时不会重复插入。
解决办法是在单据编号中建立唯一约束,这样就能解决。
另外值得一提的是,这样设计没办法建立外键约束,外键索引(Oracle和MSSQLSERVER在建立外键时会自动加上外键索引,这里不能建立外键,只能手动添加索引),因为ID=0在申请单据表中找不到相对应的意思。前面说到在程序中要明确地判断,就是要判断这个ID为0是什么意思,到底是去找申请单据ID=0的记录呢?还是表示他没有通过申请直接入库的?比如在查询所有直接入库时需要加入where 申请单据ID = 0 ,除非在申请单据表中事先存在ID=0的数据,但还是需要判断这个单据的意思是表示没有单据。
2.再看单据编号
这里的单据编号其实有两个意思,入库单据编号和出库单据编号。
前面提到给该列建立唯一约束,来保证数据的完整性。但是建立唯一约束以后,单据编号中就需要有一部分能区分入库与出库的标示,不然可能出现撞号,这将让单据编号的规则变的更加复杂。
例如,入库单据编号为20121212001,意思是2012年12月12日第一个入库的单据,填写第一个出库单据时撞上了,得给编号留一位作为标示符,比如I20121212001,O201212001。
单据编号的真正意思,取决于单据类别这个字段。
3.可维护性比较差
在用户使用了一段时间以后,觉得这个功能还可以再表现出更多信息将会更好。比如入库时填写物料的来源(供应商),出库时填写物料领取者(部门)。
程序中为领域建模时,建立的是入出库,那么在入库时将会把领取者属性设为空,这就需要人为地区分,哪些属性是入库,哪些属性属于出库。
但通常领域驱动设计会直接把他们抽象为入库和出库两个领域对象,而数据库设计时自然也会分为两张表,不然只有在领域对象中加入单据类别属性。
class 入库单据 { public char 单据类别 { get{return 'i';} } } class 出库单据 { public char 单据类别 { get{return 'o';} } }
是的,你得关注当初设置这个类别字段时i代表什么意思,o又代表什么意思,可能它是0代表入库,1代表出库,反正你得去看一下。
总结:一个糟糕的设计不但对性能造成影响,还让程序设计变得复杂。当程序员听到“又要该”时,脸色通常都会比较难看,因为在糟糕的设计下,维护的成本很高,编程就变得不那么愉快。
图2
解决了一部分问题,但是申请单据ID的问题还是没有解决。
图3
似乎是不错的设计,但是在统计总的入库单据时需要做一个视图,把两种情况的入库单据做union。
另外如果有要求入库时需要申请单据和不需要申请的单据也不能重复,那么需要在另一张表里检测单据编号是否存在再插入记录。
总结:消除了字段两意性以后,编程时就少了一些if else 或者是SQL 语句中少了些where ,每个领域对象也不必关注与它无关的事情。也避免了由程序控制不当造成的数据不完整,后期维护会少了很多意想不到的问题。