通过反射注解批量插入数据到DB的实现方法

批量导入思路

最近遇到一个需要批量导入数据问题。后来考虑运用反射做成一个工具类,思路是首先定义注解接口,在bean类上加注解,运行时通过反射获取传入Bean的注解,自动生成需要插入DB的SQL,根据设置的参数值批量提交。不需要写具体的SQL,也没有DAO的实现,这样一来批量导入的实现就和具体的数据库表彻底解耦。实际批量执行的SQL如下:

insert into company_candidate(company_id,user_id,card_id,facebook_id,type,create_time,weight,score) VALUES (?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE type=?,weight=?,score=?

第一步,定义注解接口

注解接口Table中定义了数据库名和表名。RetentionPolicy.RUNTIME表示该注解保存到运行时,因为我们需要在运行时,去读取注解参数来生成具体的SQL。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Table {
  /**
   * 表名
   * @return
   */
  String tableName() default "";
  /**
   * 数据库名称
   * @return
   */
  String dbName();
}

注解接口TableField中定义了数据库表名的各个具体字段名称,以及该字段是否忽略(忽略的话就会以数据库表定义默认值填充,DB非null字段的注解不允许出现把ignore注解设置为true)。update注解是在主键在DB重复时,需要更新的字段。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface TableField {
  /**
   * 对应数据库字段名称
   * @return
   */
  String fieldName() default "";
  /**
   * 是否是主键
   * @return
   */
  boolean pk() default false;
  /**
   * 是否忽略该字段
   * @return
   */
  boolean ignore() default false;
  /**
   * 当数据存在时,是否更新该字段
   * @return
   */
  boolean update() default false;
}

第二步,给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")
public class CompanyCandidateModel implements Serializable{
 private static final long serialVersionUID = -1234554321773322135L;
 @TableField(fieldName = "company_id")
 private int companyId;
 @TableField(fieldName = "user_id")
 private int userId;
 //名片id
 @TableField(fieldName = "card_id")
 private int cardId;
 //facebookId
 @TableField(fieldName = "facebook_id")
 private long facebookId;
  @TableField(fieldName="type", update = true)
 private int type;
 @TableField(fieldName = "create_time")
 private Date createTime;
 @TableField(fieldName = "update_time", ignore=true)
 private Date updateTime;
 // 权重
  @TableField(fieldName="weight", update = true)
 private int weight;
 // 分值
  @TableField(fieldName="score", update = true)
 private double score;

第三步,读取注解的反射工具类

读取第二步Bean类的注解的反射工具类。利用反射getAnnotation(TableField.class)读取注解信息,为批量SQL的拼接最好准备。

getTableBeanFieldMap()方法里生成一个LinkedHashMap对象,是为了保证生成插入SQL的field顺序,之后也能按同样的顺序给参数赋值,避免错位。getSqlParamFields()方法也类似,是为了给PreparedStatement设置参数用。

代码如下:

public class ReflectUtil {
  /**
   * <Class,<表定义Field名,Bean定义Field>>的map缓存
   */
  private static final Map<Class<?>, Map<string field="">> classTableBeanFieldMap = new HashMap<Class<?>, Map<string field="">>();
  // 用来按顺序填充SQL参数,其中存储的Field和classTableBeanFieldMap保存同样的顺序,但数量多出ON DUPLICATE KEY UPDATE部分Field
  private static final Map<Class<?>, List<field>> sqlParamFieldsMap = new HashMap<Class<?>, List<field>>();
  private ReflectUtil(){};
  /**
   * 获取该类上所有@TableField注解,且没有忽略的字段的Map。
   * <br />返回一个有序的LinkedHashMap类型
   * <br />其中key为DB表中的字段,value为Bean类里的属性Field对象
   * @param clazz
   * @return
   */
  public static Map<string field=""> getTableBeanFieldMap(Class<?> clazz) {
   // 从缓存获取
   Map<string field=""> fieldsMap = classTableBeanFieldMap.get(clazz);
   if (fieldsMap == null) {
   fieldsMap = new LinkedHashMap<string field="">();
      for (Field field : clazz.getDeclaredFields()) {// 获得所有声明属性数组的一个拷贝
       TableField annotation = field.getAnnotation(TableField.class);
        if (annotation != null && !annotation.ignore() && !"".equals(annotation.fieldName())) {
          field.setAccessible(true);// 方便后续获取私有域的值
         fieldsMap.put(annotation.fieldName(), field);
        }
  }
      // 放入缓存
      classTableBeanFieldMap.put(clazz, fieldsMap);
   }
   return fieldsMap;
  }
  /**
   * 获取该类上所有@TableField注解,且没有忽略的字段的Map。ON DUPLICATE KEY UPDATE后需要更新的字段追加在list最后,为了填充参数值准备
   * <br />返回一个有序的ArrayList类型
   * <br />其中key为DB表中的字段,value为Bean类里的属性Field对象
   * @param clazz
   * @return
   */
  public static List<field> getSqlParamFields(Class<?> clazz) {
   // 从缓存获取
   List<field> sqlParamFields = sqlParamFieldsMap.get(clazz);
   if (sqlParamFields == null) {
   // 获取所有参数字段
     Map<string field=""> fieldsMap = getTableBeanFieldMap(clazz);
   sqlParamFields = new ArrayList<field>(fieldsMap.size() * 2);
     // SQL后段ON DUPLICATE KEY UPDATE需要更新的字段
     List<field> updateParamFields = new ArrayList<field>();
   Iterator<Entry<string field="">> iter = fieldsMap.entrySet().iterator();
   while (iter.hasNext()) {
    Entry<string field=""> entry = (Entry<string field="">) iter.next();
    Field field = entry.getValue();
    // insert语句对应sql参数字段
    sqlParamFields.add(field);
        // ON DUPLICATE KEY UPDATE后面语句对应sql参数字段
        TableField annotation = field.getAnnotation(TableField.class);
    if (annotation != null && !annotation.ignore() && annotation.update()) {
    updateParamFields.add(field);
    }
   }
   sqlParamFields.addAll(updateParamFields);
      // 放入缓存
   sqlParamFieldsMap.put(clazz, sqlParamFields);
   }
   return sqlParamFields;
  }
  /**
   * 获取表名,对象中使用@Table的tableName来标记对应数据库的表名,若未标记则自动将类名转成小写
   *
   * @param clazz
   * @return
   */
  public static String getTableName(Class<?> clazz) {
    Table table = clazz.getAnnotation(Table.class);
    if (table != null && table.tableName() != null && !"".equals(table.tableName())) {
      return table.tableName();
    }
    // 当未配置@Table的tableName,自动将类名转成小写
    return clazz.getSimpleName().toLowerCase();
  }
  /**
   * 获取数据库名,对象中使用@Table的dbName来标记对应数据库名
   * @param clazz
   * @return
   */
  public static String getDBName(Class<?> clazz) {
    Table table = clazz.getAnnotation(Table.class);
    if (table != null && table.dbName() != null) {
      // 注解@Table的dbName
      return table.dbName();
    }
    return "";
  }

第四步,生成SQL语句

根据上一步的方法,生成真正执行的SQL语句。

insert into company_candidate(company_id,user_id,card_id,facebook_id,type,create_time,weight,score) VALUES (?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE type=?,weight=?,score=?

代码如下:

public class SQLUtil {
  private static final char COMMA = ',';
  private static final char BRACKETS_BEGIN = '(';
  private static final char BRACKETS_END = ')';
  private static final char QUESTION_MARK = '?';
  private static final char EQUAL_SIGN = '=';
  private static final String INSERT_BEGIN = "INSERT INTO ";
  private static final String INSERT_VALURS = " VALUES ";
  private static final String DUPLICATE_UPDATE = " ON DUPLICATE KEY UPDATE ";
  // 数据库表名和对应insertupdateSQL的缓存
  private static final Map<string string=""> tableInsertSqlMap = new HashMap<string string="">();
  /**
   * 获取插入的sql语句,对象中使用@TableField的fieldName来标记对应数据库的列名,若未标记则忽略
   * 必须标记@TableField(fieldName = "company_id")注解
   * @param tableName
   * @param fieldsMap
   * @return
   * @throws Exception
   */
  public static String getInsertSql(String tableName, Map<string field=""> fieldsMap) throws Exception {
   String sql = tableInsertSqlMap.get(tableName);
   if (sql == null) {
   StringBuilder sbSql = new StringBuilder(300).append(INSERT_BEGIN);
   StringBuilder sbValue = new StringBuilder(INSERT_VALURS);
   StringBuilder sbUpdate = new StringBuilder(100).append(DUPLICATE_UPDATE);
   sbSql.append(tableName);
   sbSql.append(BRACKETS_BEGIN);
   sbValue.append(BRACKETS_BEGIN);
   Iterator<Entry<string field="">> iter = fieldsMap.entrySet().iterator();
   while (iter.hasNext()) {
    Entry<string field=""> entry = (Entry<string field="">) iter.next();
    String tableFieldName = entry.getKey();
    Field field = entry.getValue();
    sbSql.append(tableFieldName);
    sbSql.append(COMMA);
    sbValue.append(QUESTION_MARK);
    sbValue.append(COMMA);
    TableField tableField = 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);
   }
    return sql;
  }

第五步,批量SQL插入实现

从连接池获取Connection,SQLUtil.getInsertSql()获取执行的SQL语句,根据sqlParamFields来为PreparedStatement填充参数值。当循环的值集合到达batchNum时就提交一次。

代码如下:

  /**
   * 批量插入,如果主键一致则更新。结果返回更新记录条数<br />
   * @param dataList
   *      要插入的对象List
   * @param batchNum
   *      每次批量插入条数
   * @return 更新记录条数
   */
  public int batchInsertSQL(List<? extends Object> dataList, int batchNum) throws Exception {
   if (dataList == null || dataList.isEmpty()) {
   return 0;
   }
    Class<?> clazz = dataList.get(0).getClass();
    String tableName = ReflectUtil.getTableName(clazz);
    String dbName = ReflectUtil.getDBName(clazz);
    Connection connnection = null;
    PreparedStatement preparedStatement = null;
    // 获取所有需要更新到DB的属性域
    Map<string field=""> fieldsMap = ReflectUtil.getTableBeanFieldMap(dataList.get(0).getClass());
    // 根据需要插入更新的字段生成SQL语句
    String sql = SQLUtil.getInsertSql(tableName, fieldsMap);
    log.debug("prepare to start batch operation , sql = " + sql + " , dbName = " + dbName);
    // 获取和SQL语句同样顺序的填充参数Fields
    List<field> sqlParamFields = ReflectUtil.getSqlParamFields(dataList.get(0).getClass());
    // 最终更新结果条数
    int result = 0;
    int parameterIndex = 1;// SQL填充参数开始位置为1
    // 执行错误的对象
    List<object> errorsRecords = new ArrayList</object><object>(batchNum);//指定数组大小
    // 计数器,batchNum提交后内循环累计次数
    int innerCount = 0;
    try {
      connnection = this.getConnection(dbName);
      // 设置非自动提交
      connnection.setAutoCommit(false);
      preparedStatement = connnection.prepareStatement(sql);
      // 当前操作的对象
      Object object = null;
      int totalRecordCount = dataList.size();
      for (int current = 0; current < totalRecordCount; current++) {
        innerCount++;
        object = dataList.get(current);
       parameterIndex = 1;// 开始参数位置为1
       for(Field field : sqlParamFields) {
       // 放入insert语句对应sql参数
          preparedStatement.setObject(parameterIndex++, field.get(object));
       }
       errorsRecords.add(object);
        preparedStatement.addBatch();
        // 达到批量次数就提交一次
        if (innerCount >= batchNum || current >= totalRecordCount - 1) {
          // 执行batch操作
          preparedStatement.executeBatch();
          preparedStatement.clearBatch();
          // 提交
          connnection.commit();
          // 记录提交成功条数
          result += innerCount;
          innerCount = 0;
          errorsRecords.clear();
        }
        // 尽早让GC回收
        dataList.set(current, null);
      }
      return result;
    } catch (Exception e) {
      // 失败后处理方法
      CallBackImpl.getInstance().exectuer(sql, errorsRecords, e);
      BatchDBException be = new BatchDBException("batch run error , dbName = " + dbName + " sql = " + sql, e);
      be.initCause(e);
      throw be;
    } finally {
      // 关闭
      if (preparedStatement != null) {
       preparedStatement.clearBatch();
        preparedStatement.close();
      }
      if (connnection != null)
        connnection.close();
    }
  }

最后,批量工具类使用例子

在mysql下的开发环境下测试,5万条数据大概13秒。

List<companycandidatemodel> updateDataList = new ArrayList<companycandidatemodel>(50000);
// ...为updateDataList填充数据
int result = batchJdbcTemplate.batchInsertSQL(updateDataList, 50);

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对我们的支持。如果你想了解更多相关内容请查看下面相关链接

(0)

相关推荐

  • 实例讲解Java中动态代理和反射机制

    反射机制 Java语言提供的一种基础功能,通过反射,我们可以操作这个类或对象,比如获取这个类中的方法.属性和构造方法等. 动态代理:分为JDK动态代理.cglib动态代理(spring中的动态代理). 静态代理 预先(编译期间)确定了代理者与被代理者之间的关系,也就是说,若代理类在程序运行前就已经存在了,这种情况就叫静态代理 动态代理 代理类在程序运行时创建的代理方式.也就是说,代理类并不是在Java代码中定义的,而是在运行期间根据我们在Java代码中的"指示"动态生成的. 动态代理比

  • 基于Java反射的map自动装配JavaBean工具类设计示例代码

    前言 JavaBean是一个特殊的java类,本文将给大家详细介绍关于基于Java反射的map自动装配JavaBean工具类设计的相关内容,下面话不多说了,来一起看看详细的介绍吧 方法如下 我们平时在用Myabtis时不是常常需要用map来传递参数,大体是如下的步骤: public List<Role> findRoles(Map<String,Object> param); <select id="dindRoles" parameterType=&qu

  • Java反射机制的精髓讲解

    1,什么是反射? java的反射,允许程序在运行时,创建一个对象,获取一个类的所有相关信息等. 2,Class类 要了解反射,就绕不开Class类. 我们平时开发的类,例如ClassA,一般会有一些属性,会有几个构造方法,也会有一些普通方法,我们还可以使用ClassA来创建对象,例如ClassA classA = new ClassA(). java程序在运行时,其实是很多类的很多个对象之间的协作.jvm如何管理这些类呢?它如何知道各个类的名称,每个类都有哪些属性和哪些方法呢? jvm会给每个类

  • 详解Golang利用反射reflect动态调用方法

    编程语言中反射的概念 在计算机科学领域,反射是指一类应用,它们能够自描述和自控制.也就是说,这类应用通过采用某种机制来实现对自己行为的描述(self-representation)和监测(examination),并能根据自身行为的状态和结果,调整或修改应用所描述行为的状态和相关的语义. 每种语言的反射模型都不同,并且有些语言根本不支持反射.Golang语言实现了反射,反射机制就是在运行时动态的调用对象的方法和属性,官方自带的reflect包就是反射相关的,只要包含这个包就可以使用. 多插一句,

  • 利用lambda表达式树优化反射详解

    前言 本节重点不讲反射机制,而是讲lambda表达式树来替代反射中常用的获取属性和方法,来达到相同的效果但却比反射高效. 每个人都知道,用反射调用一个方法或者对属性执行SetValue和GetValue操作的时候都会比直接调用慢很多,这其中设计到CLR中内部的处理,不做深究.然而,我们在某些情况下又无法不使用反射,比如:在一个ORM框架中,你要将一个DataRow转化为一个对象,但你又不清楚该对象有什么属性,这时候你就需要写一个通用的泛型方法来处理,以下代码写得有点恶心,但不妨碍理解意思: //

  • 使用反射机制控制Toast的显示时间

    本文为大家分享了使用反射机制控制Toast显示时间的具体代码,供大家参考,具体内容如下 1.Toast源码分析: Toast的默认view是在transient_notification.xml中定义的一个TextView,如果需要设置Toast的界面,可以通过setView方法实现:如果需要设置Toast默认显示的位置,可以通过setGravity或者setMargin方法进行设置,值得一提的是setMargin方法的参数范围是0-1即它是屏幕的百分比,如setMargin(0.1,0.1).

  • Java高级特性之反射机制实例详解

    本文实例讲述了Java高级特性之反射机制.分享给大家供大家参考,具体如下: 老规矩我们还是先提出几个问题,一门技术必然要能解决一定的问题,才有去学习掌握它的价值 一. 什么是反射? 二.反射能做什么? 一. 什么是反射? 用在Java身上指的是我们可以于运行时加载.探知.使用编译期间完全未知的classes.换句话说,Java程序可以加载一个运行时才得知名称的class,获悉其完整构造(但不包括methods定义),并生成其对象实体.或对其fields设值.或唤起其methods. 如果你是一个

  • Kotlin中的反射机制深入讲解

    前言 Java中的反射机制,使得我们可以在运行期获取Java类的字节码文件中的构造函数,成员变量,成员函数等信息.这一特性使得反射机制被常常用在框架中,想要比较系统的了解Kotlin中的反射,先从Java的反射说起. Java中的反射 通常我们写好的.java源码文件,经过javac的编译,最终生成了.class字节码文件.这些字节码文件是与平台无关的,使用时通过Classloader去加载这些.class字节码文件,从而让程序按照我们编写好的业务逻辑运行.Java的反射主要是从这些.class

  • ES6 如何改变JS内置行为的代理与反射

    代理(Proxy)可以拦截并改变 JS 引擎的底层操作,如数据读取.属性定义.函数构造等一系列操作.ES6 通过对这些底层内置对象的代理陷阱和反射函数,让开发者能进一步接近 JS 引擎的能力. 一.代理与反射的基本概念 什么是代理和反射呢? 代理是用来替代另一个对象(target),JS 通过new Proxy()创建一个目标对象的代理,该代理与该目标对象表面上可以被当作同一个对象来对待. 当目标对象上的进行一些特定的底层操作时,代理允许你拦截这些操作并且覆写它,而这原本只是 JS 引擎的内部能

  • 实例讲解Java基础之反射

    前期准备 编写一个真实类phone,实现list接口 public class Phone implements List { public double price; public String name; public Phone() { } public Phone(double price, String name) { this.price = price; this.name = name; } public double getPrice() { return price; } p

随机推荐