mybatis插入与批量插入返回ID的原理详解

网友投稿 777 2023-07-31

mybatis插入与批量插入返回ID的原理详解

mybatis插入与批量插入返回ID的原理详解

背景

最近正在整理之前基于mybatis的半ORM框架。原本的框架底层类ORM操作是通过StringBuilder的append拼接的,这次打算用jsqlParser重写一遍,一来底层不会存在太多的文本拼接,二来基于其他开源包维护难度会小一些,最后还可以整理一下原有的冗余方法。

这两天整理insert相关的方法,在将对象插入数据库后,期望是要返回完整对象,并且包含实际的数据库id。

基础相关框架为:spring、mybatis、hikari。

底层调用方法

最底层的做法实际上很直白,就是利用mybatis执行最简单的sql语句,给上代码

@Repository("baseDao")

public class BaseDao extends SqlSessionDaoSupport {

private Logger logger = LoggerFactory.getLogger(this.getClass());

/**

* 最大的单次批量插入的数量

*/

private static final int MAX_BATCH_SIZE = 10000;

@Override

@Autowired

public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {

super.setSqlSessionFactory(sqlSessionFactory);

}

/**

* 根据sql方法名称和对象插入数据库

*/

public Object insert(String sqlName, Object obj) throws SQLException {

return getSqlSession().insert(sqlName, obj); // 此处直接执行传入的xml中对应的sql id,以及参数

}

}

单个对象插入

java代码

/**

* 简单插入实体对象

*

* @param entity 实体对象

* @throws SQLException

*/

public 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 param = new HashMap<>();

param.put("baseSql", insewoqZNnCBurt.toString());

param.put("entity", entity);

this.insert("BaseDao.insertEntity", param);

return entity;

}

xml代码

${baseSql}

其他的就不多说了,这里针对如何返回已经入库的id给个说明。

在xml的 insert 标签中,设置 keyProperty 为 对应对象的id字段,和 insert(sqlName, obj) 这个方法中的 obj 是对应的。

这里一般有两种情况:

直接保存实体的对象作为参数传入(给伪代码示例

SaveObject saveObject = new SaveObject(); // SaveObject中包含字段soid,作为自增id

saveObject.setName("my name");

saveObject.setNums(2);

getSqlSession().insert("saveObject.insert",saveObject);

这种情况实际就是传入了待保存的对象。这时候我们的xml应该这样

insert into save_object (`name`,nums) values (#{names},#{nums})

这里我们传入了SaveObject实体对象作为参数,所以我们的 keyProperty 就是parameter的id对应的字段,在这里就是 soid 。

多个对象,实体对象作为其中一个对象传入

Map param = new HashMap<>();

param.put("baseSql", insert.toString());

param.put("entity", entity); // 此处对应实体作为map的第二个参数传入

this.insert("BaseDao.insertEntity", param);

${baseSql}

这里也是比较容易理解,当传入参数是Map时,我们的 keyProperty 对应方式就是先从Map中读出对应value,再指向 value中的id字段。

列表批量插入

批量插入数据有两种做法,一种是多次调用单个insert方法,这种效率较低就不说了。另外一种是 insert into table (cols) values (val1),(val2),(val3) 这样批量插入。

到mybatis中,也是分为两种

直接保存实体的对象作为参数传入(给伪代码示例)

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 saveObjects = new ArrayList();

saveObjects.add(saveObjects1);

saveObjects.add(saveObjects2);

getSqlSession().insert("saveObject.insertList",saveObjects);

这种情况实际就是传入了待保存的对象。这时候我们的xml应该这样

insert into save_object (`name`,nums) values

(#{saveObject.numsnames}, #{saveObject.nums})

多个对象,实体对象作为其中一个对象传入

本文的重点来了,我自己卡在这里很久,反复调试才摸清逻辑。接下来就顺着mybatis的思路来讲,只会讲id生成相关的,其他的流程就不多说了。

先看这个类:org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator (很多代码我用...代替了,不是特别重要,放在还占地方)

/**

* 这个方法是在执行完插入语句之后处理的,两个关键参数

* 1. MappedStatement ms 里面包含了我们的 keyProperty

* 2. Object parameter 就是我们inser方法传入的参数

*/

@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);

}

}

利用这个代码先解释一下上一节 直接保存实体的对象作为参数传入 为什么id会被更新至实体内的soid字段。

上一节的是 keyProperty="soid"

我们来看19行的代码Object soleParam = getSoleParameter(parameter); ,当我们传入的对象是List的时候 soleParam != null,所以 直接执行 assignKeysToParam 方法。

注意64和65行

for (Object obj : paramAsCollection) {

if (!rs.next()) {

paramAsCollection 是将我们传入的转换为 Collection 类型,所以这里是循环我们的给定实体列表参数。

rs就是ResultSet,就是插入之后的结果集。 rs.next()就是指针指向下一条记录,所以实际上这里是同步循环,将结果集中的id直接设置到我们给的实体列表中

我们现在来看看多参数插入是会有什么问题。

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 param = new HashMap<>();

param.put("baseSql", insert.toString());

param.put("list", entitys);

this.insert("BaseDao.insertEntityList", param);

return entitys;

}

Xml:

${baseSql}

会有什么问题??根据这样的xml,最后的结果是我们传入的map中会多一个key 叫 “id”,里面存的是一个插入的实体的id。

因为根据源码 Map并非 Collection 类型,所以会做为只有一个元素的数组传入,在刚才同步循环的地方就只会循环一次,把结果集中第一条数据的id放进map中,循环就结束了。

怎么解决呢??

解决的方法就在 assignKeysToOneOfParams 这个方法,方法名其实已经说了,将主键赋给其中一个参数,这里确实也是取了其中的一个参数进行赋值主键。所以我们只要能够跳转到这个方法就好。所以需要满足 getSoleParameter(parameter) == null ,点进代码看

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,条件是这样:

参数是ParamMap或者 StrictMap

参数大于两个,且第一个和后面任意一个不相等

所以解决方案出炉,很简单,只需要改动代码两个地方即可。

/**

* 简单批量插入实体对象

*

* @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 param = new MapperMethod.ParamMap<>(); // 这里替换为 MapperMethod.ParamMap 类型

param.put("baseSql", insert.toString());

param.put("list", entitys);

this.insert("BaseDao.insertEntityList", param);

return entitys;

}

Xml:

${baseSql}

完成

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对我们的支持。

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:SpringCloud版本问题报错及解决方法
下一篇:Spring Boot利用Docker快速部署项目的完整步骤
相关文章

 发表评论

暂时没有评论,来抢沙发吧~