mybatisplus批量更新太慢该怎么解决

其他教程   发布日期:2023年08月04日   浏览次数:466

这篇文章主要讲解了“mybatisplus批量更新太慢该怎么解决”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“mybatisplus批量更新太慢该怎么解决”吧!

最近使用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<AbstractMethod> getMethodList(Class<?> mapperClass) {
  21. List<AbstractMethod> 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<T> 定义

  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<T> extends BaseMapper<T> {
  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<T> 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<T> list);
  28. int pgInsertOrUpdateBatch(@Param("list") List<T> 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<TScore> {
  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 = "<script>insert into %s %s values %s</script>";
  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("<foreach collection="list" item="item" index="index" open="(" separator="),(" close=")">");
  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("</foreach>");
  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 = "<script>insert into %s %s values %s ON DUPLICATE KEY UPDATE %s</script>";
  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("<foreach collection="list" item="item" index="index" open="(" separator="),(" close=")">");
  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("</foreach>");
  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 = "<script>insert into %s %s values %s on conflict (id) do update set %s </script>";
  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("<foreach collection="list" item="item" index="index" open="(" separator="),(" close=")">");
  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("</foreach>");
  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<TNewExerciseMapper, TNewExercise>
  3. implements TNewExerciseService {
  4. Logger logger = LoggerFactory.getLogger(getClass());
  5. //引入mapper
  6. @Autowired
  7. TScoreMapper scoreMapper;
  8. //这样就可以批量新增更新操作了
  9. public void test(List<TScore> 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<TScore> 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. }

以上就是mybatisplus批量更新太慢该怎么解决的详细内容,更多关于mybatisplus批量更新太慢该怎么解决的资料请关注九品源码其它相关文章!