在很多業務數據處理的場合,整條數據鏈的數據完整性是非常重要的,因為我們在系統里面,往往需要同時更新或者寫入一些數據,如果其中任何一環處理錯誤,都應該逐條滾回,這種原子性的確保就是通過事務來進行的,本文介紹的這個事務處理,適用於我的所有開發框架,如Winform開發框架、混合式開發框架、Web框架等,本文主要介紹基於我的會員系統的一些事務處理案例,對事務的使用進行介紹和代碼講解。
由於上面介紹的這些框架都是基於業務邏輯層BLL層之上的,如復雜一點的混合式框架,在BLL層之上還有一個WCF服務層、或者Web API的數據提供層,因此為了適應多種框架的適用性,我們建議把業務規則封裝在BLL層,這樣各種應用框架使用的時候,代碼就不用修改很多,而且業務邏輯統一,也很方便理解。
1、框架的事務支持
上面我提到的幾個框架,在底層我們主要是采用了微軟的Enterprise Library的數據庫訪問模塊,因此它能夠很好抽象各種數據庫的事務,以適應各種不同數據庫的事務處理。使用微軟的Enterprise Library模塊,可以很好支持SQLSever、Oracle、Mysql、Access、SQLite等數據庫。
開發框架,常見的分層模式,可以分為UI層、BLL層、DAL層、IDAL層、Entity層、公用類庫層等等
框架的基類我們封裝了大量的通用性處理函數,包括數據訪問層、業務邏輯層的基類,所有的基類函數基本上都帶有一個DbTransaction trans = null 的定義,就是我們可以采用事務,也可以默認不采用事務,是一個可選性的事務參數。
如數據訪問類的部分接口定義如下所示。
/// <summary> /// 數據訪問層的接口 /// </summary> public interface IBaseDAL<T> where T : BaseEntity { /// <summary> /// 插入指定對象到數據庫中 /// </summary> /// <param name="obj">指定的對象</param> /// <param name="trans">事務對象</param> /// <returns>執行成功返回True</returns> bool Insert(T obj, DbTransaction trans = null); /// <summary> /// 根據指定對象的ID,從數據庫中刪除指定對象 /// </summary> /// <param name="key">指定對象的ID</param> /// <param name="trans">事務對象</param> /// <returns>執行成功返回<c>true</c>,否則為<c>false</c>。</returns> bool Delete(object key, DbTransaction trans = null); /// <summary> /// 更新對象屬性到數據庫中 /// </summary> /// <param name="obj">指定的對象</param> /// <param name="primaryKeyValue">主鍵的值</param> /// <param name="trans">事務對象</param> /// <returns>執行成功返回<c>true</c>,否則為<c>false</c>。</returns> bool Update(T obj, object primaryKeyValue, DbTransaction trans = null); /// <summary> /// 查詢數據庫,檢查是否存在指定ID的對象 /// </summary> /// <param name="key">對象的ID值</param> /// <param name="trans">事務對象</param> /// <returns>存在則返回指定的對象,否則返回Null</returns> T FindByID(object key, DbTransaction trans = null); .....................//其他操作 }
數據訪問接口和數據訪問類的實現圖示如下所示。
因此在最高級的抽象基類AbstractBaseDAL的數據訪問層里面,都有大量關於事務的接口可以使用,同樣在BLL層也一樣,里面大多數就是基於對AbstractBaseDAL的包裝處理,同樣預留了DbTransaction trans = null的參數方便我們進行事務性的處理。
同樣,在BLL層的基類BaseBLL里面的接口,也同樣包含了這樣的事務參數的,因此在框架里面整合事務是一件非常方便的事情,因為所有接口都預留了使用事務的可能,方便我們整合各個方法。
2、框架事務接口的使用
在框架的DAL層和BLL層,我們都定義了很多公用的、帶有事務參數的接口,如果我們在業務處理里面,使用事務的話,那么也是很方便的事情。如在
事務的處理,除了包含對數據庫的更新修改需要使用外,如果在事務的處理中,查詢數據等接口也要一並采用事務處理,否則就容易發生鎖住堵塞的情況,因此,如果使用事務,那么事務參數是全程需要傳入來進行數據的檢索、修改等操作的。
事務的標准使用過程,一般是先創建事務對象,然后在各個接口里面使用事務處理,如下代碼示例所示。
DbTransaction trans = base.CreateTransaction(); if (trans != null) { string sql = .............; base.SqlExecute(sql, trans); sql = .............; base.SqlExecute(sql, trans); try { trans.Commit(); result = true; } catch { trans.Rollback(); throw; } }
如果我們定義的處理接口,只是其中事務處理的一個環節,那么我們就需要在接口定義的時候,預留有DbTransaction trans = null的參數,示例代碼如下所示。
/// <summary> /// 添加積分 /// </summary> /// <param name="id">會員ID</param> /// <param name="points">積分(正數為加,負數為減)</param> /// <returns></returns> public bool AddPoints(string id, decimal points, DbTransaction trans = null) { string sql = string.Format("Update {0} Set TotalPoints = TotalPoints + {1} Where ID='{2}' ", tableName, points, id); return SqlExecute(sql, trans) > 0; } /// <summary> /// 添加消費金額 /// </summary> /// <param name="id">會員ID</param> /// <param name="amount">消費金額(正數為加,負數為減)</param> /// <returns></returns> public bool AddConsumption(string id, decimal amount, DbTransaction trans = null) { //消費的時候,同時修改累計消費金額和最后到店時間 string sql = string.Format("Update {0} Set TotalConsumption = TotalConsumption + {1}, LastConsumptionDate=getdate() Where ID='{2}' ", tableName, amount, id); return SqlExecute(sql, trans) > 0; }
在BLL層,我們同樣可以對這些接口預留事務接口,對數據訪問的DAL 層進行封裝的。
/// <summary> /// 添加積分 /// </summary> /// <param name="id">會員ID</param> /// <param name="points">積分(正數為加,負數為減)</param> /// <returns></returns> public bool AddPoints(string id, decimal points, DbTransaction trans = null) { IMember dal = baseDal as IMember; return dal.AddPoints(id, points, trans); } /// <summary> /// 添加消費金額 /// </summary> /// <param name="id">會員ID</param> /// <param name="amount">消費金額(正數為加,負數為減)</param> /// <returns></returns> public bool AddConsumption(string id, decimal amount, DbTransaction trans = null) { IMember dal = baseDal as IMember; return dal.AddConsumption(id, amount, trans); }
預留這樣的事務參數,對於我們在整合各個處理過程有很大的幫助,否則任何一環沒有事務參數,處理就可能出現問題,這也是為什么框架的基類參數,都提供了一個事務參數的可選參數,就是這個道理。
如整合上面積分和金額的處理過程,就是會員系統里面必要的一環,而且這些數據需要連貫在一起進行修改,不能再中間任何一個環節發生數據部分丟失或者不成功的操作,因此我們必須使用事務處理過程。
例如,我在會員消費的過程處理中,其中包含了對消費商品主表、明細表的記錄,還包括對庫存的調整、還有會員消費、積分贈送等等細粒度的處理,因此我們在事務里面可以按照下面的方式進行處理。
DbTransaction trans = base.CreateTransaction(); if (trans != null) { try { bool success = BLLFactory<MemberConsumption>.Instance.Insert(info, trans); if (success) { //保存消費明細記錄 foreach (ConsumptionDetailInfo detail in detailList) { BLLFactory<ConsumptionDetail>.Instance.Insert(detail, trans); //減少對應庫存 string note = "會員消費"; double quantity = (-1) * detail.Quantity; BLLFactory<Stock>.Instance.ModifyQuantity(info.Corp_ID, info.Shop_ID, info.Creator, detail.ProductNo, quantity, note, trans); } //增加消費金額 BLLFactory<Member>.Instance.AddConsumption(info.Member_ID, info.Amount, trans); //添加積分(消費一元積一分) BLLFactory<Member>.Instance.AddPoints(info.Member_ID, info.Amount, trans); //減少余額 decimal subBalance = (-1) * info.Amount; BLLFactory<Member>.Instance.AddBalance(info.Member_ID, subBalance, trans); } trans.Commit(); result.Success = true; } catch (Exception ex) { LogTextHelper.Error(ex); trans.Rollback(); result.ErrorMessage = ex.Message; } }
上面的方法就是一個完整的事務處理過程,就是在會員消費的情況下發生的,如果我們需要考慮多種應用框架的封裝,如Web、Web API、Winform、WCF等方式的調用,那么我們就把它放到了業務邏輯層BLL層進行封裝,如下就是BLL層的方法。
/// <summary> /// 保存消費記錄,同時修改庫存 /// </summary> /// <param name="info">消費主記錄</param> /// <param name="detailList">消費明細列表</param> /// <returns></returns> public CommonResult SaveConsumption(MemberConsumptionInfo info, List<ConsumptionDetailInfo> detailList)
這樣我們在Web API的調用的時候,就可以不在使用這個特定的DbTransaction trans = null參數了,因此上面的方法體就是一個最小的操作單元了。
3、混合框架中Web API的封裝和調用
在我的混合式開發框架基礎上,我們服務提供可以是傳統Winform、WCF,以及WebAPI的方式,框架的效果圖如下所示。
對於Web API模式,我們對BLL業務邏輯層進行了封裝,它的APIController的方法也就是說如下所示。
/// <summary> /// 保存消費記錄,同時修改庫存 /// </summary> /// <param name="info">消費主記錄</param> /// <param name="detailList">消費明細列表</param> /// <returns></returns> [HttpPost] public CommonResult SaveConsumption(JObject param, string token, string signature, string timestamp, string nonce, string appid) { //如果用戶簽名檢查不通過,則拋出MyApiException異常。 base.CheckTokenAndSignatrue(token, signature, timestamp, nonce, appid); dynamic obj = param; if (obj != null) { MemberConsumptionInfo info = obj.info; List<ConsumptionDetailInfo> detailList = obj.detailList; return BLLFactory<MemberConsumption>.Instance.SaveConsumption(info, detailList); } else { throw new MyApiException("傳遞參數錯誤"); } }
這里面的方法有很多參數,第一個JObject param是一個動態對象的定義參數,具體可以參考《Web API接口設計經驗總結》里面介紹的“動態對象的接口定義”節點了解。
由於這個處理過程是對數據進行了修改等重要的處理,因此參數需要增加簽名數據,以及Token身份的標識,而且整個接口是公布在HTTPS協議的基礎上,因此 接口的安全性是得到了非常好的保證。
對於在混合框架中,訪問Web API的接口安全性方面,可以參考我前面的文章《Web API應用架構在Winform混合框架中的應用(1)》的“Web API訪問的安全性考慮”節點了解。整個框架里面,最有保證、最方便的、適應最廣泛應用的就是基於Web API的方式接入了,它不僅可以在桌面程序進行處理,也可以在移動端(包括APP和微信公眾號等),使用Web API的接口進行數據的獲取和提交。WCF方式雖然功能強大,但是相對顯得笨重一些,而且數據安全性,需要服務端和客戶端采用X509證書進行通訊,對移動端則是很難的一件事情。
言歸正傳,上面的事務處理,在它的基礎上Web API接口進行了封裝,我們調用Web API就不需要進行事務的參數傳遞了,因為它已經是一個操作的整體了,要么成功,要么全部失敗滾回即可。
為了適應在混合框架的Winform里面進行調用,我們還是需要對剛才的Web API接口進行了客戶端的封裝,給Web API傳遞對應的參數(通過POST方式提交JSON參數給Web API接口),具體的代碼如下所示。
public CommonResult SaveConsumption(MemberConsumptionInfo info, List<ConsumptionDetailInfo> detailList) { var action = "SaveConsumption"; var postData = new { info = info, detailList = detailList }.ToJson(); string url = GetPostUrlWithToken(action); return JsonHelper<CommonResult>.ConvertJson(url, postData); }
這個過程就是封裝了對Web API的調用,並通過JSON數據返回的方式,把他們轉換為對應的結果對象,這里的結果是一個通用的結果集對象CommonResult 。
這樣我們在Winform的客戶端里面就有了統一的調用方式了,非常方便簡潔,代碼如下所示。
//獲取消費明細 List<ConsumptionDetailInfo> detailList = GetConsumptionDetail(); //保存消費明細,以及在后台利用事務處理各種關系的修改 CommonResult result = CallerFactory<IMemberConsumptionService>.Instance.SaveConsumption(info, detailList);
最后來一個基於Web API的雲會員管理系統的界面作為佐證,這個界面就是使用上面的事務實現多種數據關系的處理的。
開發框架中使用事務處理的文章介紹:
2)Winform開發框架之通用數據導入導出操作的事務性操作完善
3)使用事務操作SQLite數據批量插入,提高數據批量寫入速度,源碼講解
關於Web API的知識和框架使用方面的文章,可以參考下面系列:
Web API應用架構在Winform混合框架中的應用(1)
Web API應用架構在Winform混合框架中的應用(2)--自定義異常結果的處理
Web API應用架構在Winform混合框架中的應用(3)--Winfrom界面調用WebAPI的過程分解
Web API應用架構在Winform混合框架中的應用(4)--利用代碼生成工具快速開發整套應用
Web API應用架構在Winform混合框架中的應用(5)--系統級別字典和公司級別字典並存的處理方式