Spring 的 JdbcTemplate 为我们操作数据库提供非常大的便利,不需要显式的管理资源和处理异常。在我们进入到了 Java 8 后,JdbcTemplate 方法中的回调函数可以用 Lambda 表达式进行简化,而本文要说的正是这种 Lambda 简化容易给我们带来的一个 Bug, 这是我在一个实际项目中写的单元测试发现的。
下面就是我们的一个样板代码,在我们的 UserRespository
中有一个方法 findAll() 用于获得所有用户:
public List<User> findAll(){ List<User> users=new ArrayList<>(); jdbcTemplate.query("select id, name from user",rs->{ while(rs.next()){ users.add(new User(rs.getInt("id"),rs.getString("name"))); } }); return users; }
初看上面的代码,好像也没问题啊,调用 jdbcTemplate.query(sql, callback) 方法执行 SQL 语句,接着在回调函数中拿到 ResultSet 循环获得每一行结果啊。
那么我们用事实来验证,下面是相应的测试代码
@Test @Sql(statements={ "delete from user", "INSERT INTO user(id, name) VALUES(1, 'user1'), (2, 'user2'), (3,'user3'), (4, 'user4'), (5, 'user5')" }) public void findAllShouldFetchAllUsers(){ List<User> allUsers=userRepository.findAll(); allUsers.forEach(System.out::println); assertEquals(5,allUsers.size()); }
用 @Sql 往数据库中只插入 5 条记录,可是上面的断言失败了
java.lang.AssertionError:
Expected :5
Actual :4
findAll()
返回的是 4 条记录,而不是我们所期望的 5 条记录,那么还有一条记录跑哪去了。上面的 allUser.forEach(System.out::println)
打印出来的结果是:
User{id=2, name='user2'}
User{id=3, name='user3'}
User{id=4, name='user4'}
User{id=5, name='user5'}
是的,第一条记录不见了,如果我们反复针对数据库表中不同的记录数进行测试的的话,丢失的记录总是第一条。分析总是丢失第一条记录的原因肯定是有人帮我们做了一次 rs.next()
把光标跳了一下。
这是为何呢?这就是我要说的 JdbcTemplate 被 Java 8 的 Lambda 表达式带沟里去了,因为 Lambda,让我们忽略了方法原型是什么,Lambda 相对应的 @FunctionalInterface
是什么,同时 IDE 也是帮凶。因为当我们在 IDE 中写到
jdbcTemplate.query("select id, name from user", rs -> {
后,很容易仗着先前用原生 JDBC 操作 ResultSet 的惯性立即就会对 rs
变量用 whilc (rs.next) {...}
进行遍历,于是问题就发生了。
如果我们回归到从前,还是用匿名类的方式来写回调函数的时候,findAll()
相应的不正确的代码就是
public List<User> findAll(){ List<User> users=new ArrayList<>(); jdbcTemplate.query("select id, name from user",new RowCallbackHandler(){ @Override public void processRow(ResultSet rs)throws SQLException{ while(rs.next()){ users.add(new User(rs.getInt("id"),rs.getString("name"))); } } }); return users; }
现在我们明明白白的能看到回调函数的类型是 RowCallbackHandler
, 如类名所示,它就是处理 ResultSet 的当前行, 有人在帮我们遍历结果集,所以我们再次对 ResultSet 就跳过了第一行记录。
在应用 Java 8 之前的 JDK, 我们出现上面错误的概率应该很小的吧,会写成如下正确的代码
public List<User> findAll(){ List<User> users=new ArrayList<>(); jdbcTemplate.query("select id, name from user",new RowCallbackHandler(){ @Override public void processRow(ResultSet rs)throws SQLException{ users.add(new User(rs.getInt("id"),rs.getString("name"))); } }); return users; }
因此再回到我们 Java 8 用 Lambda 简化后的版本就是
public List<User> findAll(){ List<User> users=new ArrayList<>(); jdbcTemplate.query("select id, name from user",rs->{ users.add(new User(rs.getInt("id"),rs.getString("name"))); }); return users; }
这个才是正确的代码,相比于文中最开始出现的错误代码,我们做了一件吃力不讨好的事情,代码行多了反而引入了一个 Bug。
这真是被 Java 8 的 Lambda 和 IDE 惯坏了,当我们在享受 Lambda 给我们带来便利的同时,却忘记了自己是谁,方法原型是什么,以及Lambda 所代表的功能性接口是什么。
针对上面的 findAll()
方法的的意图,其实我们更应该调用
<T> List<T> query(String sql, RowMapper<T> rowMapper) throws DataAccessException;
而不是现在的
void query(String sql, RowCallbackHandler rch) throws DataAccessException;
对上面的方法再进一步简化就是
public List<User> findAll(){ return jdbcTemplate.query("select id, name from user", (rs,index)->new User(rs.getInt("id"),rs.getString("name"))); }
为何我这么衷情于 JdbcTemplate 的各个 query(...)
的重载方法呢,而不是直接调用 queryForList(...)
, 各种变体呢?因为有时候需要作流式处理,而是一下把所有结果全加载到内存中。当然这里的 findAll()
完全可以用 queryForList(...)
来简化
public List<Map<String,Object>> findAll(){ return jdbcTemplate.queryForList("select id, name from user",new BeanPropertyRowMapper<>(User.class)); }
话说到现在,我们还是有必要从 JdbcTemplate 的原代码来理解 query(String sql, RowCallbackHandler rch)
的实现原理。下面的代码来自于 JdbcTemplate 类
@Override public <T> T query(final String sql,final ResultSetExtractor<T> rse)throws DataAccessException{ Assert.notNull(sql,"SQL must not be null"); Assert.notNull(rse,"ResultSetExtractor must not be null"); if(logger.isDebugEnabled()){ logger.debug("Executing SQL query ["+sql+"]"); } class QueryStatementCallback implements StatementCallback<T>,SqlProvider{ @Override public T doInStatement(Statement stmt)throws SQLException{ ResultSet rs=null; try{ rs=stmt.executeQuery(sql); ResultSet rsToUse=rs; if(nativeJdbcExtractor!=null){ rsToUse=nativeJdbcExtractor.getNativeResultSet(rs); } return rse.extractData(rsToUse); } finally{ JdbcUtils.closeResultSet(rs); } } @Override public String getSql(){ returnsql; } } return execute(new QueryStatementCallback()); } @Override public void query(String sql,RowCallbackHandler rch)throws DataAccessException{ query(sql,new RowCallbackHandlerResultSetExtractor(rch)); } private static class RowCallbackHandlerResultSetExtractor implements ResultSetExtractor<Object>{ private final RowCallbackHandler rch; public RowCallbackHandlerResultSetExtractor(RowCallbackHandler rch){ this.rch=rch; } @Override public Object extractData(ResultSet rs)throws SQLException{ while(rs.next()){ this.rch.processRow(rs); } return null; } }
关键是类 RowCallbackHandlerResultSetExtractor
, 它在遍历结果集,针对每一行调用我们传入的回调函数,所以它至少有一次机会作 rs.next()
, 如果我们在 Lambda 也作一次 rs.next()
就成功的跳过了第一条记录。
这里还有一个要非常小心的地方,如果调用的是
<T> T query(String sql, ResultSetExtractor<T> rse)
而不是
void query(String sql, RowCallbackHandler rch)
的话,是可以在 Lambda 中进行自主 while(rs.next())
的,即下面的代码是下确的
public List<User> findAll(){ return jdbcTemplate.query("select * from user",rs->{ List<User> users=new ArrayList<>(); while(rs.next()){ users.add(new User(rs.getInt("id"),rs.getString("name"))); } return users; }); }
Lambda 的写法上与第一段代码毫无区别,唯一的不同是这个 query 方法有返回值。也就是说
- 有返回值的 JdbcTemplate.query(sql, rs -> {....}) 要自己遍历结果集
- 无反回值的 JdbcTemplate.query(sql, rs -> {....}) 不可自己遍历结果集,否则会丢失第一条记录,也就是在 Lambda 内部最后写上一句
return null
就行为大变了
Java 中是不能仅以返回值的不同来重载方法,但是转换为 Lambda 表达式制造出来的假象就是根据返回值的不同而调用了不同的方法。
何时可以 while(rs.next())
何时不可以,真是极具隐蔽性,而且出问题了还不明显,真是一个事故多发地,一不小心就会踩上地雷。
相关推荐
JDBC(Java Database Connectivity)是Java平台中用于访问数据库的标准API,但它直接使用起来繁琐且易出错。为了简化JDBC的使用,Spring框架提供了JDBCTemplate,它是一个基于模板方法设计模式的数据库访问类,能够...
JDBCTemplateDemo.java
Java JDBC (Java Database Connectivity) 是Java编程语言中用于与数据库交互的一组接口和类,它提供了标准的方法来连接、查询和操作数据库。Spring JDBC是Spring框架的一个模块,它简化了JDBC的操作,提供了更高级别...
使用Spring的JdbcTemplate实现分页功能
在Java世界中,Spring框架是应用最广泛的IoC(Inversion of Control)和AOP(Aspect Oriented Programming)容器之一。其中,`JdbcTemplate`是Spring JDBC模块的核心组件,为数据库操作提供了简单、灵活的API。这篇...
java组件开发(16)JdbcTemplate
SpringJdbcTemplate是Spring框架中用于简化Java数据库访问的工具,它是Spring JDBC模块的核心。这个封装工具类的出现是为了提供一种更简洁、易于使用的接口来执行SQL操作,减轻开发者处理数据库连接、事务管理以及...
Spring框架为简化这一过程提供了JdbcTemplate类,它基于Java Database Connectivity (JDBC) API,但通过一系列抽象和自动处理,使得数据库操作更加安全、易用且易于测试。JdbcTemplate是Spring JDBC模块的核心组件,...
在Java的Spring框架中,JdbcTemplate是一个非常重要的组件,它为数据库操作提供了简便的模板方法。在处理大量数据时,传统的分页方式可能会导致内存溢出,这时可以使用游标滚动来实现高效的分页。本篇文章将深入探讨...
Java中的JdbcTemplate是Spring框架提供的一种用于简化JDBC(Java Database Connectivity)操作的工具,它在数据持久层操作中扮演着重要角色。JdbcTemplate通过消除大量重复的JDBC样板代码,提高了代码的可读性和可...
commons-logging-1.2.jar spring-jdbc-5.1.10.RELEASE.jar spring-core-5.1.10.RELEASE.jar spring-beans-5.1.10.RELEASE.jar ...
我们将首先编写 Oracle 存储过程,然后编写 Java 代码使用 Spring JdbcTemplate 调用这些存储过程。 Oracle 存储过程 首先,我们编写了两个 Oracle 存储过程:`P_EMP_SELECT` 和 `P_EMP_ADD`。 `P_EMP_SELECT` ...
最近项目中的工作流需要查询多个数据源的数据,数据源可能是不同种类的:如sql server,oracl等等,一开始是用的配置实现,后来发现在项目运行中,可能需要动态的添加更多不同类型的数据源,所以最终的逻辑是将数据源...
《SpringMVC与JDBCTemplate结合实现在线装机系统详解》 在现代软件开发中,构建一个在线装机系统可以极大地提升效率,减少人为错误。本系统利用SpringMVC作为控制层框架,配合JDBCTemplate进行数据访问,旨在提供...
Java Spring JdbcTemplate是Spring框架中一个非常重要的组件,它为开发者提供了一种简单而有效的方式来操作数据库。在本文中,我们将深入探讨JdbcTemplate的基本概念、使用场景、优势以及如何在实际项目中进行配置和...
### Oracle + jdbcTemplate + Spring + Java + Flex 实现分页 #### 一、Oracle存储过程分页 在Oracle数据库中,为了实现高效的分页查询,通常会采用存储过程的方式来完成。这种方式能够有效地减少网络传输的数据量...
本文将深入探讨Spring JdbcTemplate的常用方法,并结合提供的`JsonBean.java`和`JdbcUtils.java`文件,来理解其在实际应用中的使用。 首先,JdbcTemplate的核心功能在于它提供了一系列的方法来执行SQL语句,包括...
基于java+Spring+SpringMVC+JDBCTemplate+JSP开发的博客论坛系统+源码+开发文档+视频演示,适合毕业设计、课程设计、项目开发。项目源码已经过严格测试,可以放心参考并在此基础上延申使用~ 基于java+Spring+...
关于使用JDBC的一些小Demo.主要是关于使用基本的JDBC进行增删改查操作,还有使用数据库连接池技术进行连接,最后写了几个关于JDBCTemplate的小Demo