OOM专题 - 如何使用JdbcTemplate查询百万行超大结果,以及其他JdbcTemplate 其他冷门实用技能

OOM专题 - 如何使用JdbcTemplate查询百万行超大结果,以及其他JdbcTemplate 其他冷门实用技能_jdbctemplate查询大批量数据-CSDN博客

问题:当一次性查询 百万行的数据,返回给前端的时候,很容易就会导致OOM。全网搜索,没有满意的解决方案,因此自己研究了 几番 JdbcTemplate 源码,手动编码解决。

 

原因:JDBC 一次性封装成了 百万个 对象,而且都不能被回收。如此大规模的快速生成对象,会立刻把JVM 盛满,而且都是不可回收对象,导致GC频繁,触发FullGC, 但是没什么实际效果。机器变得卡顿,OOM随之发生。

思路:自己手动控制 ResultSet 对象的生成过程,及时 将大对象 改变成小对象,临时保存为 磁盘的File ,立刻释放内存的占用,让GC及时回收垃圾。最终将文件流封装进 response 的输出流中,返回给前端。

          核心思想:此时的 ResultSet 仅仅是 数据库的一个游标指针(cursor),并不是真实的全部数据,所以并不会占据内存空间。理解这一点非常重要!!!我当年研究了 很多资料才得到这一看似简单的真知灼见。

          避坑指南:千万不能使用 SqlRowSet,因为 该对象是 真实的全部数据,并不是游标指针,所以会占据很多的内存空间。请看:

 
  1. // The results will be mapped to an SqlRowSet which holds the data in a disconnected fashion 意思就是 这是离线版的ResultSet ,而不是指针 ResultSet
  2. SqlRowSet queryForRowSet(String sql, Object... args) throws DataAccessException;
  3.  
  4. protected SqlRowSet createSqlRowSet(ResultSet rs) throws SQLException {
  5. CachedRowSet rowSet = newCachedRowSet();
  6. rowSet.populate(rs); // 这段代码的意思,就是 将 ResultSet 指针指向的数据,全部倒腾出来,缓存到JVM 本地
  7. return new ResultSetWrappingSqlRowSet(rowSet);
  8. }
 

解决以上问题的思路,最核心的示例代码如下:

每隔5w行,写一次磁盘,并且清空一次当前的大对象,让 GC 来 work ,收走 garbage 

 
  1. File file = new File("临时文件全路径")
  2. Object query = getJdbcTemplate().query(sql.toString(), new ResultSetExtractor<Object>() {
  3. @Override
  4. public Object extractData(ResultSet rs) throws SQLException, DataAccessException {
  5. while (rs.next()) {
  6. processRow(columnCount, rs, sb);
  7. if(行数 >= 50000){
  8. IOUtils.write(sb, new FileOutputStream(file,true), "UTF-8"); //及时写1次数磁盘
  9. sb.delete(0, sb.length());// GC 清理垃圾,重置 sb 大对象
  10. }
  11. }
  12. return file;
  13. }
  14. });
 

 完整代码如下: 可以直接CP到你的项目中使用 

 
  1. import lombok.extern.slf4j.Slf4j;
  2. import org.apache.commons.io.IOUtils;
  3. import org.springframework.dao.DataAccessException;
  4. import org.springframework.jdbc.core.BeanPropertyRowMapper;
  5. import org.springframework.jdbc.core.JdbcTemplate;
  6. import org.springframework.jdbc.core.ResultSetExtractor;
  7. import org.springframework.jdbc.support.JdbcUtils;
  8.  
  9. import java.io.File;
  10. import java.io.FileOutputStream;
  11. import java.io.IOException;
  12. import java.sql.ResultSet;
  13. import java.sql.SQLException;
  14. import java.util.List;
  15. import java.util.Map;
  16.  
  17. /**
  18. * 自定义的 ResultSetExtractor, 直接操作 数据库的指针 ResultSet ,并且返回最终想要的 CSV 文件
  19. *
  20. * 在其中做 逻辑分步处理,可以避免 OOM . 比如,
  21. * 直接返回 CSV 文件,避免封装成 List 或者Map 对象
  22. * 及时处理大对象,让GC 释放临时空间,避免撑爆 JVM 内存, 也可以解决 OOM 问题
  23. */
  24. public File queryBigResult(CharSequence sql,Object... o){
  25. // 一般只有超过 5万行 的 结果集,才需要 返回csv 格式,所以容量最好是 5w行的两倍(以每行估计 100个字符来算)
  26. final StringBuilder sb = new StringBuilder(5000000 * 2);
  27. final File file = new File(SystemUtils.getJavaIoTmpDir() + File.separator + "临时大文件存储,可以删除".concat(System.currentTimeMillis() + "").concat(".csv"));
  28.  
  29. Object query = getJdbcTemplate().query(sql.toString(), new ResultSetExtractor<Object>() {
  30. @Override
  31. public Object extractData(ResultSet rs) throws SQLException, DataAccessException {
  32. ResultSetMetaInfo info = new ResultSetMetaInfo(rs); //缓存基础信息,避免在while循环中反复调用,提高效率
  33. String[] columnNames = info.getColumnNames();
  34. int columnCount = info.getColumnCount(), line = 0;
  35.  
  36. // first line : coLNames
  37. line++;
  38. for (int i = 0; i < columnCount; i++) {
  39. sb.append(columnNames[i]).append(",");
  40. }
  41. sb.deleteCharAt(sb.length() - 1).append("\n");
  42.  
  43. while (rs.next()) {
  44. processRow(columnCount, rs, sb);
  45. line++;
  46.  
  47. // 数据及时落盘
  48. if (line >= 50000) {
  49. try {
  50. IOUtils.write(sb, new FileOutputStream(file,true), "UTF-8");
  51. } catch (IOException e) {
  52. log.error("正在写盘 [{}] 行时出错:{}", line, Utils.logMessage(e));
  53. }
  54. // reset line & result
  55. line = 0;
  56. sb.delete(0, sb.length());
  57. }
  58. }
  59.  
  60. try {
  61. IOUtils.write(sb, new FileOutputStream(file,true), "UTF-8");
  62. return file;
  63. } catch (IOException e) {
  64. log.error("最后写盘 [{}] 行时出错:{}", line, Utils.logMessage(e));
  65. }
  66.  
  67. sb.delete(0, sb.length());
  68. return file;
  69. }
  70. },o);
  71.  
  72. return file;
  73. }
  74.  
  75. /**
  76. * 大文件:处理单行
  77. */
  78. private static void processRow(int columnCount, ResultSet rs, StringBuilder sb) throws SQLException {
  79. for (int i = 1; i <= columnCount; i++) {
  80. Object value = JdbcUtils.getResultSetValue(rs, i); // 参考牛掰spring的工具类方法,真汗颜,当初我自研究了个把月,手写了Jdbc框架工具,后来才发现spring的完美写法
  81. sb.append(value).append(",");
  82. }
  83. sb.deleteCharAt(result.length() - 1).append("\n");
  84. }
 

上文中用到的 对象:

ResultSetMetaInfo: 为了缓存 ResultSet 指针的元数据信息,避免 在while 循环中 反复get 调用导致 的效率降低。
 
  1. import lombok.Builder;
  2. import lombok.Data;
  3. import lombok.SneakyThrows;
  4. import lombok.AllArgsConstructor;
  5. import lombok.NoArgsConstructor;
  6. import org.springframework.jdbc.support.JdbcUtils;
  7.  
  8. import java.sql.ResultSet;
  9. import java.sql.ResultSetMetaData;
  10.  
  11. /**
  12. * @author stormfeng
  13. * @date 2020-11-25 14:39
  14. */
  15. @Data
  16. @Builder
  17. @AllArgsConstructor
  18. @NoArgsConstructor
  19. public class ResultSetMetaInfo {
  20. private int columnCount;
  21. private int[] columnTypes;
  22. private String[] columnNames;
  23.  
  24. /**
  25. * @see org.springframework.jdbc.core.RowCountCallbackHandler#processRow(java.sql.ResultSet)
  26. */
  27. @SneakyThrows
  28. public ResultSetMetaInfo(ResultSet rs) {
  29. ResultSetMetaData rsmd = rs.getMetaData();
  30. this.columnCount = rsmd.getColumnCount();
  31. this.columnTypes = new int[columnCount];
  32. this.columnNames = new String[columnCount];
  33. for (int i = 0; i < columnCount; i++) {
  34. columnTypes[i] = rsmd.getColumnType(i + 1);
  35. columnNames[i] = JdbcUtils.lookupColumnName(rsmd, i + 1);
  36. }
  37. }
  38.  
  39. }
 

如果是 mysql ,请注意 ,

官网MySQL :: MySQL Connector/J 8.0 Developer Guide :: 6.4 JDBC API Implementation Notes

进行如下设置 ,共有1种可用设置,推荐第2种设置 fetchSize =100 。 

如果按照设置1 ,那么 resultset 每次取回的就是 1行 数据;单条取回,速度肯定较慢

 
  1. stmt = conn.createStatement(java.sql.ResultSet.TYPE_FORWARD_ONLY,
  2. java.sql.ResultSet.CONCUR_READ_ONLY);
  3. stmt.setFetchSize(Integer.MIN_VALUE);
 

如果按照设置2 ,那么 resultset 每次取回的就是 fetchsize的那几行 数据;比第1种肯定快很多,推荐

 
  1. conn = DriverManager.getConnection("jdbc:mysql://localhost/?useCursorFetch=true", "user", "s3cr3t");
  2. stmt = conn.createStatement();
  3. stmt.setFetchSize(100);
  4. rs = stmt.executeQuery("SELECT * FROM your_table_here");
 

此时,虽然避免了 由于mysql数据库本身的 resultset 过大而导致的 oom ,但是,依然需要按照本文一开始的代码那样,不要一次性生成百万或者千万java对象,而是要及时将内存落地为写磁盘,避免超大java对象成的 oom 。

 

JdbcTemplate-API大全参考

 

SpringBoot高级篇JdbcTemplate之数据查询上篇 讲了如何使用JdbcTemplate进行简单的查询操作,主要介绍了三种方法的调用姿势 queryForMapqueryForListqueryForObject 本篇则继续介绍剩下的两种方法使用说明

  • queryForRowSet
  • query

I. 环境准备

环境依然借助前面一篇的配置,链接如: 190407-SpringBoot高级篇JdbcTemplate之数据插入使用姿势详解

或者直接查看项目源码: https://github.com/liuyueyi/spring-boot-demo/blob/master/spring-boot/101-jdbctemplate

我们查询所用数据,正是前面一篇插入的结果,如下图

datadata

II. 查询使用说明

1. queryForRowSet

查询上篇中介绍的三种方法,返回的记录对应的结构要么是map,要么是通过RowMapper进行结果封装;而queryForRowSet方法的调用,返回的则是SqlRowSet对象,这是一个集合,也就是说,可以查询多条记录

使用姿势也比较简单,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void queryForRowSet() {
    String sql = "select * from money where id > 1 limit 2";
    SqlRowSet result = jdbcTemplate.queryForRowSet(sql);
    while (result.next()) {
        MoneyPO moneyPO = new MoneyPO();
        moneyPO.setId(result.getInt("id"));
        moneyPO.setName(result.getString("name"));
        moneyPO.setMoney(result.getInt("money"));
        moneyPO.setDeleted(result.getBoolean("is_deleted"));
        moneyPO.setCreated(result.getDate("create_at").getTime());
        moneyPO.setUpdated(result.getDate("update_at").getTime());

        System.out.println("QueryForRowSet by DirectSql: " + moneyPO);
    }
}

对于使用姿势而言与之前的区别不大,还有一种就是sql也支持使用占位方式,如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 采用占位符方式查询
sql = "select * from money where id > ? limit ?";
result = jdbcTemplate.queryForRowSet(sql, 1, 2);
while (result.next()) {
    MoneyPO moneyPO = new MoneyPO();
    moneyPO.setId(result.getInt("id"));
    moneyPO.setName(result.getString("name"));
    moneyPO.setMoney(result.getInt("money"));
    moneyPO.setDeleted(result.getBoolean("is_deleted"));
    moneyPO.setCreated(result.getDate("create_at").getTime());
    moneyPO.setUpdated(result.getDate("update_at").getTime());

    System.out.println("QueryForRowSet by ? sql: " + moneyPO);
}

重点关注下结果的处理,需要通过迭代器的方式进行数据遍历,获取每一列记录的值的方式和前面一样,可以通过序号的方式获取(序号从1开始),也可以通过制定列名方式(db列名)

2. query

对于query方法的使用,从不同的结果处理方式来看,划分了四种,下面逐一说明

a. 回调方式 queryByCallBack

这种回调方式,query方法不返回结果,但是需要传入一个回调对象,查询到结果之后,会自动调用

1
2
3
4
5
6
7
8
9
10
11
private void queryByCallBack() {
    String sql = "select * from money where id > 1 limit 2";
    // 这个是回调方式,不返回结果;一条记录回调一次
    jdbcTemplate.query(sql, new RowCallbackHandler() {
        @Override
        public void processRow(ResultSet rs) throws SQLException {
            MoneyPO moneyPO = result2po(rs);
            System.out.println("queryByCallBack: " + moneyPO);
        }
    });
}

上面的实例代码中,可以看到回调方法中传入一个ResultSet对象,简单封装一个转换为PO的方法

1
2
3
4
5
6
7
8
9
10
private MoneyPO result2po(ResultSet result) throws SQLException {
    MoneyPO moneyPO = new MoneyPO();
    moneyPO.setId(result.getInt("id"));
    moneyPO.setName(result.getString("name"));
    moneyPO.setMoney(result.getInt("money"));
    moneyPO.setDeleted(result.getBoolean("is_deleted"));
    moneyPO.setCreated(result.getDate("create_at").getTime());
    moneyPO.setUpdated(result.getDate("update_at").getTime());
    return moneyPO;
}

在后面的测试中,会看到上面会输出两行数据,也就是说

返回结果中每一条记录都执行一次上面的回调方法,即返回n条数据,上面回调执行n次

b. 结果批量处理 ResultSetExtractor

前面回调方式主要针对的是不关系返回结果,这里的则是将返回的结果,封装成我们预期的对象,然后返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void queryByResultSet() {
    String sql = "select * from money where id > 1 limit 2";
    // extractData 接收的是批量的结果,因此可以理解为一次对所有的结果进行转换,可以和 RowMapper 方式进行对比
    List<MoneyPO> result = jdbcTemplate.query(sql, new ResultSetExtractor<List<MoneyPO>>() {
        @Override
        public List<MoneyPO> extractData(ResultSet rs) throws SQLException, DataAccessException {
            List<MoneyPO> list = new ArrayList<>();
            while (rs.next()) {
                list.add(result2po(rs));
            }
            return list;
        }
    });

    System.out.println("queryByResultSet: " + result);
}

额外注意下上面你的使用,如果返回的是多条数据,注意泛型参数类型为List<?>, 简单来说这是一个对结果进行批量转换的使用场景

因此在上面的extractData方法调用时,传入的是多条数据,需要自己进行迭代遍历,而不能像第一种那样使用

c. 结果单行处理 RowMapper

既然前面有批量处理,那当然也就有单行的转换方式了,如下

1
2
3
4
5
6
7
8
9
10
11
private void queryByRowMapper() {
    String sql = "select * from money where id > 1 limit 2";
    // 如果返回的是多条数据,会逐一的调用 mapRow方法,因此可以理解为单个记录的转换
    List<MoneyPO> result = jdbcTemplate.query(sql, new RowMapper<MoneyPO>() {
        @Override
        public MoneyPO mapRow(ResultSet rs, int rowNum) throws SQLException {
            return result2po(rs);
        }
    });
    System.out.println("queryByRowMapper: " + result);
}

在实际使用中,只需要记住RowMapper方式传入的是单条记录,n次调用;而ResultSetExtractor方式传入的全部的记录,1次调用

d. 占位sql

前面介绍的几种都是直接写sql,这当然不是推荐的写法,更常见的是占位sql,通过传参替换,这类的使用前一篇博文介绍得比较多了,这里给出一个简单的演示

1
2
3
4
5
6
7
8
9
10
11
private void queryByPlaceHolder() {
    String sql = "select * from money where id > ? limit ?";
    // 占位方式,在最后面加上实际的sql参数,第二个参数也可以换成 ResultSetExtractor
    List<MoneyPO> result = jdbcTemplate.query(sql, new RowMapper<MoneyPO>() {
        @Override
        public MoneyPO mapRow(ResultSet rs, int rowNum) throws SQLException {
            return result2po(rs);
        }
    }, 1, 2);
    System.out.println("queryByPlaceHolder: " + result);
}

e. PreparedStatement 方式

在插入记录的时候,PreparedStatement这个我们用得很多,特别是在要求返回主键id时,离不开它了, 在实际的查询中,也是可以这么用的,特别是在使用PreparedStatementCreator,我们可以设置查询的db连接参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private void queryByPreparedStatement() {
    // 使用 PreparedStatementCreator查询,主要是可以设置连接相关参数, 如设置为只读
    List<MoneyPO> result = jdbcTemplate.query(new PreparedStatementCreator() {
        @Override
        public PreparedStatement createPreparedStatement(Connection con) throws SQLException {
            con.setReadOnly(true);
            PreparedStatement statement = con.prepareStatement("select * from money where id > ? limit ?");
            // 表示 id > 1
            statement.setInt(1, 1);
            // 表示 limit 2
            statement.setInt(2, 2);
            return statement;
        }
    }, new RowMapper<MoneyPO>() {
        @Override
        public MoneyPO mapRow(ResultSet rs, int rowNum) throws SQLException {
            return result2po(rs);
        }
    });

    System.out.println("queryByPreparedStatement: " + result);
}

上面是一个典型的使用case,当然在实际使用JdbcTemplate时,基本不这么玩

f. 查不到数据场景

前面一篇查询中,在单个查询中如果没有结果命中sql,会抛出异常,那么这里呢?

1
2
3
4
5
6
7
8
9
10
11
12
private void queryNoRecord() {
    // 没有命中的情况下,会怎样
    List<MoneyPO> result = jdbcTemplate
            .query("select * from money where id > ? limit ?", new Object[]{100, 2}, new RowMapper<MoneyPO>() {
                @Override
                public MoneyPO mapRow(ResultSet rs, int rowNum) throws SQLException {
                    return result2po(rs);
                }
            });

    System.out.println("queryNoRecord: " + result);
}

从后面的输出结果会看出,没有记录命中时,并没有什么关系,上面会返回一个空集合

III. 测试&小结

1. 测试

接下来测试下上面的输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.git.hui.boot.jdbc;

import com.git.hui.boot.jdbc.insert.InsertService;
import com.git.hui.boot.jdbc.query.QueryService;
import com.git.hui.boot.jdbc.query.QueryServiceV2;
import com.git.hui.boot.jdbc.update.UpdateService;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * Created by @author yihui in 11:04 19/4/4.
 */
@SpringBootApplication
public class Application {
    private QueryServiceV2 queryServiceV2;

    public Application(QueryServiceV2 queryServiceV2) {
        this.queryServiceV2 = queryServiceV2;
        queryTest2();
    }

    public void queryTest2() {
        // 第三个调用
        queryServiceV2.queryForRowSet();
        queryServiceV2.query();
    }

    public static void main(String[] args) {
        SpringApplication.run(Application.class);
    }
}

上面执行输出结果如下

test outputtest output

2. 小结

本文主要介绍了另外两种查询姿势, queryForRowSet 与 query

queryForRowSet

  • 返回SqlRowSet对象,需要遍历获取所有的结果

query

  • 提供三种结果处理方式
    • 不返回结果的回调姿势
    • 对结果批量处理的方式 ResultSetExtractor
    • 对结果单个迭代处理方式 RowMapper
  • 可以返回>=0条数据
  • 如果需要对查询的连接参数进行设置,使用PreparedStatementCreator来创建PreparedStatement方式处理

IV. 其他

相关博文

0. 项目

前面一篇介绍如何使用JdbcTemplate实现插入数据,接下来进入实际业务中,最常见的查询篇。由于查询的姿势实在太多,对内容进行了拆分,本篇主要介绍几个基本的使用姿势

  • queryForMap
  • queryForList
  • queryForObject

I. 环境准备

环境依然借助前面一篇的配置,链接如: 190407-SpringBoot高级篇JdbcTemplate之数据插入使用姿势详解

或者直接查看项目源码: https://github.com/liuyueyi/spring-boot-demo/blob/master/spring-boot/101-jdbctemplate

我们查询所用数据,正是前面一篇插入的结果,如下图

db mysqldb mysql

II. 查询使用说明

1. queryForMap

queryForMap,一般用于查询单条数据,然后将db中查询的字段,填充到map中,key为列名,value为值

a. 基本使用姿势

最基本的使用姿势,就是直接写完整的sql,执行

1
2
3
String sql = "select * from money where id=1";
Map<String, Object> map = jdbcTemplate.queryForMap(sql);
System.out.println("QueryForMap by direct sql ans: " + map);

这种用法的好处是简单,直观;但是有个非常致命的缺点,如果你提供了一个接口为

1
2
3
4
public Map<String, Object> query(String condition) {
  String sql = "select * from money where name=" + condition;
  return jdbcTemplate.queryForMap(sql);
}

直接看上面代码,会发现问题么???

有经验的小伙伴,可能一下子就发现了sql注入的问题,如果传入的参数是 '一灰灰blog' or 1=1 order by id desc limit 1, 这样输出和我们预期的一致么?

b. 占位符替换

正是因为直接拼sql,可能到只sql注入的问题,所以更推荐的写法是通过占位符 + 传参的方式

1
2
3
4
5
6
7
8
9
// 使用占位符替换方式查询
sql = "select * from money where id=?";
map = jdbcTemplate.queryForMap(sql, new Object[]{1});
System.out.println("QueryForMap by ? ans: " + map);

// 指定传参类型, 通过传参来填充sql中的占位
sql = "select * from money where id =?";
map = jdbcTemplate.queryForMap(sql, 1);
System.out.println("QueryForMap by ? ans: " + map);

从上面的例子中也可以看出,占位符的使用很简单,用问好(?)来代替具体的取值,然后传参

传参有两种姿势,一个是传入Object[]数组;另外一个是借助java的不定长参数方式进行传参;两个的占位替换都是根据顺序来的,也就是如果你有一个值想替换多个占位符,那就得血多次

如:

1
2
sql = "select * from money where (name=? and id=?) or (name=? and id=?)";
map = jdbcTemplate.queryForMap(sql, "一灰灰blog", 1, "一灰灰blog", 2);

c. 查不到的case

使用queryForMap有个不得不注意的事项,就是如果查不到数据时,会抛一个异常出来,所以需要针对这种场景进行额外处理

1
2
3
4
5
6
7
8
// 查不到数据的情况
try {
    sql = "select * from money where id =?";
    map = jdbcTemplate.queryForMap(sql, 100);
    System.out.println("QueryForMap by ? ans: " + map);
} catch (EmptyResultDataAccessException e) {
    e.printStackTrace();
}

查询不到异常查询不到异常

2. queryForList

前面针对的主要是单个查询,如果有多个查询的场景,可能就需要用到queryForList了,它的使用姿势和上面其实差别不大;

a. 基本使用姿势

最基本的使用姿势当然是直接写sql执行了

1
2
3
4
5
6
7
System.out.println("============ query for List! ==============");
String sql =
        "select id, `name`, money, is_deleted as isDeleted, unix_timestamp(create_at) as created, unix_timestamp(update_at) as updated from money limit 3;";

// 默认返回 List<Map<String, Object>> 类型数据,如果一条数据都没有,则返回一个空的集合
List<Map<String, Object>> res = jdbcTemplate.queryForList(sql);
System.out.println("basicQueryForList: " + res);

注意返回的结果是List<Map<String, Object>>, 如果一条都没有命中,会返回一个空集合, 和 QueryForMap 抛异常是不一样的

b. 占位符替换

直接使用sql的查询方式,依然和前面一样,可能有注入问题,当然优先推荐的使用通过占位来传参方式

1
2
3
4
String sql2 = "select id, `name`, money, is_deleted as isDeleted, unix_timestamp(create_at) as created, " +
        "unix_timestamp(update_at) as updated from money where id=? or name=?;";
res = jdbcTemplate.queryForList(sql2, 2, "一灰灰2");
System.out.println("queryForList by template: " + res);

3. queryForObject

如果是简单查询,直接用上面两个也就够了,但是对于使用过mybatis,Hibernate的同学来说,每次返回Map<String, Object>,就真的有点蛋疼了, 对于mysql这种数据库,表的结构基本不变,完全可以和POJO进行关联,对于业务开发者而言,当然是操作具体的POJO比Map要简单直观多了

下面将介绍下,如何使用 queryForObject 来达到我们的目标

a. 原始使用姿势

首先介绍下利用 RowMapper 来演示下,最原始的使用姿势

第一步是定义对应的POJO类

1
2
3
4
5
6
7
8
9
10
@Data
public static class MoneyPO implements Serializable {
    private static final long serialVersionUID = -5423883314375017670L;
    private Integer id;
    private String name;
    private Integer money;
    private boolean isDeleted;
    private Long created;
    private Long updated;
}

然后就是使用姿势

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// sql + 指定返回类型方式访问
// 使用这种sql的有点就是方便使用反射方式,实现PO的赋值
String sql =
        "select id, `name`, money, is_deleted as isDeleted, unix_timestamp(create_at) as created, unix_timestamp(update_at) as updated from money limit 1;";
// 需要注意,下标以1开始
MoneyPO moneyPO = jdbcTemplate.queryForObject(sql, new RowMapper<MoneyPO>() {
    @Override
    public MoneyPO mapRow(ResultSet rs, int rowNum) throws SQLException {
        MoneyPO po = new MoneyPO();
        po.setId(rs.getInt(1));
        po.setName(rs.getString(2));
        po.setMoney(rs.getInt(3));
        po.setDeleted(rs.getBoolean(4));
        po.setCreated(rs.getLong(5));
        po.setUpdated(rs.getLong(6));
        return po;
    }
});
System.out.println("queryFroObject by RowMapper: " + moneyPO);

从使用姿势上看,RowMapper 就是一个sql执行之后的回调,实现结果封装,这里需要注意的就是 ResultSet 封装了完整的返回结果,可以通过下标方式指定,下标是从1开始,而不是我们常见的0,需要额外注意

这个下标从1开始,感觉有点蛋疼,总容易记错,所以更推荐的方法是直接通过列名获取数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 直接使用columnName来获取对应的值,这里就可以考虑使用反射方式来赋值,减少getter/setter
moneyPO = jdbcTemplate.queryForObject(sql, new RowMapper<MoneyPO>() {
    @Override
    public MoneyPO mapRow(ResultSet rs, int rowNum) throws SQLException {
        MoneyPO po = new MoneyPO();
        po.setId(rs.getInt("id"));
        po.setName(rs.getString("name"));
        po.setMoney(rs.getInt("money"));
        po.setDeleted(rs.getBoolean("isDeleted"));
        po.setCreated(rs.getLong("created"));
        po.setUpdated(rs.getLong("updated"));
        return po;
    }
});
System.out.println("queryFroObject by RowMapper: " + moneyPO);

b. 高级使用

当sql返回的列名和POJO的属性名可以完全匹配上的话,上面的这种写法就显得非常冗余和麻烦了,我需要更优雅简洁的使用姿势,最好就是直接传入POJO类型,自动实现转换

如果希望得到这个效果,你需要的就是下面这个了: BeanPropertyRowMapper

1
2
3
// 更简单的方式,直接通过BeanPropertyRowMapper来实现属性的赋值,前提是sql返回的列名能正确匹配
moneyPO = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(MoneyPO.class));
System.out.println("queryForObject by BeanPropertyRowMapper: " + moneyPO);

c. 易错使用姿势

查看JdbcTemplate提供的接口时,可以看到下面这个接口

1
2
3
4
@Override
public <T> T queryForObject(String sql, Class<T> requiredType, @Nullable Object... args) throws DataAccessException {
  return queryForObject(sql, args, getSingleColumnRowMapper(requiredType));
}

自然而然的想到,直接传入POJO的类型进去,是不是就可以得到我们预期的结果了?

1
2
3
4
5
6
7
8
String sql =
                "select id, `name`, money, is_deleted as isDeleted, unix_timestamp(create_at) as created, unix_timestamp(update_at) as updated from money limit 1;";
try {
    MoneyPO po = jdbcTemplate.queryForObject(sql, MoneyPO.class);
    System.out.println("queryForObject by requireType return: " + po);
} catch (Exception e) {
    e.printStackTrace();
}

执行上面的代码,抛出异常

从上面的源码也可以看到,上面的使用姿势,适用于sql只返回一列数据的场景,即下面的case

1
2
3
4
5
// 下面开始测试下 org.springframework.jdbc.core.JdbcTemplate.queryForObject(java.lang.String, java.lang.Class<T>, java.lang.Object...)
// 根据测试,这个类型,只能是基本类型
String sql2 = "select id from money where id=?";
Integer res = jdbcTemplate.queryForObject(sql2, Integer.class, 1);
System.out.println("queryForObject by requireId return: " + res);

showshow

4. 测试

上面所有代码可以查看: https://github.com/liuyueyi/spring-boot-demo/blob/master/spring-boot/101-jdbctemplate/src/main/java/com/git/hui/boot/jdbc/query/QueryService.java

简单的继承调用下上面的所有方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@SpringBootApplication
public class Application {
    private QueryService queryService;

    public Application(QueryService queryService) {
        this.queryService = queryService;

        queryTest();
    }
    public void queryTest() {
        queryService.queryForMap();
        queryService.queryForObject();
        queryService.queryForList();
    }

    public static void main(String[] args) {
        SpringApplication.run(Application.class);
    }
}

输出结果如下

resultresult

III. 小结

本篇博文主要介绍了JdbcTemplate查询的简单使用姿势,主要是queryForMapqueryForListqueryForObject三种方法的调用

1. 根据返回结果数量

单条记录查询

  • queryForMap : 返回一条记录,返回的结果塞入Map<String, Object>, key为固定的String对应查询的列名;value为实际值
  • queryForObject :同样返回一条数据,与上面的区别在于可以借助RowMapper来实现返回结果转换为对应的POJO

需要注意的是,上面的查询,必须有一条记录返回,如果查不到,则抛异常

批量查询

  • queryForList :一次查询>=0条数据,返回类型为 List<Map<String, Object>>

2. 根据sql类型

有两种sql传参方式

  • 一个是写完整的sql语句,就和我们普通的sql查询一样;问题是存在注入的风险
  • 其次是使用占位符(?), 实际的值通过参数方式传入

IV. 其他

0. 项目

【DB系列】JdbcTemplate之数据更新与删除

前面介绍了JdbcTemplate的插入数据和查询数据,占用CURD中的两项,本文则将主要介绍数据更新和删除。从基本使用上来看,姿势和前面的没啥两样

I. 环境准备

环境依然借助前面一篇的配置,链接如: 190407-SpringBoot高级篇JdbcTemplate之数据插入使用姿势详解

或者直接查看项目源码: https://github.com/liuyueyi/spring-boot-demo/blob/master/spring-boot/101-jdbctemplate

我们查询所用数据,正是前面一篇插入的结果,如下图

datadata

II. 更新使用说明

对于数据更新,这里会分为两种进行说明,单个和批量;这个单个并不是指只能一条记录,主要针对的是sql的数量而言

1. update 方式

看过第一篇数据插入的童鞋,应该也能发现,新增数据也是用的这个方法,下面会介绍三种不同的使用姿势

先提供一个数据查询的转换方法,用于对比数据更新前后的结果

1
2
3
4
5
6
private MoneyPO queryById(int id) {
    return jdbcTemplate.queryForObject(
            "select id, `name`, money, is_deleted as isDeleted, unix_timestamp(create_at) as " +
                    "created, unix_timestamp(update_at) as updated from money where id=?",
            new BeanPropertyRowMapper<>(MoneyPO.class), id);
}

a. 纯sql更新

这个属于最基本的方式了,前面几篇博文中大量使用了,传入一条完整的sql,执行即可

1
2
3
4
5
6
 int id = 10;

// 最基本的sql更新
String sql = "update money set money=money + 999 where id =" + id;
int ans = jdbcTemplate.update(sql);
System.out.println("basic update: " + ans + " | db: " + queryById(id));

b. 占位sql

问好占位,实际内容通过参数传递方式

1
2
3
4
// 占位方式
sql = "update money set money=money + ? where id = ?";
ans = jdbcTemplate.update(sql, 888, id);
System.out.println("placeholder update: " + ans + " | db: " + queryById(id));

c. statement

从前面的几篇文章中可以看出,使用statement的方式,最大的好处有几点

  • 可以点对点的设置填充参数
  • PreparedStatementCreator 方式可以获取db连接,主动设置各种参数

下面给出两个常见的使用方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 通过 PreparedStatementCreator 方式更新
ans = jdbcTemplate.update(new PreparedStatementCreator() {
    @Override
    public PreparedStatement createPreparedStatement(Connection connection) throws SQLException {
        // 设置自动提交,设置100ms的超时,这种方式最大的好处是可以控制db连接的参数
        try {
            connection.setAutoCommit(true);
            connection.setNetworkTimeout(Executors.newSingleThreadExecutor(), 10);
            PreparedStatement statement =
                    connection.prepareStatement("update money set money=money + ? where id " + "= ?");
            statement.setInt(1, 777);
            statement.setInt(2, id);
            return statement;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
});
System.out.println("statementCreator update: " + ans + " | db: " + queryById(id));


// 通过 PreparedStatementSetter 来设置占位参数值
ans = jdbcTemplate.update(sql, new PreparedStatementSetter() {
    @Override
    public void setValues(PreparedStatement preparedStatement) throws SQLException {
        preparedStatement.setInt(1, 666);
        preparedStatement.setInt(2, id);
    }
});
System.out.println("statementSetter update: " + ans + " | db: " + queryById(id));

注意下第一种调用中,设置了超时时间,下面给出一个动图,演示超时的使用姿势

showshow

在上图中,

  • 首先是一个开启一个事物,并修改了一条记录,这个时候这条记录会加上写锁
  • 然后JdbcTemplate中修改上面的这条记录,尝试加写锁,但是会失败,所以一直阻塞,当超时之后,抛出异常

2. batchUpdate 方式

批量方式,执行多个sql,从使用上看和前面没有太大的区别,先给出一个查询的通用方法

1
2
3
4
5
6
7
8
9
private List<MoneyPO> queryByIds(List<Integer> ids) {
    StringBuilder strIds = new StringBuilder();
    for (Integer id : ids) {
        strIds.append(id).append(",");
    }
    return jdbcTemplate.query("select id, `name`, money, is_deleted as isDeleted, unix_timestamp(create_at) as " +
            "created, unix_timestamp(update_at) as updated from money where id in (" +
            strIds.substring(0, strIds.length() - 1) + ")", new BeanPropertyRowMapper<>(MoneyPO.class));
}

a. 纯sql更新

1
2
3
4
5
6
// 批量修改,
// 执行多条sql的场景
int[] ans = jdbcTemplate
        .batchUpdate("update money set money=1300 where id =10", "update money set money=1300 where id = 11");
System.out.println(
        "batch update by sql ans: " + Arrays.asList(ans) + " | db: " + queryByIds(Arrays.asList(10, 11)));

b. 占位sql

1
2
3
4
5
// 占位替换方式
ans = jdbcTemplate.batchUpdate("update money set money=money + ? where id = ?",
        Arrays.asList(new Object[]{99, 10}, new Object[]{99, 11}));
System.out.println("batch update by placeHolder ans: " + Arrays.asList(ans) + " | db: " +
        queryByIds(Arrays.asList(10, 11)));

c. statement

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 通过 statement
ans = jdbcTemplate
        .batchUpdate("update money set money=money + ? where id = ?", new BatchPreparedStatementSetter() {
            @Override
            public void setValues(PreparedStatement preparedStatement, int i) throws SQLException {
                preparedStatement.setInt(1, 99);
                preparedStatement.setInt(2, i + 10);
            }

            @Override
            public int getBatchSize() {
                return 2;
            }
        });
System.out.println(
        "batch update by statement ans: " + Arrays.asList(ans) + " | db: " + queryByIds(Arrays.asList(10, 11)));

注意下上面的方法中,getBatchSize返回实际的sql条数,setValues中的i从0开始

3. 测试

原始数据中,money都是300,通过一系列的修改,输出如下

test resulttest result

III. 数据删除

删除的操作姿势和上面基本一样,也就是sql的写法不同罢了,因此没有太大的必要重新写一篇,下面给出一个简单的demo

1
2
3
4
5
6
7
8
9
10
@Component
public class DeleteService {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    public void delete() {
        int ans = jdbcTemplate.update("delete from money where id = 13");
        System.out.println("delete: " + ans);
    }
}

IV. 其他

相关博文

0. 项目

【DB系列】JdbcTemplate之数据插入使用姿势详解

db操作可以说是java后端的必备技能了,实际项目中,直接使用JdbcTemplate的机会并不多,大多是mybatis,hibernate,jpa或者是jooq,然后前几天写一个项目,因为db操作非常简单,就直接使用JdbcTemplate,然而悲催的发现,对他的操作并没有预期中的那么顺畅,所以有必要好好的学一下JdbcTemplate的CURD;本文为第一篇,插入数据

I. 环境

1. 配置相关

使用SpringBoot进行db操作引入几个依赖,就可以愉快的玩耍了,这里的db使用mysql,对应的pom依赖如

1
2
3
4
5
6
7
8
9
10
<dependencies>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
</dependencies>

接着就是db的配置信息,下面是连接我本机的数据库配置

1
2
3
4
5
## DataSource
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false
spring.datasource.driver-class-name= com.mysql.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=

2. 测试db

创建一个测试db

1
2
3
4
5
6
7
8
9
10
CREATE TABLE `money` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(20) NOT NULL DEFAULT '' COMMENT '用户名',
  `money` int(26) NOT NULL DEFAULT '0' COMMENT '钱',
  `is_deleted` tinyint(1) NOT NULL DEFAULT '0',
  `create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

II. 使用姿势

直接引入jdbcTemplate,注入即可,不需要其他的操作

1
2
@Autowired
private JdbcTemplate jdbcTemplate;

1. sql直接插入一条数据

直接写完整的插入sql,这种方式比较简单粗暴

1
2
3
4
5
private boolean insertBySql() {
    // 简单的sql执行
    String sql = "INSERT INTO `money` (`name`, `money`, `is_deleted`) VALUES ('一灰灰blog', 100, 0);";
    return jdbcTemplate.update(sql) > 0;
}

2. 参数替换方式插入

这种插入方式中,sql使用占位符?,然后插入值通过参数传入即可

1
2
3
4
private boolean insertBySqlParams() {
    String sql = "INSERT INTO `money` (`name`, `money`, `is_deleted`) VALUES (?, ?, ?);";
    return jdbcTemplate.update(sql, "一灰灰2", 200, 0) > 0;
}

3. 通过Statement方式插入

通过Statement可以指定参数类型,这种插入方式更加安全,有两种常见的方式,注意设置参数时,起始值为1,而不是通常说的0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private boolean insertByStatement() {
    String sql = "INSERT INTO `money` (`name`, `money`, `is_deleted`) VALUES (?, ?, ?);";
    return jdbcTemplate.update(sql, new PreparedStatementSetter() {
        @Override
        public void setValues(PreparedStatement preparedStatement) throws SQLException {
            preparedStatement.setString(1, "一灰灰3");
            preparedStatement.setInt(2, 300);
            byte b = 0;
            preparedStatement.setByte(3, b);
        }
    }) > 0;
}

private boolean insertByStatement2() {
    String sql = "INSERT INTO `money` (`name`, `money`, `is_deleted`) VALUES (?, ?, ?);";
    return jdbcTemplate.update(new PreparedStatementCreator() {
        @Override
        public PreparedStatement createPreparedStatement(Connection connection) throws SQLException {
            PreparedStatement preparedStatement = connection.prepareStatement(sql);
            preparedStatement.setString(1, "一灰灰4");
            preparedStatement.setInt(2, 400);
            byte b = 0;
            preparedStatement.setByte(3, b);
            return preparedStatement;
        }
    }) > 0;
}

4. 插入并返回主键id

这个属于比较常见的需求了,我希望获取插入数据的主键id,用于后续的业务使用; 这时就需要用KeyHolder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
 * 新增数据,并返回主键id
 *
 * @return
 */
private int insertAndReturnId() {
    String sql = "INSERT INTO `money` (`name`, `money`, `is_deleted`) VALUES (?, ?, ?);";
    KeyHolder keyHolder = new GeneratedKeyHolder();
    jdbcTemplate.update(new PreparedStatementCreator() {
        @Override
        public PreparedStatement createPreparedStatement(Connection connection) throws SQLException {
            // 指定主键
            PreparedStatement preparedStatement = connection.prepareStatement(sql, new String[]{"id"});
            preparedStatement.setString(1, "一灰灰5");
            preparedStatement.setInt(2, 500);
            byte b = 0;
            preparedStatement.setByte(3, b);
            return preparedStatement;
        }
    }, keyHolder);
    return keyHolder.getKey().intValue();
}

看上面的实现,和前面差不多,但是有一行需要额外注意, 在获取Statement时,需要制定主键,否则会报错

1
2
// 指定主键
PreparedStatement preparedStatement = connection.prepareStatement(sql, new String[]{"id"});

5. 批量插入

基本插入看完之后,再看批量插入,会发现和前面的姿势没有太大的区别,无非是传入一个数组罢了,如下面的几种使用姿势

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
private void batchInsertBySql() {
    String sql = "INSERT INTO `money` (`name`, `money`, `is_deleted`) VALUES " +
            "('Batch 一灰灰blog', 100, 0), ('Batch 一灰灰blog 2', 100, 0);";
    int[] ans = jdbcTemplate.batchUpdate(sql);
    System.out.println("batch insert by sql: " + JSON.toJSONString(ans));
}

private void batchInsertByParams() {
    String sql = "INSERT INTO `money` (`name`, `money`, `is_deleted`) VALUES (?, ?, ?);";

    Object[] param1 = new Object[]{"Batch 一灰灰 3", 200, 0};
    Object[] param2 = new Object[]{"Batch 一灰灰 4", 200, 0};
    int[] ans = jdbcTemplate.batchUpdate(sql, Arrays.asList(param1, param2));
    System.out.println("batch insert by params: " + JSON.toJSONString(ans));
}

private void batchInsertByStatement() {
    String sql = "INSERT INTO `money` (`name`, `money`, `is_deleted`) VALUES (?, ?, ?);";

    int[] ans = jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
        @Override
        public void setValues(PreparedStatement preparedStatement, int i) throws SQLException {
            if (i == 0) {
                preparedStatement.setString(1, "batch 一灰灰5");
            } else {
                preparedStatement.setString(1, "batch 一灰灰6");
            }
            preparedStatement.setInt(2, 300);
            byte b = 0;
            preparedStatement.setByte(3, b);
        }

        @Override
        public int getBatchSize() {
            return 2;
        }
    });
    System.out.println("batch insert by statement: " + JSON.toJSONString(ans));
}

6. 测试

接下来我们测试下上面的代码执行情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@Component
public class InsertService {
    /**
     * 简单的新增一条数据
     */
    public void basicInsert() {
        System.out.println("basic insert: " + insertBySql());
        System.out.println("insertBySqlParams: " + insertBySqlParams());
        System.out.println("insertByStatement: " + insertByStatement());
        System.out.println("insertByStatement2: " + insertByStatement2());
        System.out.println("insertAndReturn: " + insertAndReturnId());

        List<Map<String, Object>> result = jdbcTemplate.queryForList("select * from money");
        System.out.println("after insert, the records:\n" + result);
    }
    
    /**
     * 批量插入数据
     */
    public void batchInsert() {
        batchInsertBySql();
        batchInsertByParams();
        batchInsertByStatement();
    }
}

@SpringBootApplication
public class Application {

    public Application(InsertService insertService) {
        insertService.basicInsert();
        insertService.batchInsert();
    }

    public static void main(String[] args) {
        SpringApplication.run(Application.class);
    }
}

输出结果如

1
2
3
4
5
6
7
8
9
10
basic insert: true
insertBySqlParams: true
insertByStatement: true
insertByStatement2: true
insertAndReturn: 5
after insert, the records:
[{id=1, name=一灰灰blog, money=100, is_deleted=false, create_at=2019-04-08 10:22:50.0, update_at=2019-04-08 10:22:50.0}, {id=2, name=一灰灰2, money=200, is_deleted=false, create_at=2019-04-08 10:22:55.0, update_at=2019-04-08 10:22:55.0}, {id=3, name=一灰灰3, money=300, is_deleted=false, create_at=2019-04-08 10:22:55.0, update_at=2019-04-08 10:22:55.0}, {id=4, name=一灰灰4, money=400, is_deleted=false, create_at=2019-04-08 10:22:55.0, update_at=2019-04-08 10:22:55.0}, {id=5, name=一灰灰5, money=500, is_deleted=false, create_at=2019-04-08 10:22:55.0, update_at=2019-04-08 10:22:55.0}]
batch insert by sql: [2]
batch insert by params: [1,1]
batch insert by statement: [1,1]

执行结果执行结果

II. 扩展

1. 批量插入并返回主键id

上面还漏了一个批量插入时,也需要返回主键id,改怎么办?

直接看JdbcTemplate的接口,并没有发现类似单个插入获取主键的方式,是不是意味着没法实现呢?

当然不是了,既然没有提供,我们完全可以依葫芦画瓢,自己实现一个 ExtendJdbcTemplate, 首先看先单个插入返回id的实现如

源码源码

接下来,我们自己的实现可以如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class ExtendJdbcTemplate extends JdbcTemplate {
    public ExtendJdbcTemplate(DataSource dataSource) {
        super(dataSource);
    }

    public int[] batchUpdate(final String sql, final BatchPreparedStatementSetter pss,
            final KeyHolder generatedKeyHolder) throws DataAccessException {
        return execute(new PreparedStatementCreator() {
            @Override
            public PreparedStatement createPreparedStatement(Connection conn) throws SQLException {
                return conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
            }
        }, ps -> {
            try {
                int batchSize = pss.getBatchSize();
                int totalRowsAffected = 0;
                int[] rowsAffected = new int[batchSize];
                List generatedKeys = generatedKeyHolder.getKeyList();
                generatedKeys.clear();
                ResultSet keys = null;
                for (int i = 0; i < batchSize; i++) {
                    pss.setValues(ps, i);
                    rowsAffected[i] = ps.executeUpdate();
                    totalRowsAffected += rowsAffected[i];
                    try {
                        keys = ps.getGeneratedKeys();
                        if (keys != null) {
                            RowMapper rowMapper = new ColumnMapRowMapper();
                            RowMapperResultSetExtractor rse = new RowMapperResultSetExtractor(rowMapper, 1);
                            generatedKeys.addAll(rse.extractData(keys));
                        }
                    } finally {
                        JdbcUtils.closeResultSet(keys);
                    }
                }
                if (logger.isDebugEnabled()) {
                    logger.debug("SQL batch update affected " + totalRowsAffected + " rows and returned " +
                            generatedKeys.size() + " keys");
                }
                return rowsAffected;
            } finally {
                if (pss instanceof ParameterDisposer) {
                    ((ParameterDisposer) pss).cleanupParameters();
                }
            }
        });
    }
}

封装完毕之后,我们的使用姿势可以为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Autowired
private ExtendJdbcTemplate extendJdbcTemplate;

private void batchInsertAndReturnId() {
    String sql = "INSERT INTO `money` (`name`, `money`, `is_deleted`) VALUES (?, ?, ?);";

    GeneratedKeyHolder generatedKeyHolder = new GeneratedKeyHolder();
    extendJdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
        @Override
        public void setValues(PreparedStatement preparedStatement, int i) throws SQLException {
            if (i == 0) {
                preparedStatement.setString(1, "batch 一灰灰7");
            } else {
                preparedStatement.setString(1, "batch 一灰灰8");
            }
            preparedStatement.setInt(2, 400);
            byte b = 0;
            preparedStatement.setByte(3, b);
        }

        @Override
        public int getBatchSize() {
            return 2;
        }
    }, generatedKeyHolder);

    System.out.println("batch insert and return id ");
    List<Map<String, Object>> objectMap = generatedKeyHolder.getKeyList();
    for (Map<String, Object> map : objectMap) {
        System.out.println(map.get("GENERATED_KEY"));
    }
}

然后测试执行,输出结果如下

批量插入返回id批量插入返回id

2. 小结

本篇主要介绍使用JdbcTemplate插入数据的几种常用姿势,分别从单个插入和批量插入进行了实例演示,包括以下几种常见姿势

  • update(sql)
  • update(sql, param1, param2...)
  • update(sql, new PreparedStatementCreator(){})
  • update(new PreparedStatementSetter(){})
  • update(new PreparedStatementCreator(){}, new GeneratedKeyHolder())

批量插入姿势和上面差不多,唯一需要注意的是,如果你想使用批量插入,并获取主键id,目前我没有找到可以直接使用的接口,如果有这方面的需求,可以参考下我上面的使用姿势

IV. 其他

0. 项目

posted @ 2025-01-17 09:49  CharyGao  阅读(100)  评论(0)    收藏  举报