避免數據庫表或字段具有多種意思。
先來看一個糟糕的設計,以下將分析由數據庫字段兩意性引發的慘案。
圖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 ,每個領域對象也不必關注與它無關的事情。也避免了由程序控制不當造成的數據不完整,后期維護會少了很多意想不到的問題。