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

  1. 参数是ParamMap或者 StrictMap
  2. 参数大于两个,且第一个和后面任意一个不相等

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

    /**
     * 简单批量插入实体对象
     *
     * @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>

完成

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,658评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,482评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,213评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,395评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,487评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,523评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,525评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,300评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,753评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,048评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,223评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,905评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,541评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,168评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,417评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,094评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,088评论 2 352

推荐阅读更多精彩内容