通过反射注解批量插入数据到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){
//从缓存获取
MapfieldsMap=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){
//从缓存获取
ListsqlParamFields=sqlParamFieldsMap.get(clazz);
if(sqlParamFields==null){
//获取所有参数字段
MapfieldsMap=getTableBeanFieldMap(clazz);
sqlParamFields=newArrayList(fieldsMap.size()*2);
//SQL后段ONDUPLICATEKEYUPDATE需要更新的字段
ListupdateParamFields=newArrayList();
Iterator>iter=fieldsMap.entrySet().iterator();
while(iter.hasNext()){
Entryentry=(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,MapfieldsMap)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()){
Entryentry=(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);
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对毛票票的支持。如果你想了解更多相关内容请查看下面相关链接