在开发框架中使用事务进行数据的统一处理

在很多业务数据处理的场合,整条数据链的数据完整性是非常重要的,因为我们在系统里面,往往需要同时更新或者写入一些数据,如果其中任何一环处理错误,都应该逐条滚回,这种原子性的确保就是通过事务来进行的,本文介绍的这个事务处理,适用于我的所有开发框架,如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)--系统级别字典和公司级别字典并存的处理方式

本文转自博客园伍华聪的博客,原文链接:在开发框架中使用事务进行数据的统一处理,如需转载请自行联系原博主。

时间: 2024-09-21 11:21:43

在开发框架中使用事务进行数据的统一处理的相关文章

在ADO.NET中使用事务保护数据的完整性(1)

ado|数据 在ADO.NET中使用事务保护你数据的完整性 Christa May 2004 小结: 当修改数据时,事务是维护数据完整性的一个关键特征. 纵览一下事务及其重要性,接下来学习如何在你的应用中使用事务保护数据. 内容 介绍 事务剖析 事务在ADO.Net中 实施事务 总结 介绍 大多数企业数据库为了在数据修改时保证数据的完整性而提供了事务这样一个特征. 维护数据的完整性目的是保证组织依赖数据的质量; 毕竟, 当你生成报表或进行一些数据驱动的处理, 你希望知道你操作的数据是正确的. 一

在ADO.NET中使用事务保护数据的完整性(3)

ado|数据 事务在ADO.Net中 Ado.net 支持两种事务模型,这在.Net Framework 文档中作为指南有定义.事务指南手册介绍了通常应该知道的数据库事务, 它影响操作单个目标数据库.一个类通过由ado.net显示描述的事务边界使用事务类和方法能提供事务指南.在接下来的章节中,我将把重点放在事务指南上. 自动事务也是可行的,为了使用类来参与事务来协调跨多个数据源的改变.在这种情景下事务本身是在外层处理的,比如通过com+ 和 DTC. 需要了解更多自动事务的,可以参照相关自动事务

在ADO.NET中使用事务保护数据的完整性(2)

ado|数据 事务剖析 事务最基本上包含两个步骤 – 开始, 然后是提交或回滚. 开始调用定义了事务的边界, 同时调用提交或回滚在定义结束. 在事务边界内, 所有的执行描述被认为是完成任务的一部分, 必须作为成功或失败的一种. 如果一切都成功提交将会执行所有的数据修改, 如果有任何错误发生, 回滚将会取消修改. 所有的.Net 数据提供对象提供了类似的类和方法来完成这些操作. 孤立等级 孤立等级在事务中界定事务孤立行为. 越是孤立等级越高, 数据越不会被别的事务干扰. 许多数据库通过加锁来增强孤

在ADO.NET中使用事务保护数据的完整性(4)

ado|数据 实施事务 既然我们已经看了类和成员,让我们来看一下基本的实施情况.接下来的代码是一个简单的情况,使用事务来保证两个存储过程-一个从表中删除库存,另一个增加库存在另个表中,或同时执行,或失败. using System;using System.Drawing;using System.Collections;using System.ComponentModel;using System.Windows.Forms;using System.Data;using System.Da

mysql中不同事务隔离级别下数据的显示效果

  事务是一组原子性的SQL查询语句,也可以被看做一个工作单元.如果数据库引擎能够成功地对数据库应用所有的查询语句,它就会执行所有查询,如果任何一条查询语句因为崩溃或其他原因而无法执行,那么所有的语句就都不会执行.也就是说,事务内的语句要么全部执行,要么一句也不执行. 事务的特性:acid,也称为事务的四个测试(原子性,一致性,隔离性,持久性) automicity:原子性,事务所引起的数据库操作,要么都完成,要么都不执行 consisitency:一致性,事务执行前的总和和事务执行后的总和是不

PHP中执行MYSQL事务解决数据写入不完整等情况

 事务可以进行模拟SQL操作,当所有的SQL都操作成功的时候才进行SQL操作,只要有一个操作失败就回滚当前事务的所有SQL操作,避免出现上面描述中出现的数据写入不完整等情况 近来稍有时间研究了下MYSQL中的事务操作,在很多场合下很是适用,譬如在注册的时候需要初始化很多张关联表的时候,问答回复的时候需要至少同时操作两张表,这些都会在某些时候只能成功更新一张表,而另外的SQL语句出现错误,正常的操作会导致初始化了一张表 ,其他的都木有能初始化,这个时候就会导致用户表里的用户信息已经执行插入,导致提

Spring中如何实现插入数据后调用存储过程,且在同一事务里,如何证明。

问题描述 Spring中如何实现插入数据后调用存储过程,且在同一事务里,如何证明.Spring是如何管理事务的,我在插入后如何自动提交的. 解决方案 解决方案二:<propertyname="sqlMapClientTemplate"ref="sqlMapClientTemplate"></property>DAO中使用的这个对应的beanxml是<beanid="sqlMapClientTemplate"class

PHP中执行MYSQL事务解决数据写入不完整等情况_php技巧

近来稍有时间研究了下MYSQL中的事务操作,在很多场合下很是适用,譬如在注册的时候需要初始化很多张关联表的时候,问答回复的时候需要至少同时操作两张表,这些都会在某些时候只能成功更新一张表,而另外的SQL语句出现错误,正常的操作会导致初始化了一张表 ,其他的都木有能初始化,这个时候就会导致用户表里的用户信息已经执行插入,导致提示注册失败,但是用户已经注册了部分信息,这个时候需要程序员去数据库删除相应的数据是一个比较不好的事情. 因此这边考虑使用事务,事务可以进行模拟SQL操作,当所有的SQL都操作

在Winform开发框架中,利用DevExpress控件实现数据的快速录入和选择

在实际的项目开发过程中,有好的控件或者功能模块,我都是想办法尽可能集成到我的WInform开发框架中,这样后面开发项目起来,就可以节省很多研究时间,并能重复使用,非常高效方便.在我很早之前的一篇博客<在GridControl控件中使用SearchLookUpEdit构建数据快速输入>就曾经介绍,如何在列表控件中实现数据的快速录入,本文介绍另外一种方式,通过文本输入框的输入选择,可以实现数据的快速录入,原理和之前一篇差不多,不过这次利用DevExpress控件的GridLookupEdit控件封