iBATIS.net:批量数据插入?

  前言:本文标题有一个问号,带个问号的原因就是到本文写成为止“批量数据插入”依然是没有解决并困扰着楼猪的一个疑难问题。期待有研究iBatis透彻的高人看完本文后不吝赐教,绝非单单出于礼貌,真诚地谢谢了先。

一、问题来源
  在楼猪做过的大大小小项目中,用iBatis.net实现的传统CRUD操作里,只能实现一条记录的插入操作,大多数情况下,这个也满足了项目需要,但是在大数据量的批量数据导入的情况下就有瓶颈。在数据量较大的情况下,如果还是一条一条数据插入,好比洪水暴发而堤坝只开了一个小小的泄水口,楼猪曾经测试过几十万条数据的插入,等的那叫一个销魂,而且要处理的数据较长时间占据内存,影响整体处理性能。
  由于数据库服务器限制,在“直接调用ssis包实现Sql Server的数据导入功能”无法使用后,楼猪google了网上用iBatis批量插入的文章。在google的结果上草草看了几页,赫然发现就是那么两篇在转来转去,而且还是在java下的iBatis结合Spring共同实现的。楼猪不禁怀疑难道是自己老眼昏花看错了,抑或是有人写好了隐藏很深等待考古发掘或是科学发现呢?

二、批量插入的变通解决方案
鉴于常见插入都是针对单表的,本文的示例延续使用“iBATIS.net获取运行时sql语句”中的Person表来讲解。
1、批量数据插入
  既然没看到iBatis.net有现成的类库可实现批量导入,再次祭出杀手锏,查找ado.net下的批量数据操作。这一次资料就灰常的丰富和全面了。在常见的几种解决方案中,楼猪选择了看上去非常简洁而且效率也不低的SqlBulkCopy对象的实现方式,间接实现了iBatis.net的数据导入:
(1)、基类中两个虚方法

代码

        
///   <summary>
        
///  批量插入(这个方法外部重写)
        
///   </summary>
        
///   <typeparam name="M"></typeparam>
        
///   <param name="listModels"></param>
        
///   <returns></returns>
         public   virtual   bool  BatchInsert < M > (IList < M >  listModels)  where  M :  class
        {
            
bool  flag  =   false ;
            
return  flag;
        }

        
///   <summary>
        
///  执行插入命令
        
///   </summary>
        
///   <param name="connStr"> sql连接字符串 </param>
        
///   <param name="tableName"> 表名称 </param>
        
///   <param name="dt"> 组装好的要批量导入的datatable </param>
        
///   <returns></returns>
         protected   virtual   bool  ExecuteInsertCommand( string  connStr,  string  tableName, DataTable dt)
        {
            
bool  flag  =   false ;
            
// SqlTransaction transaction = null;
            
// ISqlMapSession sesseion = this.SqlMapper.CreateSqlMapSession();
             try
            {
                
using  (System.Transactions.TransactionScope scope  =   new  System.Transactions.TransactionScope())
                {
                    
using  (SqlConnection conn  =   new  SqlConnection(connStr))
                    {
                        conn.Open();
                        
using  (SqlBulkCopy sbc  =   new  SqlBulkCopy(conn))
                        {
                            
// sesseion.BeginTransaction();
                            
// transaction = conn.BeginTransaction();
                            
// 服务器上目标表的名称
                            sbc.DestinationTableName  =  tableName;
                            sbc.BatchSize 
=   50000 ;
                            sbc.BulkCopyTimeout 
=   180 ;
                            
for  ( int  i  =   0 ; i  <  dt.Columns.Count; i ++ )
                            {
                                
// 列映射定义数据源中的列和目标表中的列之间的关系
                                sbc.ColumnMappings.Add(dt.Columns[i].ColumnName, dt.Columns[i].ColumnName);
                            }
                            sbc.WriteToServer(dt);
                            flag 
=   true ;
                            
// throw new Exception("Test...");
                            
// transaction.Commit(); // 无效事务
                            
// sesseion.Complete();   // 无效事务
                            scope.Complete(); // 有效的事务
                        }
                    }
                }
            }
            
catch  (Exception ex)
            {
                
// if (transaction != null)
                
// {
                
//     transaction.Rollback();
                
// }
                
// if (sesseion != null)
                
// {
                
//     sesseion.RollBackTransaction();
                
// }
                flag  =   false ;
                
string  errMsg  =  ex.Message;
            }
            
return  flag;
        }

 说明:
a、从ExecuteInsertCommond的实现代码中,可以清楚地看到,其实我们就是直接利用了ado.net的SqlBulkCopy对象,通过WriteToServer方法实现的。在示例代码中我们需要传递的三个参数分别是数据库连接字符串,表名和一个datatable对象。其中datatable是在外部组装好传递进来给WriteToServer方法使用的,WriteToServer方法还有另外3个重载方法,您可以扩展实现其他形式的参数传递。
b、在批量插入的代码中,事务的处理楼猪选择了传说已久的TransactionScope,记得刚毕业那会,没少这么写:

// .Net 2.0 可以这样子: 
using  (TransactionScope trans  =   new  TransactionScope()) 

   
// 执行你的事务,如果不成功自动回滚

 TransactionScope已经验证通过,而且省事的一塌糊涂。
至于iBatis自己的事务SqlMapperSession或者ado.net的SqlTransaction,楼猪在注释中写的很清楚,两种事务无一例外地有异常。
(2)、dao重写批量导入方法

代码
     ///   <summary>
        
///  批量插入
        
///   </summary>
        
///   <typeparam name="M"></typeparam>
        
///   <param name="listModels"></param>
        
///   <returns></returns>
         public   override   bool  BatchInsert < M > (IList < M >  listModels)
        {
            
bool  flag  =   false ;
            
try
            {
                
string  connStr  =   this .SqlMapper.DataSource.ConnectionString;
                
string  tbName  =   typeof (M).Name;
                DataTable dt 
=  DataTableHelper.CreateTable < M > (listModels);
                flag 
=  ExecuteInsertCommand(connStr, tbName, dt);
            }
            
catch
            {
                flag 
=   false ;
            }
            
return  flag;
        }

 我们看到,在重写的方法里准备了需要传递的三个参数:数据库连接字符串,表名和一个datatable对象。在datatable组装的时候我们借助了一个辅助类:

代码
using  System;
using  System.Collections.Generic;
using  System.Data;
using  System.Reflection;

namespace  IBatisNetApp.DAL.Common
{
    
using  IBatisNetApp.DAL.Model;

    
public   class  DataTableHelper
    {
        
private   static  IList < string >  CreateModelProperty < T > (T obj)  where  T :  class
        {
            IList
< string >  listColumns  =   new  List < string > ();
            BindingFlags bf 
=  BindingFlags.Instance  |  BindingFlags.NonPublic  |  BindingFlags.Public  |  BindingFlags.Static;
            Type objType 
=   typeof (T);
            PropertyInfo[] propInfoArr 
=  objType.GetProperties(bf);
            
foreach  (PropertyInfo item  in  propInfoArr)
            {
                
object [] objAttrs  =  item.GetCustomAttributes( typeof (TableColumnAttribute),  true );
                
if  (objAttrs  !=   null   &&  objAttrs.Length  >   0 ) // 取出实体对应表的实际列名
                {
                    listColumns.Add(item.Name);
                }
            }
            
return  listColumns;
        }

        
private   static  DataTable CreateTable(IList < string >  listColumns)
        {
            DataTable dt 
=   new  DataTable();
            
for  ( int  i  =   0 ; i  <  listColumns.Count; i ++ )
            {
                dt.Columns.Add(
new  DataColumn(listColumns[i]));
            }
            
return  dt;
        }

        
public   static  DataTable CreateTable < T > (IList < T >  listModels)  where  T :  class
        {
            T model 
=   default (T);
            IList
< string >  listProperties  =  CreateModelProperty < T > (model);
            DataTable dataTable 
=  CreateTable(listProperties);
            BindingFlags bf 
=  BindingFlags.Instance  |  BindingFlags.NonPublic  |  BindingFlags.Public  |  BindingFlags.Static;
            Type objType 
=   typeof (T);
            PropertyInfo[] propInfoArr 
=  objType.GetProperties(bf);
            
foreach  (T itemModel  in  listModels)
            {
                DataRow dataRow 
=  dataTable.NewRow();
                
foreach  (PropertyInfo item  in  propInfoArr)
                {
                    
string  propName  =  item.Name;
                    
if  (listProperties.Contains(propName))
                    {
                        
object  value  =  item.GetValue(itemModel,  null );
                        dataRow[propName] 
=  value;
                    }
                }
                dataTable.Rows.Add(dataRow);
            }
            
return  dataTable;
        }
    }
}

 必须注意,datatable里的列名顺序不限,但是列名必须对应实际表里的列(datatable里的列可以少于等于实际表里的列,但是不能大于)。示例中Person实体的TableColumn特性就是为了让属性和表的列名匹配,否则,实体继承的一些不是实际表的属性字段也会映射到表里,这样就会发生“给定的 ColumnMapping 与源或目标中的任意列均不匹配”的异常。

2、批量数据插入并获取所有新插入的自增Id
  其实这个是在问题1的基础上延伸的一个问题。楼猪想了一个笨拙的解决方案,思路就是,将要插入的大数据量的泛型List(设有count项)第一项先单独插入,返回一个自增id设为firstId,然后按照1里的解决方案(有事务的那种,不带事务的话,结果就不是有偏差那么简单了,很可能错的非常离谱),批量插入剩余的count-1项,插入成功后,我们可以断定批量插入的数据Id的范围为firstId<=Id<firstId+count。
ps,在实际的执行环境中,楼猪还是觉得这个非常不靠谱,一定要慎用。哎,懒得贴代码了。
最后,期待高手给出iBATIS.net的批量数据操作的完美解决方案。

【补充】高手你在哪里,顶上去啊?

demo下载:IBatisNetApp

你可能感兴趣的:(ibatis)