mybatis插入與批量插入返回ID的原理詳解
背景
最近正在整理之前基于mybatis的半ORM框架。原本的框架底層類ORM操作是通過StringBuilder的append拼接的,這次打算用JsqlParser重寫一遍,一來底層不會存在太多的文本拼接,二來基于其他開源包維護(hù)難度會小一些,最后還可以整理一下原有的冗余方法。
這兩天整理insert相關(guān)的方法,在將對象插入數(shù)據(jù)庫后,期望是要返回完整對象,并且包含實際的數(shù)據(jù)庫id。
基礎(chǔ)相關(guān)框架為:spring、mybatis、hikari。
底層調(diào)用方法
最底層的做法實際上很直白,就是利用mybatis執(zhí)行最簡單的sql語句,給上代碼。
@Repository("baseDao") public class BaseDao extends SqlSessionDaoSupport { private Logger logger = LoggerFactory.getLogger(this.getClass()); /** * 最大的單次批量插入的數(shù)量 */ private static final int MAX_BATCH_SIZE = 10000; @Override @Autowired public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) { super.setSqlSessionFactory(sqlSessionFactory); } /** * 根據(jù)sql方法名稱和對象插入數(shù)據(jù)庫 */ public Object insert(String sqlName, Object obj) throws SQLException { return getSqlSession().insert(sqlName, obj); // 此處直接執(zhí)行傳入的xml中對應(yīng)的sql id,以及參數(shù) } }
單個對象插入
java代碼
/** * 簡單插入實體對象 * * @param entity 實體對象 * @throws SQLException */ public <T extends BaseEntity> T insertEntity(T entity) throws SQLException { Insert insert = new Insert(); insert.setTable(new Table(entity.getClass().getSimpleName())); insert.setColumns(JsqlUtils.getColumnNameFromEntity(entity.getClass())); insert.setItemsList(JsqlUtils.getAllColumnValueFromEntity(entity,insert.getColumns())); Map<String, Object> param = new HashMap<>(); param.put("baseSql", insert.toString()); param.put("entity", entity); this.insert("BaseDao.insertEntity", param); return entity; }
xml代碼
<insert id="insertEntity" parameterType="java.util.Map" useGeneratedKeys="true" keyProperty="entity.id"> ${baseSql} </insert>
其他的就不多說了,這里針對如何返回已經(jīng)入庫的id給個說明。
在xml的 insert 標(biāo)簽中,設(shè)置 keyProperty 為 對應(yīng)對象的id字段,和 insert(sqlName, obj) 這個方法中的 obj 是對應(yīng)的。
這里一般有兩種情況:
直接保存實體的對象作為參數(shù)傳入(給偽代碼示例)
SaveObject saveObject = new SaveObject(); // SaveObject中包含字段soid,作為自增id saveObject.setName("my name"); saveObject.setNums(2); getSqlSession().insert("saveObject.insert",saveObject);
這種情況實際就是傳入了待保存的對象。這時候我們的xml應(yīng)該這樣
<insert id="insert" parameterType="SaveObject " useGeneratedKeys="true" keyProperty="soid"> insert into save_object (`name`,nums) values (#{names},#{nums}) </insert>
這里我們傳入了SaveObject實體對象作為參數(shù),所以我們的 keyProperty 就是parameter的id對應(yīng)的字段,在這里就是 soid 。
多個對象,實體對象作為其中一個對象傳入
Map<String, Object> param = new HashMap<>(); param.put("baseSql", insert.toString()); param.put("entity", entity); // 此處對應(yīng)實體作為map的第二個參數(shù)傳入 this.insert("BaseDao.insertEntity", param);
<insert id="insertEntity" parameterType="java.util.Map" useGeneratedKeys="true" keyProperty="entity.id"> ${baseSql} </insert>
這里也是比較容易理解,當(dāng)傳入?yún)?shù)是Map時,我們的 keyProperty 對應(yīng)方式就是先從Map中讀出對應(yīng)value,再指向 value中的id字段。
列表批量插入
批量插入數(shù)據(jù)有兩種做法,一種是多次調(diào)用單個insert方法,這種效率較低就不說了。另外一種是 insert into table (cols) values (val1),(val2),(val3)
這樣批量插入。
到mybatis中,也是分為兩種
直接保存實體的對象作為參數(shù)傳入(給偽代碼示例)
SaveObject saveObject1 = new SaveObject(); // SaveObject中包含字段soid,作為自增id saveObject1.setName("my name"); saveObject1.setNums(2); SaveObject saveObject2 = new SaveObject(); // SaveObject中包含字段soid,作為自增id saveObject2.setName("my name"); saveObject2.setNums(2); List<SaveObject> saveObjects = new ArrayList<SaveObject>(); saveObjects.add(saveObjects1); saveObjects.add(saveObjects2); getSqlSession().insert("saveObject.insertList",saveObjects);
這種情況實際就是傳入了待保存的對象。這時候我們的xml應(yīng)該這樣
<insert id="insertList" parameterType="java.util.List" useGeneratedKeys="true" keyProperty="soid"> insert into save_object (`name`,nums) values <foreach collection="list" index="index" item="saveObject" separator=","> (#{saveObject.numsnames}, #{saveObject.nums}) </foreach> </insert>
多個對象,實體對象作為其中一個對象傳入
本文的重點來了,我自己卡在這里很久,反復(fù)調(diào)試才摸清邏輯。接下來就順著mybatis的思路來講,只會講id生成相關(guān)的,其他的流程就不多說了。
先看這個類:org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator
(很多代碼我用...代替了,不是特別重要,放在還占地方)
/** * 這個方法是在執(zhí)行完插入語句之后處理的,兩個關(guān)鍵參數(shù) * 1. MappedStatement ms 里面包含了我們的 keyProperty * 2. Object parameter 就是我們inser方法傳入的參數(shù) */ @Override public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) { processBatch(ms, stmt, parameter); } public void processBatch(MappedStatement ms, Statement stmt, Object parameter) { final String[] keyProperties = ms.getKeyProperties(); if (keyProperties == null || keyProperties.length == 0) { return; } try (ResultSet rs = stmt.getGeneratedKeys()) { final Configuration configuration = ms.getConfiguration(); if (rs.getMetaData().getColumnCount() >= keyProperties.length) { Object soleParam = getSoleParameter(parameter); if (soleParam != null) { assignKeysToParam(configuration, rs, keyProperties, soleParam); } else { assignKeysToOneOfParams(configuration, rs, keyProperties, (Map<?, ?>) parameter); } } } catch (Exception e) { ... } } protected void assignKeysToOneOfParams(final Configuration configuration, ResultSet rs, final String[] keyProperties, Map<?, ?> paramMap) throws SQLException { // Assuming 'keyProperty' includes the parameter name. e.g. 'param.id'. int firstDot = keyProperties[0].indexOf('.'); if (firstDot == -1) { ... } String paramName = keyProperties[0].substring(0, firstDot); Object param; if (paramMap.containsKey(paramName)) { param = paramMap.get(paramName); } else { ... } ... assignKeysToParam(configuration, rs, modifiedKeyProperties, param); } private void assignKeysToParam(final Configuration configuration, ResultSet rs, final String[] keyProperties, Object param) throws SQLException { final TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry(); final ResultSetMetaData rsmd = rs.getMetaData(); // Wrap the parameter in Collection to normalize the logic. Collection<?> paramAsCollection = null; if (param instanceof Object[]) { paramAsCollection = Arrays.asList((Object[]) param); } else if (!(param instanceof Collection)) { paramAsCollection = Arrays.asList(param); } else { paramAsCollection = (Collection<?>) param; } TypeHandler<?>[] typeHandlers = null; for (Object obj : paramAsCollection) { if (!rs.next()) { break; } MetaObject metaParam = configuration.newMetaObject(obj); if (typeHandlers == null) { typeHandlers = getTypeHandlers(typeHandlerRegistry, metaParam, keyProperties, rsmd); } populateKeys(rs, metaParam, keyProperties, typeHandlers); } }
利用這個代碼先解釋一下上一節(jié) 直接保存實體的對象作為參數(shù)傳入 為什么id會被更新至實體內(nèi)的soid字段。
上一節(jié)的是 keyProperty="soid"
我們來看19行的代碼Object soleParam = getSoleParameter(parameter);
,當(dāng)我們傳入的對象是List的時候 soleParam != null,所以 直接執(zhí)行 assignKeysToParam 方法。
注意64和65行
for (Object obj : paramAsCollection) { if (!rs.next()) {
paramAsCollection 是將我們傳入的轉(zhuǎn)換為 Collection 類型,所以這里是循環(huán)我們的給定實體列表參數(shù)。
rs就是ResultSet,就是插入之后的結(jié)果集。 rs.next()
就是指針指向下一條記錄,所以實際上這里是同步循環(huán),將結(jié)果集中的id直接設(shè)置到我們給的實體列表中
我們現(xiàn)在來看看多參數(shù)插入是會有什么問題。
Java方法:
/** * 簡單批量插入實體對象 * * @param entitys * @throws SQLException */ public List insertEntityList(List<? extends BaseEntity> entitys) throws SQLException { if (entitys == null || entitys.size() == 0) { return null; } Insert insert = new Insert(); insert.setTable(new Table(entitys.get(0).getClass().getSimpleName())); insert.setColumns(JsqlUtils.getColumnNameFromEntity(entitys.get(0).getClass())); MultiExpressionList multiExpressionList = new MultiExpressionList(); entitys.stream().map(e -> JsqlUtils.getAllColumnValueFromEntity(e,insert.getColumns())).forEach(e -> multiExpressionList.addExpressionList(e)); insert.setItemsList(multiExpressionList); Map<String, Object> param = new HashMap<>(); param.put("baseSql", insert.toString()); param.put("list", entitys); this.insert("BaseDao.insertEntityList", param); return entitys; }
Xml:
<insert id="insertEntityList" parameterType="java.util.Map" useGeneratedKeys="true" keyProperty="id"> ${baseSql} </insert>
會有什么問題??根據(jù)這樣的xml,最后的結(jié)果是我們傳入的map中會多一個key 叫 “id”,里面存的是一個插入的實體的id。
因為根據(jù)源碼 Map并非 Collection 類型,所以會做為只有一個元素的數(shù)組傳入,在剛才同步循環(huán)的地方就只會循環(huán)一次,把結(jié)果集中第一條數(shù)據(jù)的id放進(jìn)map中,循環(huán)就結(jié)束了。
怎么解決呢??
解決的方法就在 assignKeysToOneOfParams 這個方法,方法名其實已經(jīng)說了,將主鍵賦給其中一個參數(shù),這里確實也是取了其中的一個參數(shù)進(jìn)行賦值主鍵。所以我們只要能夠跳轉(zhuǎn)到這個方法就好。所以需要滿足 getSoleParameter(parameter) == null
,點進(jìn)代碼看
private Object getSoleParameter(Object parameter) { if (!(parameter instanceof ParamMap || parameter instanceof StrictMap)) { return parameter; } Object soleParam = null; for (Object paramValue : ((Map<?, ?>) parameter).values()) { if (soleParam == null) { soleParam = paramValue; } else if (soleParam != paramValue) { soleParam = null; break; } } return soleParam; }
要返回null,條件是這樣:
- 參數(shù)是ParamMap或者 StrictMap
- 參數(shù)大于兩個,且第一個和后面任意一個不相等
所以解決方案出爐,很簡單,只需要改動代碼兩個地方即可。
/** * 簡單批量插入實體對象 * * @param entitys * @throws SQLException */ public List insertEntityList(List<? extends BaseEntity> entitys) throws SQLException { if (entitys == null || entitys.size() == 0) { return null; } Insert insert = new Insert(); insert.setTable(new Table(entitys.get(0).getClass().getSimpleName())); insert.setColumns(JsqlUtils.getColumnNameFromEntity(entitys.get(0).getClass())); MultiExpressionList multiExpressionList = new MultiExpressionList(); entitys.stream().map(e -> JsqlUtils.getAllColumnValueFromEntity(e,insert.getColumns())).forEach(e -> multiExpressionList.addExpressionList(e)); insert.setItemsList(multiExpressionList); Map<String, Object> param = new MapperMethod.ParamMap<>(); // 這里替換為 MapperMethod.ParamMap 類型 param.put("baseSql", insert.toString()); param.put("list", entitys); this.insert("BaseDao.insertEntityList", param); return entitys; }
Xml:
<insert id="insertEntityList" parameterType="java.util.Map" useGeneratedKeys="true" keyProperty="list.id"> <!-- 這里是map中的key.實體id --> ${baseSql} </insert>
完成
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,謝謝大家對腳本之家的支持。
- mybatis中批量插入的兩種方式(高效插入)
- MyBatis批量插入(insert)數(shù)據(jù)操作
- Mybatis三種批量插入數(shù)據(jù)的方式
- MyBatis-Plus 批量插入數(shù)據(jù)的操作方法
- MyBatis批量插入大量數(shù)據(jù)(1w以上)
- MyBatis批量插入數(shù)據(jù)的三種方法實例
- 給你的MyBatis-Plus裝上批量插入的翅膀(推薦)
- MyBatis批量插入數(shù)據(jù)到Oracle數(shù)據(jù)庫中的兩種方式(實例代碼)
- Mybatis-plus 批量插入太慢的問題解決(提升插入性能)
- Mybatis批量插入的三種實現(xiàn)方法
相關(guān)文章
Java數(shù)據(jù)結(jié)構(gòu)最清晰圖解二叉樹前 中 后序遍歷
樹是一種重要的非線性數(shù)據(jù)結(jié)構(gòu),直觀地看,它是數(shù)據(jù)元素(在樹中稱為結(jié)點)按分支關(guān)系組織起來的結(jié)構(gòu),很象自然界中的樹那樣。樹結(jié)構(gòu)在客觀世界中廣泛存在,如人類社會的族譜和各種社會組織機構(gòu)都可用樹形象表示2022-01-01淺談java中math類中三種取整函數(shù)的區(qū)別
下面小編就為大家?guī)硪黄獪\談java中math類中三種取整函數(shù)的區(qū)別。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2016-11-11Springboot中攔截GET請求獲取請求參數(shù)驗證合法性核心方法
這篇文章主要介紹了Springboot中攔截GET請求獲取請求參數(shù)驗證合法性,在Springboot中創(chuàng)建攔截器攔截所有GET類型請求,獲取請求參數(shù)驗證內(nèi)容合法性防止SQL注入,這種方法適用攔截get類型請求,需要的朋友可以參考下2023-08-08Java中Integer的parseInt和valueOf的區(qū)別詳解
這篇文章主要介紹了Java中Integer的parseInt和valueOf的區(qū)別詳解,nteger.parseInt(s)是把字符串解析成int基本類型,Integer.valueOf(s)是把字符串解析成Integer對象類型,其實int就是Integer解包裝,Integer就是int的包裝,需要的朋友可以參考下2023-11-11Spring Boot REST國際化的實現(xiàn)代碼
本文我們將討論如何在現(xiàn)有的Spring Boot項目中添加國際化。只需幾個簡單的步驟即可實現(xiàn)Spring Boot應(yīng)用的國際化,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-10-10