文檔版本管理系統 數據表設計
最近一個朋友接手了一個項目,為自己部門開發文檔版本管理系統。我在和他閑聊中,聽他說起數據表設計時遇到的一個疑惑。聽他說完后感覺這樣的問題還是有一些普遍性的,在這里進行一下分享。
問題描述
文檔版本管理最主要的是要維護文檔的版本鏈。這很容易讓人想到鏈表這種數據結構,所以我的那位朋友很快就給出了如下的表結構:
create table Table_Docunment ( Docunment_Id int not null identity(1000, 1) primary key, Docunment_Name nvarchar(64) not null, Docunment_SubmitDate datetime not null, Docunment_PreId int not null default(-1), Docunment_NxtId int not null default(-1), ....... );
其中Docunment_PreId存放前一版本文檔的Id,Docunment_NxtId存放下一版本文檔的Id。
起初還沒有感覺出什么問題,但當他試圖向Table_Docunment填充測試數據或試圖寫一個存儲過程來獲取其鏈上最新的文檔時,感覺非常痛苦。
在他的存儲過程中需要進行循環,他自己知道這樣性能不好,但又不清楚如何解決。
解決方案
問題的關鍵在于初始的設計并不適合系統的使用場景。原始的設計導致了取某文檔的下一版本或上一版本都需要進行連接(Join)操作,若要獲得最新版本文檔,還需要進行循環。
對原始設計進行修改,新的表結構如下(省略了部分字段):
create table Table_Docunment ( Docunment_Id int not null identity(1000, 1) primary key, Docunment_Name nvarchar(64) not null, Docunment_ChainId int not null default(-1), Docunment_VersionId int not null default(-1), Docunment_SubmitDate datetime not null, ...... );
其中Docunment_ChainId為當前文檔所屬的版本鏈,Docunment_VersionId為當前文檔在版本鏈中的版本號(從1開始的連續編號)。
在列(Docunment_ChainId, Docunment_VersionId)上可加unique約束。
舉例來說,有如下兩條文檔鏈:
(圖1)
其中文檔上方的兩個數字分別代表文檔編號(Docunment_Id)和版本編號(Docunment_VersionId)。把這些信息存儲到新的Table_Docunment中,結果如下:
Docunment_Id | Docunment_Name | Docunment_ChainId | Docunment_VersionId | Docunment_SubmitDate | …… |
1000 | aaa | 1 | 1 | 2010-01-01 12:03:00 | …… |
1001 | bbb | 1 | 2 | 2010-01-02 06:02:00 | …… |
1002 | ccc | 2 | 1 | …… | …… |
1003 | …… | 1 | 3 | …… | …… |
1004 | …… | 1 | 4 | …… | …… |
1005 | …… | 2 | 2 | …… | …… |
1006 | …… | 2 | 3 | …… | …… |
對于給定的一個文檔,要找其上一版本或下一版本的文檔時,只要找其同一條鏈上版本號小1或大1的文檔。若要找最新版本文檔只要在鏈上對版本號取max就行了,也很方便。
新的需求
這樣的設計已基本滿足我那位朋友的需求了,但在某些使用場景下,情況可能會更復雜些。
若文檔鏈有文檔歸并的情況,即兩個文檔鏈的最新文檔版本是同一個文檔,示意圖如下:
(圖2)
對于這個新的需求,先前的設計就會有一些問題,對于圖中文檔1007,其版本號對于鏈1應為5,對于鏈2應為4,實在是沒辦法填,我先用了一個問號。
新的需求改變了鏈和文檔之間的關系。原先鏈和文檔之間為1對多關系(注:標準情況下1對多關系會有兩張表,但由于鏈在此系統中是一個虛概念,而且鏈實體也只會包含一個Id列,所以在先前設計中省去),現在鏈和文檔之間變為多對多關系。多對多關系需要3張表,兩個實體表,一個關系表。在此系統中鏈的實體表可以省去,所以我們只要引入一張關系表。
重構原先設計,腳本如下:
create table Table_Docunment ( Docunment_Id int not null identity(1000, 1) primary key, Docunment_Name nvarchar(64) not null, ...... ); create table Table_DocChain ( DocChain_ChainId int not null, DocChain_VersionId int not null default(1) check(DocChain_VersionId >= 1), Docunment_Id int not null references Table_Docunment(Docunment_Id), DocChain_SubmitDate datetime not null, primary key(DocChain_ChainId, DocChain_VersionId) );
主要是添加了Table_DocChain這張關系表,對于我在此表上加的約束大家可以自己思考。
檢驗一下重構后的設計,把圖2中的信息存入新的表結構中。
Table_Docunment:
Docunment_Id | Docunment_Name | …… |
1000 | aaa | …… |
1001 | bbb | …… |
1002 | ccc | …… |
1003 | …… | …… |
1004 | …… | …… |
1005 | …… | …… |
1006 | …… | …… |
1007 | …… | …… |
Table_DocChain:
DocChain_ChainId | DocChain_VersionId | Docunment_Id | DocChain_SubmitDate |
1 | 1 | 1000 | 2010-01-01 12:03:00 |
1 | 2 | 1001 | 2010-01-02 06:02:00 |
2 | 1 | 1002 | …… |
1 | 3 | 1003 | …… |
1 | 4 | 1004 | …… |
1 | 5 | 1007 | …… |
2 | 2 | 1005 | …… |
2 | 3 | 1006 | …… |
2 | 4 | 1007 | …… |
其中關鍵的兩行記錄已用粗體標出。
反過來思考
前一節討論了文檔歸并的情況。有文檔歸并,就有可能出現文檔分支,那該如何處理呢?是否需要修改設計?
我們先看一下文檔分支的示意圖:
(圖3)
文檔分支沒有改變鏈和文檔之間的關系,所以我自己覺得前面的表結構設計不需要修改。
那圖3中分支鏈上的問號處如何填呢?
當文檔進行分支時,其已經不歸屬于原先的鏈了,應新創建一條鏈。圖3中,當文檔1005分支時,在表Table_DocChain中應插入一條DocChain_ChainId:3, DocChain_VersionId: 1, Docunment_Id: 1005的記錄,此分支的隨后文檔都歸屬此新鏈,這樣問題就解決了。
防止文檔鏈成環
對于文檔鏈的一個重要約束是不能成環。這個約束可以在應用程序端實現,但數據庫端的檢查永遠是我們最后的一道防線。我們應盡可能通過約束或其他手段來避免錯誤數據進入數據庫。
如果能用check約束來避免鏈成環是最為直接的,在Table_DocChain中加如下約束:
alter table Table_DocChain add constraint CK_LoopChain check(not exists (select * from Table_DocChain DC1 inner join Table_DocChain DC2 on DC1.DocChain_ChainId = DC2.DocChain_ChainId and DC1.DocChain_VersionId <> DC2.DocChain_VersionId and DC1.Docunment_Id = DC2.Docunment_Id ) );
其邏輯是在同一條鏈中,不存在版本號不同,且文檔號相同的記錄。
但非常可惜無論在SQL Server 2008還是Oracle中,check約束都不允許使用子查詢(Subqueries)。
我們可以通過帶有with check option的視圖來達到目的,代碼如下:
create view View_DocChain as select DC1.DocChain_ChainId, DC1.DocChain_VersionId, DC1.Docunment_Id, DC1.DocChain_SubmitDate from Table_DocChain DC1 where not exists (select * from Table_DocChain DC2 where DC1.DocChain_ChainId = DC2.DocChain_ChainId and DC1.DocChain_VersionId <> DC2.DocChain_VersionId and DC1.Docunment_Id = DC2.Docunment_Id ) with check option;
對于Table_DocChain的插入、修改,都通過View_DocChain來進行,就能防止文檔鏈成環的發生。
留言列表