1、通用数据导入导出操作模块回顾
在我的Winfrom开发框架里面,有一个通用的导入模块,它在默默处理这把规范的Excel数据导入到不同的对象表里面,一直用它来快速完成数据导入的工作。很早在随笔《》里面就很全面的介绍过它的相关功能了,在代码生成工具Database2Sharp里面,生成的Winfrom界面代码也已经把它的调用代码放进去了,因此使用起来真是很好,很开心。
在不断的项目实践中,发现使用基于Sqlite的客户端作为单机版的操作也越来越多,因此大批量的数据导入,也是经常碰到的事情,我们知道,SqlServer批量插入数据会很快,即使你没有使用事务,一条条的插入,大批量也会比较快,这个可能得益于SqlServer本身的事务优化效果。但是作为单机版的数据库,Sqlite每次操作都是单独一个事务的,插入一条数据效率可能不明显,如果操作一千条,一万条,数据的缓慢就很明显,甚至不可忍耐了。我曾经在《》里面提到了批量插入通用字典模块的字典数据,使用事务前后批量插入数据,那个速度可是差别很大。
基于以上的因素考虑,决定对通用的数据导入模块进行事务性的优化,以便适应我频繁使用Sqlite数据库大批量导入数据的情况,提高客户的良好体验。本篇主要基于事务性操作的完善,实现基于Sqlite数据的批量快速导入操作。
2、事务性代理事件的定义
由于是通用的模块,所以我们不知道具体的数据库事务对象,但是我们能够通过定义一些事件,给调用者进行事务对象的传递,这样才能在基类中使用事务对象,首先我们定义两个委托事件,一个是SaveDataHandler,用来进行单条数据的处理委托,一个是CreateTransactionHandler,让调用者创建并传递事务对象的委托,具体代码如下所示。
public partial class FrmImportExcelData : BaseForm { ............................... private DbTransaction transaction = null; ////// 使用事务对数据进行保存的委托,加快速度 /// /// 数据行 /// 事务对象 ///public delegate bool SaveDataHandler(DataRow dr, DbTransaction trans); /// /// 创建事务对象的委托,在导入操作初始化的时候赋值 /// ///public delegate DbTransaction CreateTransactionHandler();
定义好委托后,我们需要创建对应委托的事件对象,作为通用模块的事件,如下所示。
////// 保存数据事件 /// public event SaveDataHandler OnDataSave; ////// 刷新数据事件 /// public event EventHandler OnRefreshData; ////// 让调用者创建事务并传递给通用模块 /// public event CreateTransactionHandler OnCreateTransaction;
在实现数据导入前,我们需要使用事件来获取对应的事务对象,以便开始事务,具体代码如下所示。
if (MessageDxUtil.ShowYesNoAndWarning("该操作将把数据导入到系统数据库中,您确定是否继续?") == DialogResult.Yes) { if (myDs != null && myDs.Tables[0].Rows.Count > 0) { DataTable dt = myDs.Tables[0]; this.progressBar1.Visible = true; if (!worker.IsBusy) { if (OnCreateTransaction != null) { transaction = OnCreateTransaction(); } worker.RunWorkerAsync(); } } }
3、事务处理逻辑及调用者使用逻辑
这样,我们在通用模块里面,获取到Excel数据后,需要遍历每行数据,然后通过事务对象实现数据提交,部分代码如下所示。
#region 批量保存数据,然后事务提交 foreach (DataRow dr in dt.Rows) { if (OnDataSave != null) { try { bool success = OnDataSave(dr, transaction); if (success) { itemCount++; } } catch (Exception ex) { LogTextHelper.Error(ex); MessageDxUtil.ShowError(ex.Message); } } int currentStep = Convert.ToInt32(step * i); worker.ReportProgress(currentStep); i++; } #endregion if (transaction != null) { transaction.Commit(); }
我们看到,在通用的导入模块里面,我们只看到传递事务对象给OnDataSave(dr, transaction)事件,并最终提交整个事务处理而已,具体的
从以上的代码看到,我们把创建事务对象的方法留给调用者实现OnCreateTransaction事件接口,保存每行数据,也留给调用者实现数据的保存OnDataSave事件。
具体的模块调用代码如下所示。
private string moduleName = "药品目录"; private void btnImport_Click(object sender, EventArgs e) { string templateFile = string.Format("{0}-模板.xls", moduleName); FrmImportExcelData dlg = new FrmImportExcelData(); dlg.SetTemplate(templateFile, System.IO.Path.Combine(Application.StartupPath, templateFile)); dlg.OnDataSave += new FrmImportExcelData.SaveDataHandler(ExcelData_OnDataSave); dlg.OnCreateTransaction += new FrmImportExcelData.CreateTransactionHandler(dlg_OnCreateTransaction); dlg.OnRefreshData += new EventHandler(ExcelData_OnRefreshData); dlg.ShowDialog(); } DbTransaction dlg_OnCreateTransaction() { return BLLFactory.Instance.CreateTransaction(); } void ExcelData_OnRefreshData(object sender, EventArgs e) { BindData(); } bool ExcelData_OnDataSave(DataRow dr, DbTransaction trans) { string drugNo = dr["药品编码"].ToString(); string drugName = dr["药品名称"].ToString(); if (string.IsNullOrEmpty(drugNo) && string.IsNullOrEmpty(drugName)) return false; bool success = false; DrugDetailInfo info = new DrugDetailInfo(); info.DrugNo = drugNo; info.DrugName = drugName; info.Manufacture = dr["制造商"].ToString(); info.Formulations = dr["剂型"].ToString(); info.Specification = dr["规格"].ToString(); info.Unit = dr["药品单位"].ToString(); info.Note = dr["备注信息"].ToString(); info.StockQuantity = ConvertHelper.ToInt32(dr["库存量"].ToString(), 0); info.EditTime = DateTime.Now; info.Editor = Portal.gc.LoginInfo.Name; info.Dept_ID = Portal.gc.LoginInfo.Dept_ID; success = BLLFactory .Instance.Insert(info, trans); return success; }
写到这里,可能很多时候大家觉得随笔应该画上句号了吧,其实不然,还有很重要一个地方,需要提及一下,就是我们使用了事务保存数据,那么如果需要在单条记录保存的时候,需要判断检索数据,才决定插入还是更新操作呢?
如果你觉得随便写一个select语句调用不就可以了吗?那样可能就会有问题了,事务性操作会锁定当前的表,不会让你继续写入了,很快就会得到操作超时的错误异常了。
那么我们应该如何解决这种需求呢?就是你要使用事务的数据库连接对象,来实现数据的检索就可以了,如下面的代码就是OK的了。
bool dlg_OnDataSave(DataRow dr, DbTransaction trans) { string PlaneModel = dr["装备型号"].ToString(); if (string.IsNullOrEmpty(PlaneModel)) return false; bool success = false; PlaneModelInfo info = BLLFactory.Instance.FindSingle(string.Format("PlaneModel='{0}'", PlaneModel), trans); if (info != null) { info.PlaneModel = PlaneModel; info.PlaneNote = dr["保障特点"].ToString(); info.Demand = dr["保障要求"].ToString(); info.Note = dr["备注"].ToString(); info.Dept_ID = Portal.gc.LoginInfo.Dept_ID; success = BLLFactory .Instance.Update(info, info.ID, trans); } else { info = new PlaneModelInfo(); info.PlaneModel = PlaneModel; info.PlaneNote = dr["保障特点"].ToString(); info.Demand = dr["保障要求"].ToString(); info.Note = dr["备注"].ToString(); info.Dept_ID = Portal.gc.LoginInfo.Dept_ID; success = BLLFactory .Instance.Insert(info, trans); } return success; }
4、Winform开发框架的事务接口支持
基于此,我们很多查找的接口可能都会在事务中调用,需要重新构造我的框架基类接口了,把事务作为默认的对象参数,默认为NULL,调整我的基类,为所有的事务内操作提供支持,如数据访问接口层部分接口定义如下所示。
////// 数据访问层的接口 /// public interface IBaseDALwhere T : BaseEntity { #region 通用操作 /// /// 获取表的所有记录数量 /// /// 事务对象 ///int GetRecordCount(DbTransaction trans = null); /// /// 获取表的指定条件记录数量 /// /// 条件语句 /// 事务对象 ///int GetRecordCount(string condition, DbTransaction trans = null); /// /// 根据condition条件,判断是否存在记录 /// /// 查询的条件 /// 事务对象 ///如果存在返回True,否则False bool IsExistRecord(string condition, DbTransaction trans = null); ////// 查询数据库,检查是否存在指定键值的对象 /// /// Hashtable:键[key]为字段名;值[value]为字段对应的值 /// 事务对象 ///存在则返回 bool IsExistKey(Hashtable recordTable, DbTransaction trans = null);...................................true ,否则为false 。
BaseBLL业务基类的部分接口实现如下所示
////// 业务基类对象 /// ///业务对象类型 public class BaseBLLwhere T : BaseEntity, new() {............................ #region 对象添加、修改、查询接口 /// /// 插入指定对象到数据库中 /// /// 指定的对象 /// 事务对象 ///执行操作是否成功。 public virtual bool Insert(T obj, DbTransaction trans = null) { CheckDAL(); return baseDal.Insert(obj, trans); } ////// 插入指定对象到数据库中 /// /// 指定的对象 /// 事务对象 ///执行成功返回新增记录的自增长ID。 public virtual int Insert2(T obj, DbTransaction trans = null) { return baseDal.Insert2(obj, trans); } ////// 更新对象属性到数据库中 /// /// 指定的对象 /// 主键的值 /// 事务对象 ///执行成功返回 public virtual bool Update(T obj, object primaryKeyValue, DbTransaction trans = null) { CheckDAL(); return baseDal.Update(obj, primaryKeyValue, trans); }......................true ,否则为false 。
基于事务性的调整,优化了整个基类接口和实现类的类库,以方便在框架中更好整合事务性操作的支持。