一行代碼實現表操作日志記錄
在前面介紹了幾篇關於我的權限系統改進的一些經驗總結,本篇繼續這一系列主體,介紹如何一行代 碼實現重要表的操作日志記錄。我們知道,在很多業務系統裡面,數據是很敏感的,特別對於一些增加 、修改、刪除等關鍵的操作,如果能在框架層面的支持基礎上,以最少的代碼實現重要表的日志記錄, 那麼是一件非常值得慶賀的事情,也能夠為我們客戶的數據提供重要的日志跟蹤,甚至是數據恢復的參 考。
1、數據訪問層的對象繼承關系
首先,為了減少重復代碼的編寫,合理的繼承關系是必要的,我們需要在數據訪問層上建立合理的繼 承關系,如下是我的Winform開發框架的繼承關系。每個數據訪問對象(如ItemDetail數據訪問對象)都 繼承一個抽象基類AbstractBaseDAL和一個IBaseDAL基類接口,同時它也有自己特殊的業務接口,如 IItemDetail,關系如下所示。
有了上面的繼承關系,我們就可以把常規的數據庫重要操作(增刪改)放到一個高一級的層次上去解 決這個問題,而不需要在每個數據訪問層的業務類來實現。
2、操作日志記錄事件的定義和使用
為了更好實現數據操作日志的記錄,我們以事件方式來觸發操作日志的記錄,事件的具體記錄實現, 可以交給外部來記錄處理。如果事件被外部賦值了,那麼就可以在底層觸發這個事件記錄,記錄事件的 定義代碼在抽象基類進行定義,如下所示。
/// 定義一個記錄操作日志的事件處理 /// </summary> /// <param name="userId">操作的用戶ID</param> /// <param name="tableName">操作表名稱</param> /// <param name="operationType">操作類型:增加、修改、刪除 </param> /// <param name="note">操作的詳細記錄信息</param> /// <returns></returns> public delegate bool OperationLogEventHandler(string userId, string tableName, string operationType, string note, DbTransaction trans = null); /// <summary> /// 數據訪問層的超級基類,所有數據庫的數據訪問基類都繼承自這個超級基類,包括Oracle、SqlServer、Sqlite、MySql、Access等 /// </summary> public abstract class AbstractBaseDAL<T> where T : BaseEntity, new() { #region 構造函數 protected string dbConfigName = ""; //數據庫配置名稱 protected string parameterPrefix = "@";//數據庫參數化訪問的占位符 protected string safeFieldFormat = "[{0}]";//防止和保留字、關鍵字同名的字段格式,如[value] protected string tableName;//需要初始化的對象表名 protected string primaryKey;//數據庫的主鍵字段名 protected string sortField;//排序字段 protected bool isDescending = true;//是否為降序 protected string selectedFields = " * ";//選擇的字段,默認為所有(*) public event OperationLogEventHandler OnOperationLog;//定義一個操作記錄的事件處理 .....................
以上是抽象基類AbstractBaseDAL的部分代碼,上面代碼定義了一個操作記錄的委托和事件對象來處 理操作日志的記錄,通過委托的定義,我們可以規定具體的事件接口定義,並在抽象基類的底層構造這 些參數的數值,傳遞給外部的對象進行處理。
那麼我們是如何在底層操作構造這些信息的呢?
其實就是在相應的重要操作接口函數上調用這個定義的事件。我們可以在抽象基類的插入、修改、刪 除等接口上調用事件進行處理即可,為了更好處理相關數據的構造邏輯,我們把調用OnOperationLog的 事件封裝到一個單獨的函數裡面進行處理,如下所示是底層更新操作的代碼,通過增加一個 OperationLogOfUpdate來實現數據日志的事件處理。
/// <summary> /// 更新對象屬性到數據庫中 /// </summary> /// <param name="obj">指定的對象</param> /// <param name="primaryKeyValue">主鍵的值</param> /// <param name="trans">事務對象</param> /// <returns>執行成功返回<c>true</c>,否則為 <c>false</c>。</returns> public virtual bool Update(T obj, object primaryKeyValue, DbTransaction trans = null) { ArgumentValidation.CheckForNullReference(obj, "傳入的對象obj為空"); OperationLogOfUpdate(obj, primaryKeyValue, trans);//根據設置記錄操作日志 Hashtable hash = GetHashByEntity(obj); return Update(primaryKeyValue, hash, trans); }
然後我們在具體的事件處理封裝函數OnOperationLog的裡面添加處理邏輯即可,一般事件的標准處理 為如下代碼
/// <summary> /// 修改操作的日志記錄 /// </summary> /// <param name="id">記錄ID</param> /// <param name="obj">數據對象</param> /// <param name="trans">事務對象</param> protected virtual void OperationLogOfUpdate(T obj, object id, DbTransaction trans = null) { if (OnOperationLog != null) { ...............................//構造相關參數 OnOperationLog(userId, this.tableName, operationType, note, trans); } } }
我們知道,一般操作日志都會記錄是誰進行操作的,然後把它寫到日志裡面,並把操作的內容可讀化 即可,那麼在更新的時候,我們如何知道是誰操作的對象呢?因為我們沒有傳遞具體的用戶ID等標識的 啊。
這個問題挺頭痛,如果增加多一個參數,那麼就得修改很多相關的調用邏輯,這個明顯不太符合我們 簡約的風格,因此最好另尋其他方式來實現這個人員身份記錄的問題。
我們知道,一般插入、更新操作,都是帶一個操作對象的,這個操作對象是一個實體類,基類是 BaseEntity,那麼我們可以在它的身上定義多一個屬性,這個屬性不參數數據的保存,只是作為參數的傳 遞和識別而已,實體類基類的代碼如下所示。
/// <summary> /// 框架實體類的基類 /// </summary> [DataContract] public class BaseEntity { private string m_CurrentLoginUserId; /// <summary> /// 當前登錄用戶ID。該字段不保存到數據表中,只用於記錄用戶的操作日志。 /// </summary> [DataMember] public string CurrentLoginUserId { get { return m_CurrentLoginUserId; } set { m_CurrentLoginUserId = value; } } } }
有了這個信息,我們就可以在剛才的事件處理邏輯上進行獲取用戶的ID操作了。
string userId = obj.CurrentLoginUserId;
下一個問題是,如何把操作的信息可讀化,我們知道,一般操作只是對部分字段進行修改,那麼我們 一般也不需要把所有的字段信息都弄出來顯示,只需要顯示那些修改的即可。
為了實現這個數據的差異化顯示,我們需要在更新操作之前進行獲取數據庫的對象信息,然後和將要 進行更新的對象進行對比,把差異的信息作為備注信息記錄下來即可,具體邏輯如下所示。
/// <summary> /// 修改操作的日志記錄 /// </summary> /// <param name="id">記錄ID</param> /// <param name="obj">數據對象</param> /// <param name="trans">事務對象</param> protected virtual void OperationLogOfUpdate(T obj, object id, DbTransaction trans = null) { if (OnOperationLog != null) { string operationType = "修改"; string userId = obj.CurrentLoginUserId; Hashtable recordField = GetHashByEntity(obj); Dictionary<string, string> dictColumnNameAlias = GetColumnNameAlias(); T objInDb = FindByID(id, trans); if (objInDb != null) { Hashtable dbrecordField = GetHashByEntity(objInDb);//把數據庫裡的實體對象數據轉換為哈希表 StringBuilder sb = new StringBuilder(); foreach (string field in recordField.Keys) { string newValue = recordField[field].ToString(); string oldValue = dbrecordField[field].ToString(); if (newValue != oldValue)//只記錄變化的內容 { string columnAlias = ""; bool result = dictColumnNameAlias.TryGetValue(field, out columnAlias); if (result && !string.IsNullOrEmpty(columnAlias)) { columnAlias = string.Format("({0})", columnAlias);//字段中文名稱前,增加一個括號顯示,方便區分顯示 } sb.AppendLine(string.Format("{0}{1}:", field, columnAlias)); sb.AppendLine(string.Format("\t {0} -> {1}", dbrecordField[field], recordField[field])); sb.AppendLine(); } } sb.AppendLine(); string note = sb.ToString(); OnOperationLog(userId, this.tableName, operationType, note, trans); } } }
上面是更新操作的日志記錄處理,其他的插入、刪除等操作類似這樣的操作方式,再次不在贅述。
3、業務層對操作日志信息的處理
上面的代碼只是實現了對底層操作的信息記錄並傳遞給操作日志的記錄事件,並沒有知道上層是如何 處理事件信息的記錄的,這個問題留給上層去處理。
為了實現這個信息的記錄,我們在權限系統裡面增加一個單獨的數據庫表如T_ACL_OperationLog表用 來專門記錄這些信息的。
上層的處理邏輯是獲取用戶ID的登陸信息,包括用戶名、用戶IP地址、Mac地址信息等,這些信息一 旦用戶登陸到系統就會發生了,所以可以方便獲取到,然後就是把這些信息作為一個數據庫記錄寫入數 據庫表即可。
OperationLogInfo info = new OperationLogInfo(); info.TableName = tableName; info.OperationType = operationType; info.Note = note; info.CreateTime = DateTime.Now; if (!string.IsNullOrEmpty(userId)) { UserInfo userInfo = BLLFactory<User>.Instance.FindByID(userId, trans); if (userInfo != null) { info.User_ID = userId; info.LoginName = userInfo.Name; info.FullName = userInfo.FullName; info.Company_ID = userInfo.Company_ID; info.CompanyName = userInfo.CompanyName; info.MacAddress = userInfo.CurrentMacAddress; info.IPAddress = userInfo.CurrentLoginIP; } } return BLLFactory<OperationLog>.Instance.Insert(info, trans);
因為這些記錄的操作都是一樣的,為了更方便,我們把這些邏輯封裝在一個靜態的方法裡面,然後所 有需要記錄操作日志的,在業務對象裡面增加一行代碼,就可以輕松實現日志記錄了,具體代碼如下所 示。
/// <summary> /// 部門機構信息 /// </summary> public class OU : BaseBLL<OUInfo> { private IOU ouDal; /// <summary> /// 構造函數 /// </summary> public OU() : base() { base.Init(this.GetType().FullName, System.Reflection.Assembly.GetExecutingAssembly().GetName().Name); baseDal.OnOperationLog += new OperationLogEventHandler(WHC.Security.BLL.OperationLog.OnOperationLog);//如果需要記錄操作日志,則實現這個事件 this.ouDal = baseDal as IOU; }
這樣數據訪問類baseDal一旦事件初始化,那麼就會在底層進行觸發,然後交給事件的處理邏輯(上 層操作)進行處理了。
為了更好控制用戶的增加、修改、刪除的相關事件,我們可以通過一個配置表進行登記處理,然後根 據配置表的參數來決定記錄那些信息,這些就是細化的問題了。
4、我的Winform開發框架的權限系統模塊裡面對於操作日志的支持
在我的Winform開發框架裡面,權限系統是其中的一個基礎部分,因此也根據上面的邏輯實現了對操 作日志的參數配置和記錄顯示,方便對業務系統所有表的操作記錄進行跟蹤和處理。
通過一行代碼就能實現業務表的日志記錄,對我們開發新的業務模塊,效率可以提高很多,同時也能 給客戶提供更好的數據支持服務。通過在權限系統模塊裡面配置參數和顯示操作日志記錄,能夠給業務 開發提供基礎性的開發框架支持。
下面是我的Winform開發框架的權限系統模塊的一些功能 截圖,供參考學習。
雙擊打開記錄的明細,可以看到操作記錄的明細顯示。
參數配置界面如下所示。
以上顯示只是基於權限系統進行日志的記錄,當然整個業務系統框架都可以提供上面的記錄操作,因 為它們所有的數據訪問基類都是繼承自同一個抽象對象基類的。把這個模塊集成在權限系統裡面,和登 陸日志一樣,是供基礎性的記錄和查閱的,和業務不太相關。
查看本欄目
最後附上Winform開發框架的一個功能總結圖形,Winform開發框架的主要功能概覽如下圖所示。
伍華聰 http://www.iqidi.com