• spring boot + sql server大数据量批量新增


    1.SQLServerBulkCopy方式(推荐这种没什么诡异的bug)

    代码
    1. /**
    2. * 获取类中所有属性注解@TableId@TableField
    3. *
    4. * @param instance
    5. * @return
    6. * @throws NoSuchFieldException
    7. */
    8. public static Map getDeclaredFieldsInfo(Object instance) throws NoSuchFieldException {
    9. Map map = new HashMap();
    10. Class clazz = instance.getClass();
    11. Field[] fields = clazz.getDeclaredFields();
    12. StringBuilder stringBuilder = new StringBuilder();
    13. for (int i = 0; i < fields.length; i++) {
    14. String annotationName = "";
    15. boolean annotationId = fields[i].isAnnotationPresent(TableId.class);
    16. if (annotationId) {
    17. // 获取注解值
    18. annotationName = fields[i].getAnnotation(TableId.class).value();
    19. }
    20. boolean annotationPresent = fields[i].isAnnotationPresent(TableField.class);
    21. if (annotationPresent) {
    22. // 获取注解值
    23. annotationName = fields[i].getAnnotation(TableField.class).value();
    24. }
    25. stringBuilder.append(annotationName);
    26. stringBuilder.append(",");
    27. // 字段名称
    28. String attributesName = fields[i].getName();
    29. map.put(attributesName, new String[]{annotationName, fields[i].getType().getName()});
    30. }
    31. return map;
    32. }
    33. /**
    34. * 采用saveBulkCopy的方式批量保存数据
    35. *
    36. * @param objects
    37. * @param
    38. */
    39. public void saveBulkCopy(List objects, String tableName) throws SQLException {
    40. if (ObjectUtil.isEmpty(objects)) {
    41. return;
    42. }
    43. T objClass = objects.get(0);
    44. Connection connection = ConnectionFactory.getConnection();
    45. try {
    46. // 数据库表名
    47. // String tableName = objClass.getClass().getAnnotation(TableName.class).value();
    48. // 表字段属性名和类型@TableId和@TableField
    49. Map declaredFieldsInfo = getDeclaredFieldsInfo(objClass);
    50. ResultSet rs = executeSQL(connection, "select * from " + tableName + " where 1=0");
    51. CachedRowSetImpl crs = new CachedRowSetImpl();
    52. crs.populate(rs);
    53. //循环插入数据
    54. long startTime = System.currentTimeMillis();
    55. //既然是批量插入肯定是需要循环
    56. for (int i = 0, leg = objects.size(); i < leg; i++) {
    57. //移动指针到“插入行”,插入行是一个虚拟行
    58. crs.moveToInsertRow();
    59. //更新虚拟行的数据(实体类要新增的字段,大家根据自己的实体类的字段来修改)
    60. //数据库字段 ,填写的值
    61. JSONObject jsonObject = JSONUtil.parseObj(objects.get(i), false, true);
    62. for (Map.Entry map : jsonObject.entrySet()) {
    63. // 属性和值
    64. String key = map.getKey();
    65. Object value = map.getValue();
    66. // 过滤不是数据库的字段
    67. if (!declaredFieldsInfo.containsKey(key)) {
    68. continue;
    69. }
    70. // 字段名称,字段类型
    71. String[] strings = declaredFieldsInfo.get(key);
    72. String type = strings[1];
    73. if (StrUtil.equals(type, String.class.getName())) {
    74. if (ObjectUtil.isEmpty(value) || StrUtil.equals("null", value.toString())) {
    75. crs.updateString(key, null);
    76. } else {
    77. crs.updateString(key, value.toString());
    78. }
    79. } else if (StrUtil.equals(type, Integer.class.getName())) {
    80. if (ObjectUtil.isEmpty(value) || StrUtil.equals("null", value.toString())) {
    81. crs.updateInt(key, 0);
    82. } else {
    83. crs.updateInt(key, Integer.parseInt(value.toString()));
    84. }
    85. } else if (StrUtil.equals(type, LocalDateTime.class.getName())) {
    86. LocalDateTime localDateTime = (LocalDateTime) value;
    87. crs.updateDate(key, localTimeToDate(localDateTime));
    88. } else if (StrUtil.equals(type, LocalDate.class.getName())) {
    89. LocalDate localDate = (LocalDate) value;
    90. crs.updateDate(key, java.sql.Date.valueOf(localDate));
    91. } else if (StrUtil.equals(type, Long.class.getName())) {
    92. crs.updateLong(key, 0);
    93. } else if (StrUtil.equals(type, Double.class.getName())) {
    94. crs.updateDouble(key, (Double) value);
    95. } else if (StrUtil.equals(type, int.class.getName())) {
    96. crs.updateInt(key, (Integer) value);
    97. } else if (StrUtil.equals(type, Float.class.getName())) {
    98. crs.updateFloat(key, (Float) value);
    99. } else if (StrUtil.equals(type, java.util.Date.class.getName())) {
    100. java.util.Date date = (java.util.Date) value;
    101. crs.updateDate(key, new java.sql.Date(date.getTime()));
    102. } else if (StrUtil.equals(type, java.math.BigDecimal.class.getName())) {
    103. if (ObjectUtil.isEmpty(value) || StrUtil.equals("null", value.toString())) {
    104. crs.updateBigDecimal(key, BigDecimal.ZERO);
    105. } else {
    106. BigDecimal bigDecimal = null;
    107. if (value instanceof BigDecimal) {
    108. bigDecimal = (BigDecimal) value;
    109. } else if (value instanceof String) {
    110. bigDecimal = new BigDecimal((String) value);
    111. } else if (value instanceof BigInteger) {
    112. bigDecimal = new BigDecimal((BigInteger) value);
    113. } else if (value instanceof Number) {
    114. bigDecimal = new BigDecimal(((Number) value).doubleValue());
    115. }
    116. crs.updateBigDecimal(key, bigDecimal);
    117. }
    118. } else {
    119. log.info("未知的数据类型:{}", type);
    120. }
    121. }
    122. //插入虚拟行
    123. crs.insertRow();
    124. //移动指针到当前行
    125. crs.moveToCurrentRow();
    126. }
    127. //进行批量插入
    128. SQLServerBulkCopyOptions copyOptions = new SQLServerBulkCopyOptions();
    129. copyOptions.setKeepIdentity(false);
    130. copyOptions.setBatchSize(1000);
    131. copyOptions.setUseInternalTransaction(false);
    132. SQLServerBulkCopy bulkCopy = new SQLServerBulkCopy(connection);
    133. bulkCopy.setBulkCopyOptions(copyOptions);
    134. bulkCopy.setDestinationTableName(tableName);
    135. bulkCopy.writeToServer(crs);
    136. crs.close();
    137. bulkCopy.close();
    138. log.info("耗时:{},数量:{}", System.currentTimeMillis() - startTime, objects.size());
    139. } catch (Exception e) {
    140. e.printStackTrace();
    141. }
    142. }
    143. // 数据查找,返回查找的内容,向上抛异常
    144. public ResultSet executeSQL(Connection con, String sql, Object... object) throws SQLException {
    145. PreparedStatement ps = con.prepareStatement(sql);
    146. for (int i = 0; i < object.length; i++) {
    147. //ps传入参数的下标是从1开始
    148. ps.setObject(i + 1, object[i]);
    149. }
    150. //返回结果集
    151. return ps.executeQuery();
    152. }
    153. public static java.sql.Date localTimeToDate(LocalDateTime lt) {
    154. return new java.sql.Date(lt.atZone(ZoneId.systemDefault()).toInstant()
    155. .toEpochMilli());
    156. }

    因为我有很多张表(表名称不同,字段相同)都要走这个方法,所以我的表名是直接写死的,如果你不是这样的话可以在实体类上直接使用@Table()指定数据库表即可,无需传入表名。

    直接复制粘贴代码就可以使用,调用的是saveBulkCopy方法,这个方法是sqlserver独有的新增方法,mybaits plus的批量新增对于sqlserver来说其实还是慢新增,而且也无法手写foreach新增,因为sqlserver对与参数是有限制的限制在2100个参数。

    用以上方法记得要在实体类标记对应注解,我把我的sqlserver数据库变成了自增主键,也是需要打上注解的,所以无需担心,直接使用即可。

    实体类
    1. package cn.iocoder.yudao.module.biz.dal.dataobject;
    2. import com.baomidou.mybatisplus.annotation.TableField;
    3. import lombok.Data;
    4. import java.math.BigDecimal;
    5. import java.util.Date;
    6. @Data
    7. public class SdyDataVisualDailySortResult {
    8. @TableField("id")
    9. private Integer id;
    10. @TableField("targetTime")
    11. private Date targetTime;
    12. @TableField("manager")
    13. private String manager;
    14. }
    ConnectionFactory 建立连接方法
    1. public class ConnectionFactory {
    2. private static String DRIVER = "com.microsoft.sqlserver.jdbc.SQLServerDriver";
    3. private static String URL = "你的数据库连接";
    4. private static String USER = "账号";
    5. private static String PASSWORD = "密码";
    6. /**
    7. * 提供getConnection()方法
    8. * @return Connection
    9. */
    10. public static Connection getConnection(){
    11. Connection conn = null;
    12. try {
    13. Class.forName(DRIVER);
    14. conn = DriverManager.getConnection(URL, USER, PASSWORD);
    15. } catch (ClassNotFoundException e) {
    16. e.printStackTrace();
    17. } catch (SQLException e) {
    18. e.printStackTrace();
    19. }
    20. return conn;
    21. }
    22. }

    实测第一种方式新增40w+数据仅需30左右

    2.原始jdbc方式(有诡异bug,技术不够无法查明原因)

    代码
    1. public void insertBach(List dashboardsDOS) throws SQLException {
    2. Connection conn = null;
    3. try {
    4. // SQLSERVER驱动
    5. Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver");
    6. // 情况2:使用批处理 ==> rewriteBatchedStatements=true ==> 每次50000条 耗时: 6秒
    7. conn = DriverManager.getConnection("数据库连接", "账号", "密码");
    8. //批量插入50000
    9. int batchSize = 50000;
    10. // 总条数1000000
    11. int count = dashboardsDOS.size();
    12. //设置自动提交为false
    13. conn.setAutoCommit(false);
    14. PreparedStatement ps = conn.prepareStatement("insert into " + "数据库表名" + " ("字段1","字段2","字段3") values ("1","2","3")");
    15. Long t1 = System.currentTimeMillis();
    16. System.out.println("========开始运行=========");
    17. for (int i = 0; i < dashboardsDOS.size(); ++i) {
    18. //设置第一个参数的值为i
    19. ps.setString(1, dashboardsDOS.get(i).getStoreCode());
    20. ps.setString(2, dashboardsDOS.get(i).getStoreName());
    21. ps.setTimestamp(3, new Timestamp(format.parse(format.format(calendar.getTime())).getTime()));
    22. //将该条记录添加到批处理中
    23. ps.addBatch();
    24. if (i % batchSize == 0) {
    25. //执行批处理
    26. ps.executeBatch();
    27. //提交
    28. conn.commit();
    29. System.out.println(i + ":添加" + batchSize + "条");
    30. }
    31. }
    32. if (count % batchSize != 0) {
    33. ps.executeBatch();
    34. conn.commit();
    35. }
    36. ps.close();
    37. Long t2 = System.currentTimeMillis();
    38. System.out.println("总条数:" + count + "条 每次插入" + batchSize + "条 " + " 每次耗时:" + (t2 - t1) / 1000 + "秒");
    39. } catch (Exception e) {
    40. e.printStackTrace();
    41. } finally {
    42. conn.close();
    43. }
    44. }
    注意事项

    如果你也有时间类型的数据建议使用Timestamp来接收处理值,使用sql.Date会出现时间精度差异问题;

    诡异bug

    在开始的时候新增也是很快,基本40w+数据在40秒左右,但是后来由于业务需求,我在调用新增方法前将数据处理赋值了一下,之后40w+数据要在240秒左右,至今无法查明到底是为什么(只是将某个字段进行了重新赋值,传入后数据新增变慢,如果不处理该数据依旧很快)

  • 相关阅读:
    《持续交付:发布可靠软件的系统方法》- 读书笔记(十四)
    1031 Hello World for U
    hgdb数据编辑
    ssm基于web的酒店预订及个性化服务系统 毕业设计源码241822
    哪些PHP开源作品值得关注
    恶意软件反向关闭EDR的原理、测试和反制思考
    ProcessEngineEndpoint
    ARM 反汇编工具objdump的使用简介
    MySQL查询数据库表记录数
    如何正确地使用ChatGPT(角色扮演+提示工程)
  • 原文地址:https://blog.csdn.net/FanZaiYo/article/details/133071167