mybatis插入与批量插入返回ID的原理详解
背景
最近正在整理之前基于mybatis的半ORM框架。原本的框架底层类ORM操作是通过StringBuilder的append拼接的,这次打算用JsqlParser重写一遍,一来底层不会存在太多的文本拼接,二来基于其他开源包维护难度会小一些,最后还可以整理一下原有的冗余方法。
这两天整理insert相关的方法,在将对象插入数据库后,期望是要返回完整对象,并且包含实际的数据库id。
基础相关框架为:spring、mybatis、hikari。
底层调用方法
最底层的做法实际上很直白,就是利用mybatis执行最简单的sql语句,给上代码。
@Repository("baseDao")
publicclassBaseDaoextendsSqlSessionDaoSupport{
privateLoggerlogger=LoggerFactory.getLogger(this.getClass());
/**
*最大的单次批量插入的数量
*/
privatestaticfinalintMAX_BATCH_SIZE=10000;
@Override
@Autowired
publicvoidsetSqlSessionFactory(SqlSessionFactorysqlSessionFactory){
super.setSqlSessionFactory(sqlSessionFactory);
}
/**
*根据sql方法名称和对象插入数据库
*/
publicObjectinsert(StringsqlName,Objectobj)throwsSQLException{
returngetSqlSession().insert(sqlName,obj);//此处直接执行传入的xml中对应的sqlid,以及参数
}
}
单个对象插入
java代码
/** *简单插入实体对象 * *@paramentity实体对象 *@throwsSQLException */ publicTinsertEntity(Tentity)throwsSQLException{ Insertinsert=newInsert(); insert.setTable(newTable(entity.getClass().getSimpleName())); insert.setColumns(JsqlUtils.getColumnNameFromEntity(entity.getClass())); insert.setItemsList(JsqlUtils.getAllColumnValueFromEntity(entity,insert.getColumns())); Map param=newHashMap<>(); param.put("baseSql",insert.toString()); param.put("entity",entity); this.insert("BaseDao.insertEntity",param); returnentity; }
xml代码
${baseSql}
其他的就不多说了,这里针对如何返回已经入库的id给个说明。
在xml的insert标签中,设置keyProperty为对应对象的id字段,和insert(sqlName,obj)这个方法中的obj是对应的。
这里一般有两种情况:
直接保存实体的对象作为参数传入(给伪代码示例)
SaveObjectsaveObject=newSaveObject();//SaveObject中包含字段soid,作为自增id
saveObject.setName("myname");
saveObject.setNums(2);
getSqlSession().insert("saveObject.insert",saveObject);
这种情况实际就是传入了待保存的对象。这时候我们的xml应该这样
insertintosave_object(`name`,nums)values(#{names},#{nums})
这里我们传入了SaveObject实体对象作为参数,所以我们的keyProperty就是parameter的id对应的字段,在这里就是soid。
多个对象,实体对象作为其中一个对象传入
Mapparam=newHashMap<>(); param.put("baseSql",insert.toString()); param.put("entity",entity);//此处对应实体作为map的第二个参数传入 this.insert("BaseDao.insertEntity",param);
${baseSql}
这里也是比较容易理解,当传入参数是Map时,我们的keyProperty对应方式就是先从Map中读出对应value,再指向value中的id字段。
列表批量插入
批量插入数据有两种做法,一种是多次调用单个insert方法,这种效率较低就不说了。另外一种是insertintotable(cols)values(val1),(val2),(val3)这样批量插入。
到mybatis中,也是分为两种
直接保存实体的对象作为参数传入(给伪代码示例)
SaveObjectsaveObject1=newSaveObject();//SaveObject中包含字段soid,作为自增id
saveObject1.setName("myname");
saveObject1.setNums(2);
SaveObjectsaveObject2=newSaveObject();//SaveObject中包含字段soid,作为自增id
saveObject2.setName("myname");
saveObject2.setNums(2);
ListsaveObjects=newArrayList();
saveObjects.add(saveObjects1);
saveObjects.add(saveObjects2);
getSqlSession().insert("saveObject.insertList",saveObjects);
这种情况实际就是传入了待保存的对象。这时候我们的xml应该这样
insertintosave_object(`name`,nums)values (#{saveObject.numsnames},#{saveObject.nums})
多个对象,实体对象作为其中一个对象传入
本文的重点来了,我自己卡在这里很久,反复调试才摸清逻辑。接下来就顺着mybatis的思路来讲,只会讲id生成相关的,其他的流程就不多说了。
先看这个类:org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator(很多代码我用...代替了,不是特别重要,放在还占地方)
/**
*这个方法是在执行完插入语句之后处理的,两个关键参数
*1.MappedStatementms里面包含了我们的keyProperty
*2.Objectparameter就是我们inser方法传入的参数
*/
@Override
publicvoidprocessAfter(Executorexecutor,MappedStatementms,Statementstmt,Objectparameter){
processBatch(ms,stmt,parameter);
}
publicvoidprocessBatch(MappedStatementms,Statementstmt,Objectparameter){
finalString[]keyProperties=ms.getKeyProperties();
if(keyProperties==null||keyProperties.length==0){
return;
}
try(ResultSetrs=stmt.getGeneratedKeys()){
finalConfigurationconfiguration=ms.getConfiguration();
if(rs.getMetaData().getColumnCount()>=keyProperties.length){
ObjectsoleParam=getSoleParameter(parameter);
if(soleParam!=null){
assignKeysToParam(configuration,rs,keyProperties,soleParam);
}else{
assignKeysToOneOfParams(configuration,rs,keyProperties,(Map,?>)parameter);
}
}
}catch(Exceptione){
...
}
}
protectedvoidassignKeysToOneOfParams(finalConfigurationconfiguration,ResultSetrs,finalString[]keyProperties,
Map,?>paramMap)throwsSQLException{
//Assuming'keyProperty'includestheparametername.e.g.'param.id'.
intfirstDot=keyProperties[0].indexOf('.');
if(firstDot==-1){
...
}
StringparamName=keyProperties[0].substring(0,firstDot);
Objectparam;
if(paramMap.containsKey(paramName)){
param=paramMap.get(paramName);
}else{
...
}
...
assignKeysToParam(configuration,rs,modifiedKeyProperties,param);
}
privatevoidassignKeysToParam(finalConfigurationconfiguration,ResultSetrs,finalString[]keyProperties,
Objectparam)
throwsSQLException{
finalTypeHandlerRegistrytypeHandlerRegistry=configuration.getTypeHandlerRegistry();
finalResultSetMetaDatarsmd=rs.getMetaData();
//WraptheparameterinCollectiontonormalizethelogic.
Collection>paramAsCollection=null;
if(paraminstanceofObject[]){
paramAsCollection=Arrays.asList((Object[])param);
}elseif(!(paraminstanceofCollection)){
paramAsCollection=Arrays.asList(param);
}else{
paramAsCollection=(Collection>)param;
}
TypeHandler>[]typeHandlers=null;
for(Objectobj:paramAsCollection){
if(!rs.next()){
break;
}
MetaObjectmetaParam=configuration.newMetaObject(obj);
if(typeHandlers==null){
typeHandlers=getTypeHandlers(typeHandlerRegistry,metaParam,keyProperties,rsmd);
}
populateKeys(rs,metaParam,keyProperties,typeHandlers);
}
}
利用这个代码先解释一下上一节直接保存实体的对象作为参数传入为什么id会被更新至实体内的soid字段。
上一节的是keyProperty="soid"
我们来看19行的代码ObjectsoleParam=getSoleParameter(parameter);,当我们传入的对象是List的时候soleParam!=null,所以直接执行assignKeysToParam方法。
注意64和65行
for(Objectobj:paramAsCollection){
if(!rs.next()){
paramAsCollection是将我们传入的转换为Collection类型,所以这里是循环我们的给定实体列表参数。
rs就是ResultSet,就是插入之后的结果集。rs.next()就是指针指向下一条记录,所以实际上这里是同步循环,将结果集中的id直接设置到我们给的实体列表中
我们现在来看看多参数插入是会有什么问题。
Java方法:
/**
*简单批量插入实体对象
*
*@paramentitys
*@throwsSQLException
*/
publicListinsertEntityList(Listentitys)throwsSQLException{
if(entitys==null||entitys.size()==0){
returnnull;
}
Insertinsert=newInsert();
insert.setTable(newTable(entitys.get(0).getClass().getSimpleName()));
insert.setColumns(JsqlUtils.getColumnNameFromEntity(entitys.get(0).getClass()));
MultiExpressionListmultiExpressionList=newMultiExpressionList();
entitys.stream().map(e->JsqlUtils.getAllColumnValueFromEntity(e,insert.getColumns())).forEach(e->multiExpressionList.addExpressionList(e));
insert.setItemsList(multiExpressionList);
Mapparam=newHashMap<>();
param.put("baseSql",insert.toString());
param.put("list",entitys);
this.insert("BaseDao.insertEntityList",param);
returnentitys;
}
Xml:
${baseSql}
会有什么问题??根据这样的xml,最后的结果是我们传入的map中会多一个key叫“id”,里面存的是一个插入的实体的id。
因为根据源码Map并非Collection类型,所以会做为只有一个元素的数组传入,在刚才同步循环的地方就只会循环一次,把结果集中第一条数据的id放进map中,循环就结束了。
怎么解决呢??
解决的方法就在assignKeysToOneOfParams这个方法,方法名其实已经说了,将主键赋给其中一个参数,这里确实也是取了其中的一个参数进行赋值主键。所以我们只要能够跳转到这个方法就好。所以需要满足getSoleParameter(parameter)==null,点进代码看
privateObjectgetSoleParameter(Objectparameter){
if(!(parameterinstanceofParamMap||parameterinstanceofStrictMap)){
returnparameter;
}
ObjectsoleParam=null;
for(ObjectparamValue:((Map,?>)parameter).values()){
if(soleParam==null){
soleParam=paramValue;
}elseif(soleParam!=paramValue){
soleParam=null;
break;
}
}
returnsoleParam;
}
要返回null,条件是这样:
- 参数是ParamMap或者StrictMap
- 参数大于两个,且第一个和后面任意一个不相等
所以解决方案出炉,很简单,只需要改动代码两个地方即可。
/**
*简单批量插入实体对象
*
*@paramentitys
*@throwsSQLException
*/
publicListinsertEntityList(Listentitys)throwsSQLException{
if(entitys==null||entitys.size()==0){
returnnull;
}
Insertinsert=newInsert();
insert.setTable(newTable(entitys.get(0).getClass().getSimpleName()));
insert.setColumns(JsqlUtils.getColumnNameFromEntity(entitys.get(0).getClass()));
MultiExpressionListmultiExpressionList=newMultiExpressionList();
entitys.stream().map(e->JsqlUtils.getAllColumnValueFromEntity(e,insert.getColumns())).forEach(e->multiExpressionList.addExpressionList(e));
insert.setItemsList(multiExpressionList);
Mapparam=newMapperMethod.ParamMap<>();//这里替换为MapperMethod.ParamMap类型
param.put("baseSql",insert.toString());
param.put("list",entitys);
this.insert("BaseDao.insertEntityList",param);
returnentitys;
}
Xml:
${baseSql}
完成