190412-SpringBoot高级篇JdbcTemplate之数据查询上篇

文章目录
  1. I. 环境准备
  2. II. 查询使用说明
    1. 1. queryForMap
      1. a. 基本使用姿势
      2. b. 占位符替换
      3. c. 查不到的case
    2. 2. queryForList
      1. a. 基本使用姿势
      2. b. 占位符替换
    3. 3. queryForObject
      1. a. 原始使用姿势
      2. b. 高级使用
      3. c. 易错使用姿势
    4. 4. 测试
  3. III. 小结
    1. 1. 根据返回结果数量
    2. 2. 根据sql类型
  4. IV. 其他
    1. 0. 项目
    2. 1. 一灰灰Blog
    3. 2. 声明
    4. 3. 扫描关注

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

  • queryForMap
  • queryForList
  • queryForObject

I. 环境准备

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

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

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

db 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);

show

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);
}
}

输出结果如下

result

III. 小结

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

1. 根据返回结果数量

单条记录查询

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

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

批量查询

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

2. 根据sql类型

有两种sql传参方式

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

IV. 其他

0. 项目

1. 一灰灰Blog

一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

2. 声明

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

3. 扫描关注

一灰灰blog

QrCode

知识星球

goals


打赏 如果觉得我的文章对您有帮助,请随意打赏。
分享到