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。

 

原文地址:https://www.cnblogs.com/haolnu/p/8290078.html