在企業資料庫設計中,經常會遇到一個需求,就是希望把操作之前的資料保留下來,能夠看到操作之前是什麼資料,操作之後是什麼資料。對於這種需求,我們可以使用保留歷史資料或者使用版本來實現。
為了能夠保留歷史資料,在版本設計時有以下方案:
一、使用版本號
版本號是一種常見的版本設計方案,就是在要進行歷史資料保留的表上面增加一個版本號欄位,該欄位可以是DateTime型別,也可以是int型別,每進行資料操作時,都是建立一個新的版本,版本是隻增不減的,所以只需要拿到最大一個版本號,就能得到最新的業務資料。
版本號除了能夠用於留存歷史資料外,還有一個功能就是避免併發編輯操作。比如我們有一個物件A,當前的版本是1,兩個使用者同時開啟了該物件的編輯頁面,進行資料更改。先是甲使用者提交更改,這個時候系統把物件的ID和版本進行查詢,發現要修改的資料最新版本是1,所以成功修改,儲存了物件A的新版本2。這個時候使用者乙也提交了修改。系統把物件的ID和版本1進行查詢,發現要修改的資料最新版本是2,不符合要求,所以拒絕使用者乙的修改。使用者乙只有重新整理介面,拿到最新的版本2,再進行修改。
ID | 單號 | 金額 | 版本號 |
1 | EXP123 | 100 | 1 |
在使用版本號的情況下,對單據的金額進行修改,修改後建立新的版本號2:
ID | 單號 | 金額 | 版本號 |
1 | EXP123 | 100 | 1 |
2 | EXP123 | 120 | 2 |
二、使用生效、失效時間
儲存歷史資料的第二辦法是使用生效失效時間來表示一個版本。要進行歷史資料記錄的表增加“生效時間”“失效時間”兩個欄位,兩個欄位不允許為空。對於剛建立的資料,生效時間是建立該資料的時間,失效時間是9999-12-31。現在對這條資料進行了修改,那麼我們只需要將當前時間設定為上一個版本的失效時間,同時建立一條新資料,生效時間是當前時間,失效時間是9999-12-31即可。
ID | 單號 | 金額 | 生效時間 | 失效時間 |
1 | EXP123 | 100 | 2013/9/1 15:30:00 | 9999/12/31 23:59:59 |
比如上面一條單據,是2013-9-1建立的,後來在2013-9-9 15:00:00對該單據進行修改,將金額從100修改為120,儲存時建立的新資料如下:
ID | 單號 | 金額 | 生效時間 | 失效時間 |
1 | EXP123 | 100 | 2013/9/1 15:30:00 | 2013/9/9 15:00:00 |
2 | EXP123 | 120 | 2013/9/9 15:00:00 | 9999/12/31 23:59:59 |
使用了生效、失效時間後,我們可以查詢任意時刻資料庫中資料的值,只需要把要查詢的時刻傳入,然後between 生效時間 and 失效時間即可。
使用前兩種方案都需要一個業務主鍵來標識具體的一個業務資料。如果我們要記錄的實體沒有明確的“單號”、“訂單號”這類的業務主鍵該怎麼辦?我們可以使用建立資料時的資料庫主鍵作為業務主鍵。
員工ID | 姓名 | 生日 | 業務ID | 版本號 |
1 | 張三 | 1984/12/29 | 1 | 1 |
比如我們有個員工表,記錄員工基本資訊,在建立張三這個員工的資料時,其在資料庫的ID為1,那麼可以將其業務ID也設定為1。接下來對張三的屬性進行更改,記錄了版本,那麼就會建立新的版本,其主鍵“員工ID”會變化,但是其業務主鍵“業務ID”始終是1,不會變化的。
員工ID | 姓名 | 生日 | 業務ID | 版本號 |
1 | 張三 | 1984/12/29 | 1 | 1 |
2 | 張三 | 1985/1/9 | 1 | 2 |
使用前面兩個方案雖然能夠很好的記錄歷史資料,但是每次修改資料都會導致新版本生成儲存,所以每個版本的ID都是新的,所以必須有一個業務主鍵來標識一個實體,這裡的兩個例子“單號”就是其業務主鍵。主鍵的變動使得所有關聯的物件都得變動,從而形成連鎖效應,使得各個關聯的物件也生成新的版本。比如我們有個訂單系統,裡面有訂單表和訂單明細表。現在我們要對訂單的修改記錄歷史版本,所以增加了生效時間和實效時間,並使用訂單號作為業務主鍵。現在有一個訂單A,下面有100條明細,如果要對訂單進行修改,將某一條明細的屬性進行修改,從而導致整個訂單的變化,那麼我們就需要建立新的訂單資料行,由於主鍵變動,所以訂單明細都需要變動,所以100條明細都需要建立新的版本,新版本的訂單明細中,“訂單ID”指向了新的版本的訂單資料的ID。
這樣的設計造成的問題就是訂單明細表會極速膨脹,如果一個訂單有1000條明細,我們只是修改了訂單本身的屬性,並不修改訂單明細,也會造成對這1000條明細做Copy,然後儲存。那怎麼辦呢?我們可以使用以下辦法:
1.對訂單明細建立版本欄位,將版本的粒度細化到訂單明細,而不是訂單。訂單與訂單明細不存在資料庫級的外來鍵關係,只存在業務級的外來鍵關係。也就是說訂單明細表中增加生效時間、失效時間之外,還需要增加“訂單號”這個欄位,用於表名該明細是屬於哪個訂單的。
我們這麼修改後,如果訂單物件進行了修改,訂單明細沒有修改(比如改了一下收件人資訊),那麼只需要在訂單表中生成新的一行資料,訂單明細不會Copy生成新的資料。如果我們對某一條訂單明細進行了更改(比調整了單價、數量)那麼只需要對具體修改的那條訂單明細進行更改,而不需要對整個訂單的所有明細進行更改。
使用這種設計後,查詢訂單及其明細,需要對兩個表執行生效失效時間的過濾,而且明細的獲取是通過訂單號去取,而不是通過訂單ID去取。
將版本控制的粒度細化到訂單明細時,後臺程式的邏輯也會更加複雜。使用者在介面上操作的是訂單物件,系統會將整個修改後的訂單物件傳到後臺,後臺程式需要對每個訂單項進行對比,如果發現訂單項進行了修改,那麼就會呼叫生成新版本訂單明細的方法。
2.使用單獨的歷史表
這是另外一種實現歷史版本記錄的方法:
三、使用單獨的歷史表
使用歷史表其實就是建立完全相同Schema的表(當然,也可以新增更多的欄位用於記錄額外的歷史版本資訊),該表只保留歷史版本的資料。這有點像一個歸檔邏輯,所有歷史版本我們認為都應該是不經常訪問的,所有可以扔到單獨的表,對於現有生效的版本,仍然保留在原表中,如果需要查詢歷史版本,那麼就從歷史表中查詢。
使用單獨的歷史表有以下好處:
- 業務資料表的資料量不會因為歷史版本記錄而膨脹。因為歷史資料都記錄到了另外一個表中,所以業務資料表只記錄了一份資料。
- 業務資料表的Schema不需要調整,增加額外的版本欄位。由於對原有資料表不做Schema變更,所以原有查詢邏輯也不用更改。對於一個現有的資料庫設計,在增加歷史資料記錄功能時更簡單。
- 業務資料表可以直接進行update操作,不會生成新的ID。由於ID不會變,所以我們並需要業務主鍵應用到程式邏輯中。
使用歷史表記錄歷史版本主要是要對資料操作方法(增加、刪除、修改)進行修改,使得每次資料操作時,先在歷史表中留痕,然後再進行資料操作。另外就是對查詢歷史版本功能進行修改,因為歷史資料在另外一個表中,所以對於的SQL是不一樣的。當然,我們也可以建立歷史版本資料庫,裡面儲存了所有的歷史表。