天天看點

mysql批量插入傳回id_Mysql批量插入傳回Id錯亂(原因分析)

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

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

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

第一種方式對于資料量過大的情況并不适用,為了采用第二種方法,我們使用了“Mybatis批量插入傳回自增主鍵”的方式進行處理。

mysql插入操作後傳回主鍵是jdbc的功能,用到的方法是getGeneratedKeys()方法,使用此方法擷取自增資料,性能良好,隻需要一次互動。

String sql = "insert IGNORE into user(user_name,password,nick_name,mail) VALUES (?,?,?,?)";

List 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 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"”即可。例如:

parameterType="Bill" useGeneratedKeys="true">

為了滿足我們的需求,我們需要對上述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 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。