mybatis插入與批量插入返回ID的原理詳解
背景
最近正在整理之前基于mybatis的半ORM框架。原本的框架底層類ORM操作是通過(guò)StringBuilder的append拼接的,這次打算用JsqlParser重寫(xiě)一遍,一來(lái)底層不會(huì)存在太多的文本拼接,二來(lái)基于其他開(kāi)源包維護(hù)難度會(huì)小一些,最后還可以整理一下原有的冗余方法。
這兩天整理insert相關(guān)的方法,在將對(duì)象插入數(shù)據(jù)庫(kù)后,期望是要返回完整對(duì)象,并且包含實(shí)際的數(shù)據(jù)庫(kù)id。
基礎(chǔ)相關(guān)框架為:spring、mybatis、hikari。
底層調(diào)用方法
最底層的做法實(shí)際上很直白,就是利用mybatis執(zhí)行最簡(jiǎn)單的sql語(yǔ)句,給上代碼。
@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方法名稱和對(duì)象插入數(shù)據(jù)庫(kù)
*/
public Object insert(String sqlName, Object obj) throws SQLException {
return getSqlSession().insert(sqlName, obj); // 此處直接執(zhí)行傳入的xml中對(duì)應(yīng)的sql id,以及參數(shù)
}
}
單個(gè)對(duì)象插入
java代碼
/**
* 簡(jiǎn)單插入實(shí)體對(duì)象
*
* @param entity 實(shí)體對(duì)象
* @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>
其他的就不多說(shuō)了,這里針對(duì)如何返回已經(jīng)入庫(kù)的id給個(gè)說(shuō)明。
在xml的 insert 標(biāo)簽中,設(shè)置 keyProperty 為 對(duì)應(yīng)對(duì)象的id字段,和 insert(sqlName, obj) 這個(gè)方法中的 obj 是對(duì)應(yīng)的。
這里一般有兩種情況:
直接保存實(shí)體的對(duì)象作為參數(shù)傳入(給偽代碼示例)
SaveObject saveObject = new SaveObject(); // SaveObject中包含字段soid,作為自增id
saveObject.setName("my name");
saveObject.setNums(2);
getSqlSession().insert("saveObject.insert",saveObject);
這種情況實(shí)際就是傳入了待保存的對(duì)象。這時(shí)候我們的xml應(yīng)該這樣
<insert id="insert" parameterType="SaveObject " useGeneratedKeys="true" keyProperty="soid">
insert into save_object (`name`,nums) values (#{names},#{nums})
</insert>
這里我們傳入了SaveObject實(shí)體對(duì)象作為參數(shù),所以我們的 keyProperty 就是parameter的id對(duì)應(yīng)的字段,在這里就是 soid 。
多個(gè)對(duì)象,實(shí)體對(duì)象作為其中一個(gè)對(duì)象傳入
Map<String, Object> param = new HashMap<>();
param.put("baseSql", insert.toString());
param.put("entity", entity); // 此處對(duì)應(yīng)實(shí)體作為map的第二個(gè)參數(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時(shí),我們的 keyProperty 對(duì)應(yīng)方式就是先從Map中讀出對(duì)應(yīng)value,再指向 value中的id字段。
列表批量插入
批量插入數(shù)據(jù)有兩種做法,一種是多次調(diào)用單個(gè)insert方法,這種效率較低就不說(shuō)了。另外一種是 insert into table (cols) values (val1),(val2),(val3) 這樣批量插入。
到mybatis中,也是分為兩種
直接保存實(shí)體的對(duì)象作為參數(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);
這種情況實(shí)際就是傳入了待保存的對(duì)象。這時(shí)候我們的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>
多個(gè)對(duì)象,實(shí)體對(duì)象作為其中一個(gè)對(duì)象傳入
本文的重點(diǎn)來(lái)了,我自己卡在這里很久,反復(fù)調(diào)試才摸清邏輯。接下來(lái)就順著mybatis的思路來(lái)講,只會(huì)講id生成相關(guān)的,其他的流程就不多說(shuō)了。
先看這個(gè)類:org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator (很多代碼我用...代替了,不是特別重要,放在還占地方)
/**
* 這個(gè)方法是在執(zhí)行完插入語(yǔ)句之后處理的,兩個(gè)關(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);
}
}
利用這個(gè)代碼先解釋一下上一節(jié) 直接保存實(shí)體的對(duì)象作為參數(shù)傳入 為什么id會(huì)被更新至實(shí)體內(nèi)的soid字段。
上一節(jié)的是 keyProperty="soid"
我們來(lái)看19行的代碼Object soleParam = getSoleParameter(parameter); ,當(dāng)我們傳入的對(duì)象是List的時(shí)候 soleParam != null,所以 直接執(zhí)行 assignKeysToParam 方法。
注意64和65行
for (Object obj : paramAsCollection) {
if (!rs.next()) {
paramAsCollection 是將我們傳入的轉(zhuǎn)換為 Collection 類型,所以這里是循環(huán)我們的給定實(shí)體列表參數(shù)。
rs就是ResultSet,就是插入之后的結(jié)果集。 rs.next()就是指針指向下一條記錄,所以實(shí)際上這里是同步循環(huán),將結(jié)果集中的id直接設(shè)置到我們給的實(shí)體列表中
我們現(xiàn)在來(lái)看看多參數(shù)插入是會(huì)有什么問(wèn)題。
Java方法:
/**
* 簡(jiǎn)單批量插入實(shí)體對(duì)象
*
* @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>
會(huì)有什么問(wèn)題??根據(jù)這樣的xml,最后的結(jié)果是我們傳入的map中會(huì)多一個(gè)key 叫 “id”,里面存的是一個(gè)插入的實(shí)體的id。
因?yàn)楦鶕?jù)源碼 Map并非 Collection 類型,所以會(huì)做為只有一個(gè)元素的數(shù)組傳入,在剛才同步循環(huán)的地方就只會(huì)循環(huán)一次,把結(jié)果集中第一條數(shù)據(jù)的id放進(jìn)map中,循環(huán)就結(jié)束了。
怎么解決呢??
解決的方法就在 assignKeysToOneOfParams 這個(gè)方法,方法名其實(shí)已經(jīng)說(shuō)了,將主鍵賦給其中一個(gè)參數(shù),這里確實(shí)也是取了其中的一個(gè)參數(shù)進(jìn)行賦值主鍵。所以我們只要能夠跳轉(zhuǎn)到這個(gè)方法就好。所以需要滿足 getSoleParameter(parameter) == null ,點(diǎn)進(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ù)大于兩個(gè),且第一個(gè)和后面任意一個(gè)不相等
所以解決方案出爐,很簡(jiǎn)單,只需要改動(dòng)代碼兩個(gè)地方即可。
/**
* 簡(jiǎn)單批量插入實(shí)體對(duì)象
*
* @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.實(shí)體id -->
${baseSql}
</insert>
完成
總結(jié)
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,謝謝大家對(duì)腳本之家的支持。
相關(guān)文章
Java Optional優(yōu)雅處理空值的最佳實(shí)踐
你是否經(jīng)常遇到 NullPointerException,是否厭倦了寫(xiě)大量的 null 檢查,今天,讓我們一起探索 Java Optional,這個(gè)讓空值處理更優(yōu)雅,更安全的工具吧2025-06-06
Java如何處理數(shù)據(jù)成為樹(shù)狀結(jié)構(gòu)
這篇文章主要介紹了Java如何處理數(shù)據(jù)成為樹(shù)狀結(jié)構(gòu)問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-07-07
Java日常練習(xí)題,每天進(jìn)步一點(diǎn)點(diǎn)(26)
下面小編就為大家?guī)?lái)一篇Java基礎(chǔ)的幾道練習(xí)題(分享)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧,希望可以幫到你2021-07-07
Java基于命令模式實(shí)現(xiàn)郵局發(fā)信功能詳解
這篇文章主要介紹了Java基于命令模式實(shí)現(xiàn)郵局發(fā)信功能,較為詳細(xì)的分析了命令行模式的概念、原理并結(jié)合實(shí)例形式分析了Java使用命令行模式實(shí)現(xiàn)郵局發(fā)信功能的相關(guān)操作技巧與注意事項(xiàng),需要的朋友可以參考下2018-04-04
Java ExecutorServic線程池異步實(shí)現(xiàn)流程
這篇文章主要介紹了Java ExecutorServic線程池異步實(shí)現(xiàn)流程,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-12-12
java List.of()與Arrays.asList()方法對(duì)比分析
這篇文章主要為大家介紹了java List.of()與Arrays.asList()方法對(duì)比分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-11-11
解決引用slf4j中Logger.info只打印出文字沒(méi)有數(shù)據(jù)的問(wèn)題
這篇文章主要介紹了解決引用slf4j中Logger.info只打印出文字沒(méi)有數(shù)據(jù)的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12
java導(dǎo)出數(shù)據(jù)庫(kù)的全部表到excel
這篇文章主要為大家詳細(xì)介紹了java導(dǎo)出數(shù)據(jù)庫(kù)的全部表到excel的相關(guān)資料,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-03-03
Java多維數(shù)組和Arrays類方法總結(jié)詳解
這篇文章主要介紹了Java多維數(shù)組和Arrays類方法總結(jié)詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-03-03

