Mybatis 批量插入 數量限制

故事

今天下午公司技術分享,一個夥伴提到他踩過坑:mybatis批量插入時動態sql允許的最大參數數量是2100個。即下面代碼中“#{…}”的數量。

<insert id="batchInsert" parameterType="list">
  insert into Adv_permeability values
  <foreach collection="permeabilityList" separator="," item="permeability">
    (#{permeability.areaId}, #{permeability.areaName}, #{permeability.flag})
  </foreach>
</insert>

當時就有一個小夥伴不開心了,他明確表示自己曾經成功插入超過2100個參數,並當場拉出了代碼證明之。由此猜測mybatis批量插入的上限是按對象數量限制的,不是參數,雙方爭執不下,一片歡樂。

探究

mybatis 真的對動態 SQL 的 參數 或者 對象 數量設定了限制麼?如果有那麼原因何在?如果沒有,那麼爲什麼雙方都那麼確定呢?難道是 數據庫 不同導致的?

本着最小投入的原則,筆者開始了測試。手頭現有 SQLServer 和 Mysql 就測這兩個吧。腳本使用上述的簡單腳本。

SQLServer

對 SQLServer 進行2000次批量插入,實體參數是3個,即本次插入參數數量是 2000*3=6000,結果是 報錯 了!

com.microsoft.sqlserver.jdbc.SQLServerException: 傳入的請求具有過多的參數。該服務器支持最多 2100 個參數。請減少參數的數目,然後重新發送該請求。

可以清楚看到,錯誤是 SQLServer 的JDBC包拋出的。

不過,筆者記得在用 SqlServer Management Studio 批量插入式,最大條數有 1000 的限制, 那麼在用 Mybatis 時還會有麼?這裏將插入參數變爲一個 {permeability.areaId} ,插入 1315 條,結果 報錯了!

com.microsoft.sqlserver.jdbc.SQLServerException: INSERT 語句中行值表達式的數目超出了 1000 行值的最大允許值。

果然,是 SQLServer 數據庫自己的鬼,不是 Mybatis 的鍋!

Mysql

對 Mysql 進行同樣的2000次批量插入,結果是完美執行。筆者一咬牙,來了次 200000 的親密接觸,結果 報錯 了!

com.mysql.jdbc.PacketTooBigException: Packet for query is too large (8346602 > 4194304). You can change this value on the server by setting the max_allowed_packet’ variable.

看來 Mysql 也有限制,跟 SQLServer “數參數個數” 的限制方法不同, Mysql 對執行的SQL語句大小進行限制,相當於對字符串進行限制。根據報錯內容,默認允許最大SQL是 4M 。這個錯誤是 Mysql 的JDBC包拋出的。

可以使用語句查詢這個值並改變:

# 查詢當前大小
mysql> select @@max_allowed_packet;
+----------------------+
| @@max_allowed_packet |
+----------------------+
|              4194304 |
+----------------------+
1 row in set (0.00 sec)

# 修改爲256M
mysql> SET GLOBAL max_allowed_packet=268435456;
Query OK, 0 rows affected (0.00 sec)

# 退出客戶端重新登錄
mysql> select @@max_allowed_packet;
+----------------------+
| @@max_allowed_packet |
+----------------------+
|            268435456 |
+----------------------+
1 row in set (0.00 sec)

Mybatis

簡單看一下 Mybatis 解析動態SQL的源碼。

// 開始解析
public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
        configurationElement(parser.evalNode("/mapper"));
        configuration.addLoadedResource(resource);
        bindMapperForNamespace();
    }

    parsePendingResultMaps();
    parsePendingChacheRefs();
    parsePendingStatements();
}
// 解析mapper
private void configurationElement(XNode context) {
    try {
        String namespace = context.getStringAttribute("namespace");
        if (namespace.equals("")) {
            throw new BuilderException("Mapper's namespace cannot be empty");
        }
        builderAssistant.setCurrentNamespace(namespace);
        cacheRefElement(context.evalNode("cache-ref"));
        cacheElement(context.evalNode("cache"));
        parameterMapElement(context.evalNodes("/mapper/parameterMap"));
        resultMapElements(context.evalNodes("/mapper/resultMap"));
        sqlElement(context.evalNodes("/mapper/sql"));
        buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
        throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
    }
}

// 創建 select|insert|update|delete 語句
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
        final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
        try {
            statementParser.parseStatementNode();
        } catch (IncompleteElementException e) {
            configuration.addIncompleteStatement(statementParser);
        }
    }
}

// 填充參數,創建語句
public BoundSql getBoundSql(Object parameterObject) {
    DynamicContext context = new DynamicContext(configuration, parameterObject);
    rootSqlNode.apply(context);
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
    SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
        boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
    }
    return boundSql;
}

從開始到結束, Mybatis 都沒有對填充的條數和參數的數量做限制。

結語

  • SqlServer 對語句的條數和參數的數量都有限制,分別是 1000 和 2100。
  • Mysql 對語句的長度有限制,默認是 4M。
  • Mybatis 對動態語句沒有數量上的限制。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章