通过源码分析Mybatis是如何返回数据库生成的自增主键值?
在Mybatis中,執行insert操作時,如果我們希望返回數據庫生成的自增主鍵值,那么就需要使用到KeyGenerator對象。
需要注意的是,KeyGenerator的作用,是返回數據庫生成的自增主鍵值,而不是生成數據庫的自增主鍵值。返回的主鍵值放到哪兒呢?放到parameter object的主鍵屬性上。
下面看看其接口定義。
public?interface?KeyGenerator?{void?processBefore(Executor?executor,?MappedStatement?ms,?Statement?stmt,?Object?parameter);void?processAfter(Executor?executor,?MappedStatement?ms,?Statement?stmt,?Object?parameter); }接口定義還是比較簡單的,就是在insert前、insert后,策略處理主鍵值。
Jdbc3KeyGenerator:用于處理數據庫支持自增主鍵的情況,如MySQL的auto_increment。
NoKeyGenerator:空實現,不需要處理主鍵。
SelectKeyGenerator:用于處理數據庫不支持自增主鍵的情況,比如Oracle的sequence序列。
上面都比較泛泛而談,我們來點實際的,看看它們都是如何工作的。
1. JDBC實現insert后,返回自增主鍵值的原理
Class.forName("com.mysql.jdbc.Driver");Connection?conn?=?DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb",?"root",?"123"); conn.setAutoCommit(false); PreparedStatement?pstm?=?conn.prepareStatement("insert?into?students(name,?email)?values(?,??)", Statement.RETURN_GENERATED_KEYS);pstm.setString(1,?"name1"); pstm.setString(2,?"email1"); pstm.addBatch(); pstm.setString(1,?"name2"); pstm.setString(2,?"email2"); pstm.addBatch(); pstm.executeBatch(); //?返回自增主鍵值 ResultSet?rs?=?pstm.getGeneratedKeys(); while?(rs.next())?{Object?value?=?rs.getObject(1);System.out.println(value);} conn.commit(); rs.close(); pstm.close(); conn.close();output: 246 247以上代碼,僅作為演示使用。Mybatis是對JDBC的封裝,其Jdbc3KeyGenerator類,就是使用上面的原理,來返回數據庫生成的主鍵值的。
2. Jdbc3KeyGenerator源碼解讀
public?class?Jdbc3KeyGenerator?implements?KeyGenerator?{@Overridepublic?void?processBefore(Executor?executor,?MappedStatement?ms,?Statement?stmt,?Object?parameter)?{//?do?nothing}@Overridepublic?void?processAfter(Executor?executor,?MappedStatement?ms,?Statement?stmt,?Object?parameter)?{processBatch(ms,?stmt,?getParameters(parameter));}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();final?String[]?keyProperties?=?ms.getKeyProperties();final?ResultSetMetaData?rsmd?=?rs.getMetaData();TypeHandler<?>[]?typeHandlers?=?null;if?(keyProperties?!=?null?&&?rsmd.getColumnCount()?>=?keyProperties.length)?{//?給參數object對象的屬性賦主鍵值(批量插入,可能是多個)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);}//?賦值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++)?{TypeHandler<?>?th?=?typeHandlers[i];if?(th?!=?null)?{Object?value?=?th.getResult(rs,?i?+?1);//?反射賦值metaParam.setValue(keyProperties[i],?value);}}} //...Mapper.Xml配置方式。
<insert?id="insertStudents"?useGeneratedKeys="true"?keyProperty="studId"?parameterType="Student">3. NoKeyGenerator源碼解讀
完全是空實現,沒啥可說的。
4. SelectKeyGenerator的原理
?<insert?id="insertStudent"?parameterType="Student"?><selectKey?keyProperty="studId"?resultType="int"?order="BEFORE">?SELECT?ELEARNING.STUD_ID_SEQ.NEXTVAL?FROM?DUAL?</selectKey>INSERT?INTOSTUDENTS(STUD_ID,?NAME,?EMAIL,?DOB,?PHONE)VALUES(#{studId},?#{name},#{email},?#{dob},?#{phone})</insert>在執行insert之前,先發起一個sql查詢,將返回的序列值賦值給Student的stuId屬性,然后再執行insert操作,這樣表中的stud_id字段就有值了。order="BEFORE"表示insert前執行,比如取sequence序列值;order="AFTER"表示insert之后執行,比如使用觸發器給主鍵stud_id賦值。比較簡單,我就不再貼源碼了。
注意:由于selectKey本身返回單個序列主鍵值,也就無法支持批量insert操作并返回主鍵id列表了。如果要執行批量insert,請選擇使用for循環執行多次插入操作。
5. KeyGenerator的創建過程
每一個MappedStatement,都有一個非空的KeyGenerator引用。
org.apache.ibatis.mapping.MappedStatement.Builder.Builder()構造方法賦初始值源碼。
mappedStatement.keyGenerator?=?configuration.isUseGeneratedKeys()?&&?SqlCommandType.INSERT.equals(sqlCommandType)???new?Jdbc3KeyGenerator()?:?new?NoKeyGenerator();org.apache.ibatis.builder.xml.XMLStatementBuilder.parseStatementNode()覆蓋KeyGenerator初始值的源碼。
String?keyStatementId?=?id?+?SelectKeyGenerator.SELECT_KEY_SUFFIX; if?(configuration.hasKeyGenerator(keyStatementId))?{//?表示存在selectKey獲取主鍵值方式keyGenerator?=?configuration.getKeyGenerator(keyStatementId);}?else?{keyGenerator?=?context.getBooleanAttribute("useGeneratedKeys",configuration.isUseGeneratedKeys()?&&?SqlCommandType.INSERT.equals(sqlCommandType))??new?Jdbc3KeyGenerator()?:?new?NoKeyGenerator();}org.apache.ibatis.builder.xml.XMLStatementBuilder.parseSelectKeyNode()解析元素,構建SelectKeyGenerator的源碼。
MappedStatement?keyStatement?=?configuration.getMappedStatement(id,?false); configuration.addKeyGenerator(id,?new?SelectKeyGenerator(keyStatement,?executeBefore));因此,只有SelectKeyGenerator會保存至Configuration對象的Map<String, KeyGenerator> keyGenerators屬性當中。元素,會被Mybatis解析為一個MappedStatement對象,并作為構造參數傳遞至SelectKeyGenerator內保存起來。
public?class?SelectKeyGenerator?implements?KeyGenerator?{public?static?final?String?SELECT_KEY_SUFFIX?=?"!selectKey";private?boolean?executeBefore;private?MappedStatement?keyStatement; //...Map<String, KeyGenerator> keyGenerators的存儲結構如下。
{insertStudent!selectKey=org.apache.ibatis.executor.keygen.SelectKeyGenerator@59d016c9,? com.mybatis3.mappers.StudentMapper.insertStudent!selectKey=org.apache.ibatis.executor.keygen.SelectKeyGenerator@59d016c9}至此,每一個MappedStatement對象,都恰當的綁定了一個KeyGenerator對象,就可以開始工作了。
6. KeyGenerator的使用過程
keyGenerator.processBefore()方法調用時機。
org.apache.ibatis.executor.statement.BaseStatementHandler.BaseStatementHandler()構造方法源碼。
if?(boundSql?==?null)?{//?調用keyGenerator.processBefore()方法generateKeys(parameterObject);boundSql?=?mappedStatement.getBoundSql(parameterObject);} //?...protected?void?generateKeys(Object?parameter)?{KeyGenerator?keyGenerator?=?mappedStatement.getKeyGenerator();keyGenerator.processBefore(executor,?mappedStatement,?null,?parameter);}即,創建StatementHandler對象時,就會執行keyGenerator.processBefore()方法。keyGenerator.processAfter()方法,自然就是Statement執行后執行了。
org.apache.ibatis.executor.statement.SimpleStatementHandler.update(Statement)方法源碼。其他的StatementHandler都是類似的。
?@Overridepublic?int?update(Statement?statement)?throws?SQLException?{String?sql?=?boundSql.getSql();Object?parameterObject?=?boundSql.getParameterObject();KeyGenerator?keyGenerator?=?mappedStatement.getKeyGenerator();int?rows;if?(keyGenerator?instanceof?Jdbc3KeyGenerator)?{statement.execute(sql,?Statement.RETURN_GENERATED_KEYS);rows?=?statement.getUpdateCount();keyGenerator.processAfter(executor,?mappedStatement,?statement,?parameterObject);}?else?if?(keyGenerator?instanceof?SelectKeyGenerator)?{statement.execute(sql);rows?=?statement.getUpdateCount();keyGenerator.processAfter(executor,?mappedStatement,?statement,?parameterObject);}?else?{statement.execute(sql);rows?=?statement.getUpdateCount();}return?rows;}7. 批量插入,返回主鍵id列表
for?(Student?student?:?students)?{studentMapper.insertStudent(student); }對的,你沒看錯,就是像上面這樣for循環逐一insert操作的,此時,如果你考慮性能的話,可以使用BatchExecutor來完成,當然了,其他的Executor也是可以的。
如果文章就像上面這樣寫,那么就完全失去了寫文章的價值,上面的for循環,誰都懂這么操作可以實現,但是,很多人想要的并不是這個例子,而是另外一種批量插入操作,返回主鍵id列表。那么,看第8條。
8. Mybatis批量插入,返回主鍵id列表為null
<insert?id="insertStudents"?useGeneratedKeys="true"?keyProperty="studId"?parameterType="java.util.ArrayList">INSERT?INTOSTUDENTS(STUD_ID,?NAME,?EMAIL,?DOB,?PHONE)VALUES<foreach?collection="list"?item="item"?index="index"?separator=",">?(#{item.studId},#{item.name},#{item.email},#{item.dob},?#{item.phone})?</foreach>? </insert>很多同學,包括開源中國社區,都遇到使用上面的批量insert操作,返回的主鍵id列表是null的問題,很多人得出結論:Mybatis不支持這種形式的批量插入并返回主鍵id列表。真是這樣嗎?
我必須明確的跟大家說,Mybatis是支持上述形式的批量插入,且可以正確返回主鍵id列表的。之所以返回null值,是Mybatis框架的一個bug,下一篇將具體講述產生這個bug的原因,以及如何修復它。
總結
以上是生活随笔為你收集整理的通过源码分析Mybatis是如何返回数据库生成的自增主键值?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 从来没有一种技术是为了解决复用、灵活组合
- 下一篇: 想与 Oracle 说“再见”,太难了!