通过反射注解批量插入数据到DB的实现方法
批量导入思路
最近遇到一个需要批量导入数据问题。后来考虑运用反射做成一个工具类,思路是首先定义注解接口,在bean类上加注解,运行时通过反射获取传入Bean的注解,自动生成需要插入DB的SQL,根据设置的参数值批量提交。不需要写具体的SQL,也没有DAO的实现,这样一来批量导入的实现就和具体的数据库表彻底解耦。实际批量执行的SQL如下:
insertintocompany_candidate(company_id,user_id,card_id,facebook_id,type,create_time,weight,score)VALUES(?,?,?,?,?,?,?,?)ONDUPLICATEKEYUPDATEtype=?,weight=?,score=?
第一步,定义注解接口
注解接口Table中定义了数据库名和表名。RetentionPolicy.RUNTIME表示该注解保存到运行时,因为我们需要在运行时,去读取注解参数来生成具体的SQL。
@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public@interfaceTable{ /** *表名 *@return */ StringtableName()default""; /** *数据库名称 *@return */ StringdbName(); }
注解接口TableField中定义了数据库表名的各个具体字段名称,以及该字段是否忽略(忽略的话就会以数据库表定义默认值填充,DB非null字段的注解不允许出现把ignore注解设置为true)。update注解是在主键在DB重复时,需要更新的字段。
@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public@interfaceTableField{ /** *对应数据库字段名称 *@return */ StringfieldName()default""; /** *是否是主键 *@return */ booleanpk()defaultfalse; /** *是否忽略该字段 *@return */ booleanignore()defaultfalse; /** *当数据存在时,是否更新该字段 *@return */ booleanupdate()defaultfalse; }
第二步,给Bean添加注解
给Bean添加注解(为了简洁省略了import和set/get方法以及其他属性),@TableField(fieldName="company_id")表示companyId字段对应DB表的字段名为"company_id",其中updateTime属性的注解含有ignore=true,表示该属性值会被忽略。另外serialVersionUID属性由于没有@TableField注解,在更新DB时也会被忽略。
代码如下:
@Table(dbName="company",tableName="company_candidate") publicclassCompanyCandidateModelimplementsSerializable{ privatestaticfinallongserialVersionUID=-1234554321773322135L; @TableField(fieldName="company_id") privateintcompanyId; @TableField(fieldName="user_id") privateintuserId; //名片id @TableField(fieldName="card_id") privateintcardId; //facebookId @TableField(fieldName="facebook_id") privatelongfacebookId; @TableField(fieldName="type",update=true) privateinttype; @TableField(fieldName="create_time") privateDatecreateTime; @TableField(fieldName="update_time",ignore=true) privateDateupdateTime; //权重 @TableField(fieldName="weight",update=true) privateintweight; //分值 @TableField(fieldName="score",update=true) privatedoublescore;
第三步,读取注解的反射工具类
读取第二步Bean类的注解的反射工具类。利用反射getAnnotation(TableField.class)读取注解信息,为批量SQL的拼接最好准备。
getTableBeanFieldMap()方法里生成一个LinkedHashMap对象,是为了保证生成插入SQL的field顺序,之后也能按同样的顺序给参数赋值,避免错位。getSqlParamFields()方法也类似,是为了给PreparedStatement设置参数用。
代码如下:
publicclassReflectUtil{ /** *>的map缓存 */ privatestaticfinalMap ,Map >classTableBeanFieldMap=newHashMap ,Map >(); //用来按顺序填充SQL参数,其中存储的Field和classTableBeanFieldMap保存同样的顺序,但数量多出ONDUPLICATEKEYUPDATE部分Field privatestaticfinalMap ,List >sqlParamFieldsMap=newHashMap ,List >(); privateReflectUtil(){}; /** *获取该类上所有@TableField注解,且没有忽略的字段的Map。 *
返回一个有序的LinkedHashMap类型 *
其中key为DB表中的字段,value为Bean类里的属性Field对象 *@paramclazz *@return */ publicstaticMapgetTableBeanFieldMap(Class>clazz){ //从缓存获取 Map fieldsMap=classTableBeanFieldMap.get(clazz); if(fieldsMap==null){ fieldsMap=newLinkedHashMap (); for(Fieldfield:clazz.getDeclaredFields()){//获得所有声明属性数组的一个拷贝 TableFieldannotation=field.getAnnotation(TableField.class); if(annotation!=null&&!annotation.ignore()&&!"".equals(annotation.fieldName())){ field.setAccessible(true);//方便后续获取私有域的值 fieldsMap.put(annotation.fieldName(),field); } } //放入缓存 classTableBeanFieldMap.put(clazz,fieldsMap); } returnfieldsMap; } /** *获取该类上所有@TableField注解,且没有忽略的字段的Map。ONDUPLICATEKEYUPDATE后需要更新的字段追加在list最后,为了填充参数值准备 *
返回一个有序的ArrayList类型 *
其中key为DB表中的字段,value为Bean类里的属性Field对象 *@paramclazz *@return */ publicstaticListgetSqlParamFields(Class>clazz){ //从缓存获取 List sqlParamFields=sqlParamFieldsMap.get(clazz); if(sqlParamFields==null){ //获取所有参数字段 Map fieldsMap=getTableBeanFieldMap(clazz); sqlParamFields=newArrayList (fieldsMap.size()*2); //SQL后段ONDUPLICATEKEYUPDATE需要更新的字段 List updateParamFields=newArrayList (); Iterator >iter=fieldsMap.entrySet().iterator(); while(iter.hasNext()){ Entry entry=(Entry )iter.next(); Fieldfield=entry.getValue(); //insert语句对应sql参数字段 sqlParamFields.add(field); //ONDUPLICATEKEYUPDATE后面语句对应sql参数字段 TableFieldannotation=field.getAnnotation(TableField.class); if(annotation!=null&&!annotation.ignore()&&annotation.update()){ updateParamFields.add(field); } } sqlParamFields.addAll(updateParamFields); //放入缓存 sqlParamFieldsMap.put(clazz,sqlParamFields); } returnsqlParamFields; } /** *获取表名,对象中使用@Table的tableName来标记对应数据库的表名,若未标记则自动将类名转成小写 * *@paramclazz *@return */ publicstaticStringgetTableName(Class>clazz){ Tabletable=clazz.getAnnotation(Table.class); if(table!=null&&table.tableName()!=null&&!"".equals(table.tableName())){ returntable.tableName(); } //当未配置@Table的tableName,自动将类名转成小写 returnclazz.getSimpleName().toLowerCase(); } /** *获取数据库名,对象中使用@Table的dbName来标记对应数据库名 *@paramclazz *@return */ publicstaticStringgetDBName(Class>clazz){ Tabletable=clazz.getAnnotation(Table.class); if(table!=null&&table.dbName()!=null){ //注解@Table的dbName returntable.dbName(); } return""; }
第四步,生成SQL语句
根据上一步的方法,生成真正执行的SQL语句。
insertintocompany_candidate(company_id,user_id,card_id,facebook_id,type,create_time,weight,score)VALUES(?,?,?,?,?,?,?,?)ONDUPLICATEKEYUPDATEtype=?,weight=?,score=?
代码如下:
publicclassSQLUtil{ privatestaticfinalcharCOMMA=','; privatestaticfinalcharBRACKETS_BEGIN='('; privatestaticfinalcharBRACKETS_END=')'; privatestaticfinalcharQUESTION_MARK='?'; privatestaticfinalcharEQUAL_SIGN='='; privatestaticfinalStringINSERT_BEGIN="INSERTINTO"; privatestaticfinalStringINSERT_VALURS="VALUES"; privatestaticfinalStringDUPLICATE_UPDATE="ONDUPLICATEKEYUPDATE"; //数据库表名和对应insertupdateSQL的缓存 privatestaticfinalMaptableInsertSqlMap=newHashMap (); /** *获取插入的sql语句,对象中使用@TableField的fieldName来标记对应数据库的列名,若未标记则忽略 *必须标记@TableField(fieldName="company_id")注解 *@paramtableName *@paramfieldsMap *@return *@throwsException */ publicstaticStringgetInsertSql(StringtableName,Map fieldsMap)throwsException{ Stringsql=tableInsertSqlMap.get(tableName); if(sql==null){ StringBuildersbSql=newStringBuilder(300).append(INSERT_BEGIN); StringBuildersbValue=newStringBuilder(INSERT_VALURS); StringBuildersbUpdate=newStringBuilder(100).append(DUPLICATE_UPDATE); sbSql.append(tableName); sbSql.append(BRACKETS_BEGIN); sbValue.append(BRACKETS_BEGIN); Iterator >iter=fieldsMap.entrySet().iterator(); while(iter.hasNext()){ Entry entry=(Entry )iter.next(); StringtableFieldName=entry.getKey(); Fieldfield=entry.getValue(); sbSql.append(tableFieldName); sbSql.append(COMMA); sbValue.append(QUESTION_MARK); sbValue.append(COMMA); TableFieldtableField=field.getAnnotation(TableField.class); if(tableField!=null&&tableField.update()){ sbUpdate.append(tableFieldName); sbUpdate.append(EQUAL_SIGN); sbUpdate.append(QUESTION_MARK); sbUpdate.append(COMMA); } } //去掉最后的逗号 sbSql.deleteCharAt(sbSql.length()-1); sbValue.deleteCharAt(sbValue.length()-1); sbSql.append(BRACKETS_END); sbValue.append(BRACKETS_END); sbSql.append(sbValue); if(!sbUpdate.toString().equals(DUPLICATE_UPDATE)){ sbUpdate.deleteCharAt(sbUpdate.length()-1); sbSql.append(sbUpdate); } sql=sbSql.toString(); tableInsertSqlMap.put(tableName,sql); } returnsql; }
第五步,批量SQL插入实现
从连接池获取Connection,SQLUtil.getInsertSql()获取执行的SQL语句,根据sqlParamFields来为PreparedStatement填充参数值。当循环的值集合到达batchNum时就提交一次。
代码如下:
/** *批量插入,如果主键一致则更新。结果返回更新记录条数
*@paramdataList *要插入的对象List *@parambatchNum *每次批量插入条数 *@return更新记录条数 */ publicintbatchInsertSQL(ListdataList,intbatchNum)throwsException{ if(dataList==null||dataList.isEmpty()){ return0; } Class>clazz=dataList.get(0).getClass(); StringtableName=ReflectUtil.getTableName(clazz); StringdbName=ReflectUtil.getDBName(clazz); Connectionconnnection=null; PreparedStatementpreparedStatement=null; //获取所有需要更新到DB的属性域 MapfieldsMap=ReflectUtil.getTableBeanFieldMap(dataList.get(0).getClass()); //根据需要插入更新的字段生成SQL语句 Stringsql=SQLUtil.getInsertSql(tableName,fieldsMap); log.debug("preparetostartbatchoperation,sql="+sql+",dbName="+dbName); //获取和SQL语句同样顺序的填充参数Fields List sqlParamFields=ReflectUtil.getSqlParamFields(dataList.get(0).getClass()); //最终更新结果条数 intresult=0; intparameterIndex=1;//SQL填充参数开始位置为1 //执行错误的对象 List
最后,批量工具类使用例子
在mysql下的开发环境下测试,5万条数据大概13秒。
ListupdateDataList=newArrayList (50000); //...为updateDataList填充数据 intresult=batchJdbcTemplate.batchInsertSQL(updateDataList,50);
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对毛票票的支持。如果你想了解更多相关内容请查看下面相关链接