zoukankan      html  css  js  c++  java
  • Mysql批量插入返回Id错乱(原因分析)

    在项目中经常会有如下场景:

    往数据库中批量插入一批数据后,需要知道哪些插入成功,哪些插入失败了。

    这时候往往会有两种思路,一个是在插入之前判断相同的记录是否存在,过滤掉重复的数据;另外一种就是边插入边判断,动态过滤。

    第一种方式对于数据量过大的情况并不适用,为了采用第二种方法,我们使用了“Mybatis批量插入返回自增主键”的方式进行处理。

    mysql插入操作后返回主键是jdbc的功能,用到的方法是getGeneratedKeys()方法,使用此方法获取自增数据,性能良好,只需要一次交互。

            String sql = "insert IGNORE into user(user_name,password,nick_name,mail) VALUES (?,?,?,?)";
            List<User> userList = Lists.newArrayList();
            userList.add(new User("2","2","2","2"));
            userList.add(new User("3","3","3","3"));
            userList.add(new User("4","4","4","4"));
    
            try {
                conn = DatabaseUtil.getConnectDB();
                ps = conn.prepareStatement(sql,PreparedStatement.RETURN_GENERATED_KEYS);
                for(User user : userList){
                    ps.setString(1, user.getUserName());
                    ps.setString(2, user.getPassword());
                    ps.setString(3, user.getNickName());
                    ps.setString(4, user.getMail());
                    ps.addBatch();
                }
                ps.executeBatch();
    
                ResultSet generatedKeys = ps.getGeneratedKeys();
                ArrayList<Integer> list = Lists.newArrayList();
                while (generatedKeys.next()){
                    list.add(generatedKeys.getInt(1));
                }
            } catch (SQLException e) {
                LOGGER.error("error:{}", e.getMessage(), e);
            } finally {
                DatabaseUtil.close(conn, ps, null);
            }

     getGeneratedKeys()返回的就是刚刚生成的id。

    相应的如果在mybatis中使用的话,只需要在mybatis的mapper文件中设置参数“keyProperty="id" useGeneratedKeys="true"”即可。例如:

       <insert id="insertListSelective" keyColumn="id" keyProperty="id"
                parameterType="Bill" useGeneratedKeys="true">
           
       </insert>

    为了满足我们的需求,我们需要对上述sql进行改造,思路就是在批量插入的时候,如果遇到重复的数据,就忽略,继续插入下一个记录,这时我们采用的是ignore:

    MySQL 提供了Ignore 用来避免数据的重复插入.
    
    IGNORE :
    若有导致unique key 冲突的记录,则该条记录不会被插入到数据库中.
    示例:
    INSERT IGNORE INTO `table_name` (`email`, `phone`, `user_id`) VALUES ('test9@163.com', '99999', '9999');
    这样当有重复记录就会忽略,执行后返回数字0

     但是经过多次测试发现,对象返回的id错乱。

     对于上述情况,如果没有重复数据就不会出现问题,于是就猜测是因为ignore的原因,经过查看源码,验证了自己的想法:

    public void processBatch(MappedStatement ms, Statement stmt, Collection<Object> parameters) {
        ResultSet rs = null;
        try {
          rs = stmt.getGeneratedKeys();
          final Configuration configuration = ms.getConfiguration();
          final TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
    //指的是keyProperty="id" 这种参数 final String[] keyProperties = ms.getKeyProperties();
    //ResultSet的元数据,指的是有关 ResultSet 中列的名称和类型的信息。 final ResultSetMetaData rsmd = rs.getMetaData(); TypeHandler<?>[] typeHandlers = null; if (keyProperties != null && rsmd.getColumnCount() >= keyProperties.length) { for (Object parameter : parameters) { // there should be one row for each statement (also one for each parameter) if (!rs.next()) { break; } final MetaObject metaParam = configuration.newMetaObject(parameter); if (typeHandlers == null) { typeHandlers = getTypeHandlers(typeHandlerRegistry, metaParam, keyProperties, rsmd); }
    //设置返回的keyProperty(反射) populateKeys(rs, metaParam, keyProperties, typeHandlers); } } } catch (Exception e) { throw new ExecutorException("Error getting generated key or setting result to parameter object. Cause: " + e, e); } finally { if (rs != null) { try { rs.close(); } catch (Exception e) { // ignore } } } }

    private void populateKeys(ResultSet rs, MetaObject metaParam, String[] keyProperties, TypeHandler<?>[] typeHandlers) throws SQLException {
    for (int i = 0; i < keyProperties.length; i++) {
    String property = keyProperties[i];
    TypeHandler<?> th = typeHandlers[i];
    if (th != null) {
    Object value = th.getResult(rs, i + 1);
    metaParam.setValue(property, value);
    }
    }
    }

    注意代码中的这一句注释: // there should be one row for each statement (also one for each parameter)    ,翻译过来就是每一个元素对应一个ResultSet

    分析这段循环代码:

    
    
    for (Object parameter : parameters) {
              // there should be one row for each statement (also one for each parameter)
              if (!rs.next()) {
                break;
              }
              final MetaObject metaParam = configuration.newMetaObject(parameter);
              if (typeHandlers == null) {
                typeHandlers = getTypeHandlers(typeHandlerRegistry, metaParam, keyProperties, rsmd);
              }
              //设置返回的keyProperty(反射)
              populateKeys(rs, metaParam, keyProperties, typeHandlers);
    }
    
    
    

    循环遍历要插入的元素,然后通过反射方式设置主键的值,但是注意每次遍历插入元素的时候,ResultSet也在往下遍历,这时候就有问题了:
    stmt.getGeneratedKeys()永远返回的都是插入成功的记录的id,如果插入的集合中有几个重复的元素,这时候插入的集合元素与返回的ResultSet就对应不上了,所以才会造成之前的那个问题。

    为了避免上述的问题,现在我们采用的方式是单条插入,挨个返回id。

     

  • 相关阅读:
    apache域名跳转
    apache配置防盗链
    apache配置静态缓存
    apache通过rewrite限制某个目录
    apache不记录指定类型的访问日志
    apache访问日志
    apache用户认证
    apache访问控制
    apache禁止默认虚拟主机
    mysql主从复制架构配置
  • 原文地址:https://www.cnblogs.com/haolnu/p/8290078.html
Copyright © 2011-2022 走看看