在開發框架中使用事務進行數據的統一處理


在很多業務數據處理的場合,整條數據鏈的數據完整性是非常重要的,因為我們在系統里面,往往需要同時更新或者寫入一些數據,如果其中任何一環處理錯誤,都應該逐條滾回,這種原子性的確保就是通過事務來進行的,本文介紹的這個事務處理,適用於我的所有開發框架,如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的雲會員管理系統的界面作為佐證,這個界面就是使用上面的事務實現多種數據關系的處理的。

 

開發框架中使用事務處理的文章介紹:

1)Winform開發框架里面使用事務操作的原理及介紹

2)Winform開發框架之通用數據導入導出操作的事務性操作完善

3)使用事務操作SQLite數據批量插入,提高數據批量寫入速度,源碼講解

 

關於Web API的知識和框架使用方面的文章,可以參考下面系列:

Web API應用架構在Winform混合框架中的應用(1)

Web API應用架構在Winform混合框架中的應用(2)--自定義異常結果的處理

Web API接口設計經驗總結 

Web API應用架構在Winform混合框架中的應用(3)--Winfrom界面調用WebAPI的過程分解

 Web API應用架構在Winform混合框架中的應用(4)--利用代碼生成工具快速開發整套應用

Web API應用架構在Winform混合框架中的應用(5)--系統級別字典和公司級別字典並存的處理方式

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM