zoukankan      html  css  js  c++  java
  • 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>
    

    完成

  • 相关阅读:
    python快速入门及进阶
    Git基础及进阶-系统总结
    go语言快速入门教程
    go学习笔记
    win10中,vscode安装go插件排雷指南
    centos7下安装pcre库(pcretest)
    c语言typedef
    虚拟机中安装centos7后无法上网,使用桥接网络+ssh
    强化学习-Q-learning学习笔记
    python基础教程系列1-基础语法
  • 原文地址:https://www.cnblogs.com/pluto4596/p/11174300.html
Copyright © 2011-2022 走看看