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 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>
其他的就不多说了,这里针对如何返回已经入库的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 id="insert" parameterType="SaveObject " useGeneratedKeys="true" keyProperty="soid">
insert into save_object (`name`,nums) values (#{names},#{nums})
</insert>
这里我们传入了SaveObject实体对象作为参数,所以我们的 keyProperty 就是parameter的id对应的字段,在这里就是 soid 。
多个对象,实体对象作为其中一个对象传入
Map<String, Object> param = new HashMap<>();
param.put("baseSql", insert.toString());
param.put("entity", entity); // 此处对应实体作为map的第二个参数传入
this.insert("BaseDao.insertEntity", param);
<insert id="insertEntity" parameterType="java.util.Map" useGeneratedKeys="true" keyProperty="entity.id">
${baseSql}
</insert>
这里也是比较容易理解,当传入参数是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<SaveObject> saveObjects = new ArrayList<SaveObject>();
saveObjects.add(saveObjects1);
saveObjects.add(saveObjects2);
getSqlSession().insert("saveObject.insertList",saveObjects);
这种情况实际就是传入了待保存的对象。这时候我们的xml应该这样
<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>
多个对象,实体对象作为其中一个对象传入
本文的重点来了,我自己卡在这里很久,反复调试才摸清逻辑。接下来就顺着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<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>
会有什么问题??根据这样的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<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>
完成
mybatis的插入与批量插入的返回ID的原理的更多相关文章
- mybatis单个插入和批量插入的简单比较
在J2EE项目中,mybatis作为主流持久层框架,许多知识值得我们去钻研学习,今天,记录一下数据插入性能(单个插入和批量插入). 一,测试对象 public class Test { private ...
- MyBatis向数据库中批量插入数据
Foreach标签 foreach: collection:指定要遍历的集合; 表示传入过来的参数的数据类型.该参数为必选.要做 foreach 的对象,作为入参时,List 对象默认用 list 代 ...
- 【MyBatis】几种批量插入效率的比较
批处理数据主要有三种方式: 反复执行单条插入语句 foreach 拼接 sql 批处理 一.前期准备 基于Spring Boot + Mysql,同时为了省略get/set,使用了lombok,详见p ...
- mybatis 注解的方式批量插入,更新数据
一,当向数据表中插入一条数据时,一般先检查该数据是否已经存在,如果存在更新,不存在则新增 使用关键字 ON DUPLICATE KEY UPDATE zk_device_id为主键 model ...
- mybatis使用foreach进行批量插入和删除操作
一.批量插入 1.mapper层 int insertBatchRoleUser(@Param("lists") List<RoleUser> lists);//@Pa ...
- 24单行插入与批量插入-insert(必学)-天轰穿sqlserver视频教程
大纲:insert语句,简单插入数据与批量插入数据 为了冲优酷的访问量,所以这里只放优酷的地址了,其实其他网站还是都传了的哈. 代码下载http://www.cnthc.com/?/article/1 ...
- c# MongoDB插入和批量插入,插入原理
在开发之前,选择MongoDb驱动是件很重要的事情.如果选择不好,在后期的开发的是件很费力的事情,因为我就遇到这样的问题.MongoDb驱动有几种比较流行驱动,官方驱动和samus是两种使用比较多的. ...
- mybatis的三种批量插入以及次效率比较
1.表结构 CREATE TABLE `t_user` ( `id` varchar(32) CHARACTER SET utf8 NOT NULL COMMENT '主键', `name` varc ...
- Mybatis 插入与批量插入以及多参数批量删除
实体类: import java.io.Serializable; public class AttachmentTable implements Serializable { private sta ...
随机推荐
- Angular升级流程
执行命令 ng update @angular/cli --migrate-only --from=1.7.1 npm install --save-dev @angular/cli@latest 注 ...
- WPF MessageBox 添加确认取消按钮 并判断
很简单的功能随笔 if (System.Windows.MessageBox.Show("您确定要删除吗?", "提示:", MessageBoxButton. ...
- js 动态生成div显示id
<!DOCTYPE html><html lang="en" xmlns="http://www.w3.org/1999/xhtml"> ...
- WPF之路——实现自定义虚拟容器(实现VirtualizingPanel)
原文:WPF之路--实现自定义虚拟容器(实现VirtualizingPanel) 源码下载地址: http://download.csdn.net/detail/qianshen88/6618033 ...
- liunx 系统 一键安装
本文转自:http://hi.baidu.com/iamcyh/item/e777eb81ba90ed5a26ebd9b0 linux VPS环境(MySQL/Apache/PHP/Nginx)一键安 ...
- 【C】用C语言提取bmp图片像素,并进行K-means聚类分析——容易遇到的问题
关于bmp图片的格式,网上有很多文章,具体可以参考百度百科,也有例子程序.这里只提要注意的问题. (1)结构体定义问题:首先按照百度百科介绍的定义了结构体,但是编译发现重定义BITMAPFILEHEA ...
- 百度官方wormHole后门检测记录
乌云地址:http://drops.wooyun.org/papers/10061 后门端口:40310/6259 本次测试在Ubuntu下,具体adb调试工具参考 sink_cup的博客 http: ...
- C#数字图像处理时注意图像的未用区域
原文:C#数字图像处理时注意图像的未用区域 图1. 被锁定图像像素数组基本布局 如图1所示,数组的宽度并不一定等于图像像素数组的宽度,还有一部分未用区域.这是为了提高效率,系统要确定每 ...
- 微信小程序把玩(二十六)navigator组件
原文:微信小程序把玩(二十六)navigator组件 navigator跳转分为两个状态一种是关闭当前页面一种是不关闭当前页面.用redirect属性指定. 主要属性: wxml <naviga ...
- RedHat 7.3 修改ASM磁盘绑定路径
RedHat 7中,很多命令发生了改变,绑定磁盘不再是start_udev,而是udevadm,具体绑定方式,请看另一篇博文: http://www.cnblogs.com/zx3212/p/6757 ...