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 (‘[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錯亂(原因分析)