在項目中經常會有如下場景:
往資料庫中批量插入一批資料後,需要知道哪些插入成功,哪些插入失敗了。
這時候往往會有兩種思路,一個是在插入之前判斷相同的記錄是否存在,過濾掉重複的資料;另外一種就是邊插入邊判斷,動态過濾。
第一種方式對于資料量過大的情況并不适用,為了采用第二種方法,我們使用了“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。