• mybatis-plus批量更新太慢,如何解决?


    最近使用mybatis-plus的 saveOrUpdateBath 和saveBath接口执行特别慢,数据量大时往往需要十几分钟,打开日志查看原来批量操作也是循环单条数据插入的,那有没有批量更新的办法呢??

    mybatis-plus 提供了一个自定义方法sql注入器DefaultSqlInjector我们可以通过继DefaultSqlInjector来加入自定义的方法达到批量插入的效果。

    1. import com.baomidou.mybatisplus.core.injector.AbstractMethod;
    2. import com.baomidou.mybatisplus.core.injector.DefaultSqlInjector;
    3. import org.springframework.stereotype.Component;
    4. import java.util.List;
    5. /**
    6. * @Description: 自定义方法SQL注入器
    7. * @Title: CustomizedSqlInjector
    8. * @Package com.highgo.edu.common.batchOperation
    9. * @Author:
    10. * @Copyright
    11. * @CreateTime: 2022/11/3 16:21
    12. */
    13. @Component
    14. public class CustomizedSqlInjector extends DefaultSqlInjector {
    15. /**
    16. * 如果只需增加方法,保留mybatis plus自带方法,
    17. * 可以先获取super.getMethodList(),再添加add
    18. */
    19. @Override
    20. public List getMethodList(Class mapperClass) {
    21. List methodList = super.getMethodList(mapperClass);
    22. methodList.add(new InsertBatchMethod());
    23. // methodList.add(new UpdateBatchMethod());
    24. methodList.add(new MysqlInsertOrUpdateBath());
    25. methodList.add(new PGInsertOrUpdateBath());
    26. return methodList;
    27. }
    28. }

    同时我们需要继承BaseMapper 定义

    1. import com.baomidou.mybatisplus.core.mapper.BaseMapper;
    2. import org.apache.ibatis.annotations.Param;
    3. import java.util.List;
    4. /**
    5. * @description:自定义接口覆盖BaseMapper,解决mybatis-plus 批量操作慢的问题
    6. * @author:
    7. * @date: 2022/11/3 15:14
    8. * @param: null
    9. * @return:
    10. **/
    11. public interface RootMapper extends BaseMapper {
    12. /**
    13. * @description:批量插入
    14. * @author:
    15. * @date: 2022/11/3 15:13
    16. * @param: [list]
    17. * @return: int
    18. **/
    19. int insertBatch(@Param("list") List list);
    20. /**
    21. * @description:批量插入更新
    22. * @author:
    23. * @date: 2022/11/3 15:14
    24. * @param: [list]
    25. * @return: int
    26. **/
    27. int mysqlInsertOrUpdateBatch(@Param("list") List list);
    28. int pgInsertOrUpdateBatch(@Param("list") List list);
    29. }

    在需要使用批量更新插入的mapper上使用自定义的RootMapper

    如下图

    1. import com.XX.edu.common.batchOperation.RootMapper;
    2. import com.XX.edu.exam.model.TScore;
    3. import org.springframework.stereotype.Repository;
    4. /**
    5. * @Entity com.XX.edu.exam.model.TScore
    6. */
    7. @Repository
    8. public interface TScoreMapper extends RootMapper {
    9. }

    下面我们来定义批量插入的方法:

    1. package com.XX.edu.common.batchOperation;
    2. import com.baomidou.mybatisplus.annotation.IdType;
    3. import com.baomidou.mybatisplus.core.enums.SqlMethod;
    4. import com.baomidou.mybatisplus.core.injector.AbstractMethod;
    5. import com.baomidou.mybatisplus.core.metadata.TableInfo;
    6. import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
    7. import org.apache.commons.lang3.StringUtils;
    8. import org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator;
    9. import org.apache.ibatis.executor.keygen.KeyGenerator;
    10. import org.apache.ibatis.executor.keygen.NoKeyGenerator;
    11. import org.apache.ibatis.mapping.MappedStatement;
    12. import org.apache.ibatis.mapping.SqlSource;
    13. import org.slf4j.Logger;
    14. import org.slf4j.LoggerFactory;
    15. /**
    16. * @Description: 批量插入的方法
    17. * @Title: InsertBatchMethod
    18. * @Package com.XX.edu.common.batchOperation
    19. * @Author:
    20. * @CreateTime: 2022/11/3 15:16
    21. */
    22. public class InsertBatchMethod extends AbstractMethod {
    23. Logger logger = LoggerFactory.getLogger(getClass());
    24. @Override
    25. public MappedStatement injectMappedStatement(Class mapperClass, Class modelClass, TableInfo tableInfo) {
    26. final String sql = "";
    27. final String fieldSql = prepareFieldSql(tableInfo);
    28. final String valueSql = prepareValuesSql(tableInfo);
    29. final String sqlResult = String.format(sql, tableInfo.getTableName(), fieldSql, valueSql);
    30. logger.debug("sqlResult----->{}", sqlResult);
    31. SqlSource sqlSource = languageDriver.createSqlSource(configuration, sqlResult, modelClass);
    32. KeyGenerator keyGenerator = new NoKeyGenerator();
    33. SqlMethod sqlMethod = SqlMethod.INSERT_ONE;
    34. String keyProperty = null;
    35. String keyColumn = null;
    36. // 表包含主键处理逻辑,如果不包含主键当普通字段处理
    37. if (StringUtils.isNotEmpty(tableInfo.getKeyProperty())) {
    38. if (tableInfo.getIdType() == IdType.AUTO) {
    39. /* 自增主键 */
    40. keyGenerator = new Jdbc3KeyGenerator();
    41. keyProperty = tableInfo.getKeyProperty();
    42. keyColumn = tableInfo.getKeyColumn();
    43. } else {
    44. if (null != tableInfo.getKeySequence()) {
    45. keyGenerator = TableInfoHelper.genKeyGenerator(sqlMethod.getMethod(),tableInfo, builderAssistant);
    46. keyProperty = tableInfo.getKeyProperty();
    47. keyColumn = tableInfo.getKeyColumn();
    48. }
    49. }
    50. }
    51. // 第三个参数必须和RootMapper的自定义方法名一致
    52. return this.addInsertMappedStatement(mapperClass, modelClass, "insertBatch", sqlSource, keyGenerator, keyProperty, keyColumn);
    53. }
    54. /**
    55. * @description: 拼接字段值
    56. * @author:
    57. * @date: 2022/11/3 15:20
    58. * @param: [tableInfo]
    59. * @return: java.lang.String
    60. **/
    61. private String prepareValuesSql(TableInfo tableInfo) {
    62. final StringBuilder valueSql = new StringBuilder();
    63. valueSql.append("");
    64. //valueSql.append("#{item.").append(tableInfo.getKeyProperty()).append("},");
    65. tableInfo.getFieldList().forEach(x -> valueSql.append("#{item.").append(x.getProperty()).append("},"));
    66. valueSql.delete(valueSql.length() - 1, valueSql.length());
    67. valueSql.append("");
    68. return valueSql.toString();
    69. }
    70. /**
    71. * @description:拼接字段
    72. * @author:
    73. * @date: 2022/11/3 15:20
    74. * @param: [tableInfo]
    75. * @return: java.lang.String
    76. **/
    77. private String prepareFieldSql(TableInfo tableInfo) {
    78. StringBuilder fieldSql = new StringBuilder();
    79. //fieldSql.append(tableInfo.getKeyColumn()).append(",");
    80. tableInfo.getFieldList().forEach(x -> {
    81. fieldSql.append(x.getColumn()).append(",");
    82. });
    83. fieldSql.delete(fieldSql.length() - 1, fieldSql.length());
    84. fieldSql.insert(0, "(");
    85. fieldSql.append(")");
    86. return fieldSql.toString();
    87. }
    88. }

    继续定义批量插入更新的抽象方法

    1. package com.XX.edu.common.batchOperation;
    2. import com.baomidou.mybatisplus.core.injector.AbstractMethod;
    3. import com.baomidou.mybatisplus.core.metadata.TableInfo;
    4. import org.apache.ibatis.executor.keygen.NoKeyGenerator;
    5. import org.apache.ibatis.mapping.MappedStatement;
    6. import org.apache.ibatis.mapping.SqlSource;
    7. /**
    8. * @Description: 批量插入更新
    9. * @Title: InsertOrUpdateBath
    10. * @Package com.XX.edu.common.batchOperation
    11. * @Author:
    12. * @Copyright
    13. * @CreateTime: 2022/11/3 15:23
    14. */
    15. public abstract class InsertOrUpdateBathAbstract extends AbstractMethod {
    16. @Override
    17. public MappedStatement injectMappedStatement(Class mapperClass, Class modelClass, TableInfo tableInfo) {
    18. final SqlSource sqlSource = prepareSqlSource(tableInfo, modelClass);
    19. // 第三个参数必须和RootMapper的自定义方法名一致
    20. return this.addInsertMappedStatement(mapperClass, modelClass, prepareInsertOrUpdateBathName(), sqlSource, new NoKeyGenerator(), null, null);
    21. }
    22. protected abstract SqlSource prepareSqlSource(TableInfo tableInfo, Class modelClass);
    23. protected abstract String prepareInsertOrUpdateBathName();
    24. }

    继承上面的抽象类----mysql版本(本版本未测试 根据自己需求修改)

    1. package com.XX.edu.common.batchOperation;
    2. import com.baomidou.mybatisplus.core.metadata.TableInfo;
    3. import org.apache.ibatis.mapping.SqlSource;
    4. import org.springframework.util.StringUtils;
    5. /**
    6. * @Description: 批量插入更新
    7. * @Title: InsertOrUpdateBath
    8. * @Package com.XX.edu.common.batchOperation
    9. * @Author:
    10. * @Copyright
    11. * @CreateTime: 2022/11/3 15:23
    12. */
    13. public class MysqlInsertOrUpdateBath extends InsertOrUpdateBathAbstract {
    14. @Override
    15. protected SqlSource prepareSqlSource(TableInfo tableInfo, Class modelClass) {
    16. final String sql = "";
    17. final String tableName = tableInfo.getTableName();
    18. final String filedSql = prepareFieldSql(tableInfo);
    19. final String modelValuesSql = prepareModelValuesSql(tableInfo);
    20. final String duplicateKeySql = prepareDuplicateKeySql(tableInfo);
    21. final String sqlResult = String.format(sql, tableName, filedSql, modelValuesSql, filedSql, duplicateKeySql);
    22. //String.format(sql, tableName, filedSql, modelValuesSql, duplicateKeySql);
    23. //System.out.println("savaorupdatesqlsql="+sqlResult);
    24. return languageDriver.createSqlSource(configuration, sqlResult, modelClass);
    25. }
    26. @Override
    27. protected String prepareInsertOrUpdateBathName() {
    28. return "mysqlInsertOrUpdateBath";
    29. }
    30. String prepareDuplicateKeySql(TableInfo tableInfo) {
    31. final StringBuilder duplicateKeySql = new StringBuilder();
    32. if (!StringUtils.isEmpty(tableInfo.getKeyColumn())) {
    33. duplicateKeySql.append(tableInfo.getKeyColumn()).append("=values(").append(tableInfo.getKeyColumn()).append("),");
    34. }
    35. tableInfo.getFieldList().forEach(x -> {
    36. duplicateKeySql.append(x.getColumn())
    37. .append("=values(")
    38. .append(x.getColumn())
    39. .append("),");
    40. });
    41. duplicateKeySql.delete(duplicateKeySql.length() - 1, duplicateKeySql.length());
    42. return duplicateKeySql.toString();
    43. }
    44. String prepareModelValuesSql(TableInfo tableInfo) {
    45. final StringBuilder valueSql = new StringBuilder();
    46. valueSql.append("");
    47. if (!StringUtils.isEmpty(tableInfo.getKeyProperty())) {
    48. valueSql.append("#{item.").append(tableInfo.getKeyProperty()).append("},");
    49. }
    50. tableInfo.getFieldList().forEach(x -> valueSql.append("#{item.").append(x.getProperty()).append("},"));
    51. valueSql.delete(valueSql.length() - 1, valueSql.length());
    52. valueSql.append("");
    53. return valueSql.toString();
    54. }
    55. /**
    56. * @description:准备属性名
    57. * @author:
    58. * @date: 2022/11/3 15:25
    59. * @param: [tableInfo]
    60. * @return: java.lang.String
    61. **/
    62. String prepareFieldSql(TableInfo tableInfo) {
    63. StringBuilder fieldSql = new StringBuilder();
    64. fieldSql.append(tableInfo.getKeyColumn()).append(",");
    65. tableInfo.getFieldList().forEach(x -> {
    66. fieldSql.append(x.getColumn()).append(",");
    67. });
    68. fieldSql.delete(fieldSql.length() - 1, fieldSql.length());
    69. fieldSql.insert(0, "(");
    70. fieldSql.append(")");
    71. return fieldSql.toString();
    72. }
    73. }

    继承上面的抽象类----postgresql版本(已测试完成,其中id使用序列自增)

    1. package com.XX.edu.common.batchOperation;
    2. import com.baomidou.mybatisplus.core.metadata.TableInfo;
    3. import org.apache.ibatis.mapping.SqlSource;
    4. import org.slf4j.Logger;
    5. import org.slf4j.LoggerFactory;
    6. import org.springframework.util.StringUtils;
    7. /**
    8. * @Description: 批量插入更新
    9. * @Title: InsertOrUpdateBath
    10. * @Package com.XX.edu.common.batchOperation
    11. * @Author:
    12. * @Copyright
    13. * @CreateTime: 2022/11/3 15:23
    14. */
    15. public class PGInsertOrUpdateBath extends InsertOrUpdateBathAbstract {
    16. Logger logger = LoggerFactory.getLogger(getClass());
    17. @Override
    18. protected SqlSource prepareSqlSource(TableInfo tableInfo, Class modelClass) {
    19. final String sql = "";
    20. final String tableName = tableInfo.getTableName();
    21. final String filedSql = prepareFieldSql(tableInfo);
    22. final String modelValuesSql = prepareModelValuesSql(tableInfo);
    23. final String duplicateKeySql = prepareDuplicateKeySql(tableInfo);
    24. final String sqlResult = String.format(sql, tableName, filedSql, modelValuesSql, duplicateKeySql);
    25. logger.info("sql=={}",sqlResult);
    26. return languageDriver.createSqlSource(configuration, sqlResult, modelClass);
    27. }
    28. @Override
    29. protected String prepareInsertOrUpdateBathName() {
    30. return "pgInsertOrUpdateBatch";
    31. }
    32. private String prepareDuplicateKeySql(TableInfo tableInfo) {
    33. final StringBuilder duplicateKeySql = new StringBuilder();
    34. if (!StringUtils.isEmpty(tableInfo.getKeyColumn())) {
    35. duplicateKeySql.append(tableInfo.getKeyColumn()).append("=excluded.").append(tableInfo.getKeyColumn()).append(",");
    36. }
    37. tableInfo.getFieldList().forEach(x -> {
    38. duplicateKeySql.append(x.getColumn())
    39. .append("=excluded.")
    40. .append(x.getColumn())
    41. .append(",");
    42. });
    43. duplicateKeySql.delete(duplicateKeySql.length() - 1, duplicateKeySql.length());
    44. return duplicateKeySql.toString();
    45. }
    46. private String prepareModelValuesSql(TableInfo tableInfo) {
    47. final StringBuilder valueSql = new StringBuilder();
    48. valueSql.append("");
    49. if (!StringUtils.isEmpty(tableInfo.getKeyProperty())) {
    50. valueSql.append("#{item.").append(tableInfo.getKeyProperty()).append("},");
    51. }
    52. tableInfo.getFieldList().forEach(x -> valueSql.append("#{item.").append(x.getProperty()).append("},"));
    53. valueSql.delete(valueSql.length() - 1, valueSql.length());
    54. valueSql.append("");
    55. return valueSql.toString();
    56. }
    57. /**
    58. * @description:准备属性名
    59. * @author:
    60. * @date: 2022/11/3 15:25
    61. * @param: [tableInfo]
    62. * @return: java.lang.String
    63. **/
    64. private String prepareFieldSql(TableInfo tableInfo) {
    65. StringBuilder fieldSql = new StringBuilder();
    66. if (!StringUtils.isEmpty(tableInfo.getKeyProperty())) {
    67. fieldSql.append(tableInfo.getKeyColumn()).append(",");
    68. }
    69. tableInfo.getFieldList().forEach(x -> {
    70. fieldSql.append(x.getColumn()).append(",");
    71. });
    72. fieldSql.delete(fieldSql.length() - 1, fieldSql.length());
    73. fieldSql.insert(0, "(");
    74. fieldSql.append(")");
    75. return fieldSql.toString();
    76. }
    77. }

     到此定义结束,下面开始使用

    1. @Service
    2. public class TNewExerciseServiceImpl extends ServiceImpl
    3. implements TNewExerciseService {
    4. Logger logger = LoggerFactory.getLogger(getClass());
    5. //引入mapper
    6. @Autowired
    7. TScoreMapper scoreMapper;
    8. //这样就可以批量新增更新操作了
    9. public void test(List collect){
    10. scoreMapper.pgInsertOrUpdateBatch(collect);
    11. }
    12. }

    但是如果collect数据量太大会出现异常
    “Tried to send an out-of-range integer as a 2-byte value: 87923”
    是因为pg对于sql语句的参数数量是有限制的,最大为32767。

    看pg源码

    1. public void sendInteger2(int val) throws IOException {
    2. if (val >= -32768 && val <= 32767) {
    3. this.int2Buf[0] = (byte)(val >>> 8);
    4. this.int2Buf[1] = (byte)val;
    5. this.pgOutput.write(this.int2Buf);
    6. } else {
    7. throw new IOException("Tried to send an out-of-range integer as a 2-byte value: " + val);
    8. }
    9. }

    从源代码中可以看到pgsql使用2个字节的integer,故其取值范围为[-32768, 32767]。

    这意味着sql语句的参数数量,即行数*列数之积必须小于等于32767.

    比如,总共有17个字段,因为最大是32767,这样最多允许32767/ 17 大约是1 927个,所以要分批操作,或有能力的童鞋可以自己修改pg的驱动呦

    分批插入代码如下:

    1. /**
    2. * @description:
    3. * @author:
    4. * @date: 2022/11/4 14:57
    5. * @param: [list, fieldCount:列数]
    6. * @return: void
    7. **/
    8. public void detachSaveOrUpdate_score(List list, int fieldCount) {
    9. int numberBatch = 32767; //每一次插入的最大数
    10. //每一次插入的最大行数 , 向下取整
    11. int v = ((Double) Math.floor(numberBatch / (fieldCount * 1.0))).intValue();
    12. double number = list.size() * 1.0 / v;
    13. int n = ((Double) Math.ceil(number)).intValue(); //向上取整
    14. for (int i = 0; i < n; i++) {
    15. int end = v * (i + 1);
    16. if (end > list.size()) {
    17. end = list.size(); //如果end不能超过最大索引值
    18. }
    19. scoreMapper.pgInsertOrUpdateBatch(list.subList(v * i, end)); //插入数据库
    20. logger.info("更新一次~~~{}-{}", v * i, end);
    21. }
    22. }

    完成收工~~~

  • 相关阅读:
    Kotlin协程:flowOn与线程切换
    13-面试官:synchronized有几种用法?
    正则表达式
    ANR问题分析的一般套路
    Git使用大全解
    Day804.原型模式与享元模式 -Java 性能调优实战
    【python】ModuleNotFoundError: No module named ‘skimage‘...
    谈对数据库索引的认识
    防止雪崩问题
    Typescript语言基础
  • 原文地址:https://blog.csdn.net/feixiangdexiaozhidan/article/details/127685532