1. 程式人生 > >Mysql批量插入返回Id錯亂(原因分析)

Mysql批量插入返回Id錯亂(原因分析)

erro smd r文件 setvalue insert 需要 名稱 conf nal

在項目中經常會有如下場景:

往數據庫中批量插入一批數據後,需要知道哪些插入成功,哪些插入失敗了。

這時候往往會有兩種思路,一個是在插入之前判斷相同的記錄是否存在,過濾掉重復的數據;另外一種就是邊插入邊判斷,動態過濾。

第一種方式對於數據量過大的情況並不適用,為了采用第二種方法,我們使用了“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 (‘[email protected]‘, ‘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。

Mysql批量插入返回Id錯亂(原因分析)