元数据-模型-实例是一个很常见的设计成例——实际上,我觉得它应该属于解决一大类问题的设计模式的一种,在很多书里面也提到过这个东西。比如说在《面向模式的软件架构》系列里面,就提到过类似的设计模式。
这里主要讨论这种模式在数据库设计上的一些典型的做法,但是也局限于关系型数据库,对于非关系型数据库而言,设计也多有不同。在设计数据库的时候,主要分成模型表的设计和实例表的设计。
模型相关表设计
基础设计
我将模型定义为属性的集合。即一个模型通过模型所具有哪些属性来描述的,不同的属性又有不同的特征。
首先需要有一个模型表,我们暂时叫它Model。它的主要列都是描述模型自身的属性。举例来说:
比如第一条记录,其含义是有一个叫做“电脑”的模型,它适用于3C这个类目之下。
接下来是属性表(Attr):
属性表里面的记录是对属性自身的描述。举例来说,第一条记录的意思是有一个叫做memory的属性,其中文名字是内存,它的类型是数字——即实例里面该属性的取值应该是一个数字,Validator要求该属性的取值是大于0的,并且是必须具备的。也就是说,如果一个模型用到了该属性,那么任何一个实例,该属性都应该有取值,并且是大于0的。
之后,还需要一个表将模型和属性关联起来,其含义是这些模型是由这些属性组成的,而一个实例就是由这些属性的取值描述的。
关联表(Assn):
到这里,关于模型表的基本表结构就出来了。下面我们讨论一下扩展结构。
扩展设计
不论是模型表,还是属性表,在复杂业务的时候,都需要引入一些扩展结构。
首先是模型表,在Model里面,涉及的都是所有Model都需要有的公共属性——始终记得这些属性是对模型自身的描述而与实例无关。而对于一些模型来说,会有一些扩展的内容。举个例子来说,电脑这个Model要有一个Account的属性,表达仅有这个账号才能创建电脑模型的实例;而相应的汽车这个Model具有一个City的属性,表达汽车这个Model只能在这些城市使用。
于是我们可以轻易设计一个ModelExt表,用于表达这种关于模型自身的扩展属性:
同样的道理,不同的属性也会有不同的扩展内容。举例来说,轮子品牌是一个枚举量,即轮子品牌的取值是有限个预先定义的值中的一个;屏幕尺寸有一个单位的属性,即屏幕尺寸应该是XXX英寸。那么也可以通过一个属性表的扩展来实现,AttrExt:
连续的Attr只是为了强调这张表的属性,是模型属性的属性,也就是attr's attr。
这里非常重要的一点是:模型表,包括模型扩展表,存储的数据是对模型自身的描述,是将模型看成一个整体之后的描述;属性表,属性扩展表,是对组成这个模型的各个属性的描述。
实例相关表设计
基础设计
实例,是指模型的一个具体化。也就是说,模型的组成属性的各个取值能够构成一个实例。比如说,对手机模型进行实例化,我们能够得到iphone8,小米红米手机。
实例表(Instance)是第一个表:
这张表可以简单成这样,也就是只有一列ID属性和一列ModelID。后续我们会在扩展结构里面讨论这张表放置一些别的列的设计。
属性取值表AttrValue:
比如,第一列的意思就是有一台(款)电脑,内存是16G,屏幕尺寸是45英寸。
扩展设计
第一种扩展设计是扩展实例表:
注意的是,这种扩展方式,相当于默认任何模型都有两个组成属性,name和desc。其等价于:
a. 在Attr表中增加两个字段
b. 同时在Assn表中增加上相应的关联:
c. 在AttrValue里面加上name和desc的取值:
不过大多数的时候,设计都是直接在Instance表里放置模型基础属性的取值,而放弃其等价形式。所带来的后果就是当获取模型的全部属性(含自身描述属性,组成属性)时候将会缺少这一部分基础属性。
第二种变种是,在Instance实例表里面不使用ModelID,而是使用Model Name。这是可以的——因为这能够带来可读性的上升,但是要注意的就是,Model Name应该保证全局唯一那么在Model表里面,给Name列加上唯一索引,是最好的做法了。
有些人喜欢在AttrValue里面,用AssnID取代AttrID,我对此持保留意见。我认为纯粹的从语义上来说,应该使用的是AttrID,即表达在该属性下的取值。如果能够保证在Model下AttrName是唯一的,那么AttrValue里面的AttrID可以用AttrName来取代,以换取可读性的提高。
使用NoSQL
从前面的设计可以看到,我们采用了大量的key-value形式的存储结构(其标准名称应该是EAV,即Entity-Attribute-Value)。它的几个缺陷是无法在建索引,无法执行聚合运算等,查询效率低等。实际上,一种可以考虑的改进是,采用NoSQL。实际上NoSQL也不一定能够解决EAV存在的问题,只是它们看上去更加合适一点。
首要推荐的NoSQL存储方式是采用列族存储。使用列族存储的数据库,可以将表设计成宽表,成百上千个列都可以,但是每一行数据都只对其中某几个列有取值;
其次推荐的是文件式存储,如MongoDB。这一类的存储,可以将模型和实例的数据写成一种很适合人类阅读的文本形式,而后进行存储。