JdbcTemplate 易被 Java 8 Lambda 带入的坑_JAVA_编程开发_程序员俱乐部

中国优秀的程序员网站程序员频道CXYCLUB技术地图
热搜:
更多>>
 
您所在的位置: 程序员俱乐部 > 编程开发 > JAVA > JdbcTemplate 易被 Java 8 Lambda 带入的坑

JdbcTemplate 易被 Java 8 Lambda 带入的坑

 2019/8/30 12:34:10  yuqingshui  程序员俱乐部  我要评论(0)
  • 摘要:Spring的JdbcTemplate为我们操作数据库提供非常大的便利,不需要显式的管理资源和处理异常。在我们进入到了Java8后,JdbcTemplate方法中的回调函数可以用Lambda表达式进行简化,而本文要说的正是这种Lambda简化容易给我们带来的一个Bug,这是我在一个实际项目中写的单元测试发现的。下面就是我们的一个样板代码,在我们的UserRespository中有一个方法findAll()用于获得所有用户:publicList<User>findAll()
  • 标签:Java

Spring 的 JdbcTemplate 为我们操作数据库提供非常大的便利,不需要显式的管理资源和处理异常。在我们进入到了 Java 8 后,JdbcTemplate 方法中的回调函数可以用 Lambda 表达式进行简化,而本文要说的正是这种 Lambda 简化容易给我们带来的一个 Bug, 这是我在一个实际项目中写的单元测试发现的。

下面就是我们的一个样板代码,在我们的?UserRespository?中有一个方法 findAll() 用于获得所有用户:

?

?
class="java">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 方法有返回值。也就是说

  1. 有返回值的 JdbcTemplate.query(sql, rs -> {....}) 要自己遍历结果集
  2. 无反回值的? JdbcTemplate.query(sql, rs -> {....}) 不可自己遍历结果集,否则会丢失第一条记录,也就是在 Lambda 内部最后写上一句?return null?就行为大变了

Java 中是不能仅以返回值的不同来重载方法,但是转换为 Lambda 表达式制造出来的假象就是根据返回值的不同而调用了不同的方法。

何时可以?while(rs.next())? 何时不可以,真是极具隐蔽性,而且出问题了还不明显,真是一个事故多发地,一不小心就会踩上地雷。

?

?

rel:?https://yanbin.blog/jdbctemplate-java-8-lambda-trick/

发表评论
用户名: 匿名