元數(shù)據(jù)-模型-實例是一個很常見的設(shè)計成例——實際上,我覺得它應(yīng)該屬于解決一大類問題的設(shè)計模式的一種,在很多書里面也提到過這個東西。比如說在《面向模式的軟件架構(gòu)》系列里面,就提到過類似的設(shè)計模式。
這里主要討論這種模式在數(shù)據(jù)庫設(shè)計上的一些典型的做法,但是也局限于關(guān)系型數(shù)據(jù)庫,對于非關(guān)系型數(shù)據(jù)庫而言,設(shè)計也多有不同。在設(shè)計數(shù)據(jù)庫的時候,主要分成模型表的設(shè)計和實例表的設(shè)計。
模型相關(guān)表設(shè)計
基礎(chǔ)設(shè)計
我將模型定義為屬性的集合。即一個模型通過模型所具有哪些屬性來描述的,不同的屬性又有不同的特征。
首先需要有一個模型表,我們暫時叫它Model。它的主要列都是描述模型自身的屬性。舉例來說:

比如第一條記錄,其含義是有一個叫做“電腦”的模型,它適用于3C這個類目之下。
接下來是屬性表(Attr):

屬性表里面的記錄是對屬性自身的描述。舉例來說,第一條記錄的意思是有一個叫做memory的屬性,其中文名字是內(nèi)存,它的類型是數(shù)字——即實例里面該屬性的取值應(yīng)該是一個數(shù)字,Validator要求該屬性的取值是大于0的,并且是必須具備的。也就是說,如果一個模型用到了該屬性,那么任何一個實例,該屬性都應(yīng)該有取值,并且是大于0的。
之后,還需要一個表將模型和屬性關(guān)聯(lián)起來,其含義是這些模型是由這些屬性組成的,而一個實例就是由這些屬性的取值描述的。
關(guān)聯(lián)表(Assn):

到這里,關(guān)于模型表的基本表結(jié)構(gòu)就出來了。下面我們討論一下擴展結(jié)構(gòu)。
擴展設(shè)計
不論是模型表,還是屬性表,在復雜業(yè)務(wù)的時候,都需要引入一些擴展結(jié)構(gòu)。
首先是模型表,在Model里面,涉及的都是所有Model都需要有的公共屬性——始終記得這些屬性是對模型自身的描述而與實例無關(guān)。而對于一些模型來說,會有一些擴展的內(nèi)容。舉個例子來說,電腦這個Model要有一個Account的屬性,表達僅有這個賬號才能創(chuàng)建電腦模型的實例;而相應(yīng)的汽車這個Model具有一個City的屬性,表達汽車這個Model只能在這些城市使用。
于是我們可以輕易設(shè)計一個ModelExt表,用于表達這種關(guān)于模型自身的擴展屬性:

同樣的道理,不同的屬性也會有不同的擴展內(nèi)容。舉例來說,輪子品牌是一個枚舉量,即輪子品牌的取值是有限個預先定義的值中的一個;屏幕尺寸有一個單位的屬性,即屏幕尺寸應(yīng)該是XXX英寸。那么也可以通過一個屬性表的擴展來實現(xiàn),AttrExt:

連續(xù)的Attr只是為了強調(diào)這張表的屬性,是模型屬性的屬性,也就是attr's attr。
這里非常重要的一點是:模型表,包括模型擴展表,存儲的數(shù)據(jù)是對模型自身的描述,是將模型看成一個整體之后的描述;屬性表,屬性擴展表,是對組成這個模型的各個屬性的描述。
實例相關(guān)表設(shè)計
基礎(chǔ)設(shè)計
實例,是指模型的一個具體化。也就是說,模型的組成屬性的各個取值能夠構(gòu)成一個實例。比如說,對手機模型進行實例化,我們能夠得到iphone8,小米紅米手機。
實例表(Instance)是第一個表:

這張表可以簡單成這樣,也就是只有一列ID屬性和一列ModelID。后續(xù)我們會在擴展結(jié)構(gòu)里面討論這張表放置一些別的列的設(shè)計。
屬性取值表AttrValue:

比如,第一列的意思就是有一臺(款)電腦,內(nèi)存是16G,屏幕尺寸是45英寸。
擴展設(shè)計
第一種擴展設(shè)計是擴展實例表:

注意的是,這種擴展方式,相當于默認任何模型都有兩個組成屬性,name和desc。其等價于:
a. 在Attr表中增加兩個字段

b. 同時在Assn表中增加上相應(yīng)的關(guān)聯(lián):

c. 在AttrValue里面加上name和desc的取值:

不過大多數(shù)的時候,設(shè)計都是直接在Instance表里放置模型基礎(chǔ)屬性的取值,而放棄其等價形式。所帶來的后果就是當獲取模型的全部屬性(含自身描述屬性,組成屬性)時候?qū)鄙龠@一部分基礎(chǔ)屬性。
第二種變種是,在Instance實例表里面不使用ModelID,而是使用Model Name。這是可以的——因為這能夠帶來可讀性的上升,但是要注意的就是,Model Name應(yīng)該保證全局唯一那么在Model表里面,給Name列加上唯一索引,是最好的做法了。
有些人喜歡在AttrValue里面,用AssnID取代AttrID,我對此持保留意見。我認為純粹的從語義上來說,應(yīng)該使用的是AttrID,即表達在該屬性下的取值。如果能夠保證在Model下AttrName是唯一的,那么AttrValue里面的AttrID可以用AttrName來取代,以換取可讀性的提高。
使用NoSQL
從前面的設(shè)計可以看到,我們采用了大量的key-value形式的存儲結(jié)構(gòu)(其標準名稱應(yīng)該是EAV,即Entity-Attribute-Value)。它的幾個缺陷是無法在建索引,無法執(zhí)行聚合運算等,查詢效率低等。實際上,一種可以考慮的改進是,采用NoSQL。實際上NoSQL也不一定能夠解決EAV存在的問題,只是它們看上去更加合適一點。
首要推薦的NoSQL存儲方式是采用列族存儲。使用列族存儲的數(shù)據(jù)庫,可以將表設(shè)計成寬表,成百上千個列都可以,但是每一行數(shù)據(jù)都只對其中某幾個列有取值;
其次推薦的是文件式存儲,如MongoDB。這一類的存儲,可以將模型和實例的數(shù)據(jù)寫成一種很適合人類閱讀的文本形式,而后進行存儲。