zoukankan      html  css  js  c++  java
  • JDBC工具类——JdbcUtils(5)

    JDBC工具类——JdbcUtils(5)

    前言

    本系列文章介绍JDBC工具类——JdbcUtils的封装,部分实现参考了Spring框架的JdbcTemplate

    完整项目地址:https://github.com/byx2000/JdbcUtils

    回顾

    到目前为止,JDBC的查询操作已经封装得差不多了,结果集转换器和行转换器这两个抽象让查询操作使用起来非常方便,用户不仅可以直接使用预定义的转换器,还可以自定义转换器,基本能实现各种常见的查询需求。但是还是存在一些不完美的地方。

    ResultSet暴露引发的问题

    ResultSetMapper<T>接口和RowMapper<T>接口的map方法的定义中,向用户直接暴露了ResultSet参数:

    T map(ResultSet rs) throws Exception;
    

    这导致用户在实现自定义转换器的过程中,可能无意或有意地调用了ResultSet中的一些关键方法,从而导致程序崩溃。

    举个例子,假如某个用户在实现ResultSetMapper<T>的过程中,“自作聪明”地在map方法的最后调用了ResultSetclose方法:

    public class UserListResultSetMapper implements ResultSetMapper<List<User>>
    {
        @Override
        public List<User> map(ResultSet rs) throws Exception
        {
            ...
            rs.close();
            return XXX;
        }
    }
    

    这将会引发程序崩溃,因为在query函数中也会对结果集进行关闭,导致ResultSet被关闭了两次。

    另一个问题,就是用户在实现RowMapper<T> 时,可能在map方法中调用ResultSetnext函数,这会导致结果集向前移动一行:

    public class UserRowMapper implements ResultSetMapper<User>
    {
        @Override
        public User map(ResultSet rs) throws Exception
        {
            ...
            rs.next();
            ...
            return XXX;
        }
    }
    

    当然,我们可以在文档中加上说明,提醒用户实现这两个接口时的注意事项,但是并不是每个用户使用前都会仔细阅读文档。

    封装ResultSet

    为了解决这个问题,我们不能把ResultSet暴露给用户,所以需要把ResultSet封装起来。我们把封装后的结果集分离成RecordRow两个接口:

    public interface Record
    {
        Row getCurrentRow(); // 获取当前行
        boolean next(); // 移动到下一行
    }
    
    public interface Row
    {
        Object getObject(String columnLabel);
        Object getObject(int columnIndex);
        int getInt(String columnLabel);
        int getInt(int columnIndex);
        String getString(String columnLabel);
        String getString(int columnIndex);
        double getDouble(String columnLabel);
        double getDouble(int columnIndex);
    
        int getColumnCount(); // 获取列数
        String getColumnLabel(int index); // 获取列标签
    }
    

    Record表示整个结果集,它是对数据库查询操作返回结果的封装。一个结果集由若干个数据行组成。初始时,结果集的当前行指向第一行之前,调用next方法可以让当前行向前移动一行。若当前已到达最后一行,则next调用返回false,否则返回true

    Row表示数据行,封装了结果集的一行数据。数据行由若干列组成,每列都是一个值。Row中包含了一系列getXXX方法用于获取列中的值。

    Record中的getCurrentRow方法用于获取当前行。

    重构ResultSetMapper<T>RowMapper<T>

    有了RecordRow,就可以对之前的两个转换器接口做重构:

    public interface RecordMapper<T>
    {
        T map(Record record);
    }
    
    public interface RowMapper<T>
    {
        T map(Row row);
    }
    

    注意,这里把ResultSetMapper<T>重命名成了RecordMapper<T>

    此时RecordRow接口中已经没有像close一样危险的方法,所以再也不用担心用户在map方法中搞破坏了。

    实现RecordRow接口

    接下来,需要使用适配器模式ResultSet转换成RecordRow

    public class RecordAdapterForResultSet implements Record, Row
    {
        private final ResultSet rs;
    
        public RecordAdapterForResultSet(ResultSet resultSet)
        {
            this.rs = resultSet;
        }
    
        @Override
        public Object getObject(String columnLabel)
        {
            try
            {
                return rs.getObject(columnLabel);
            }
            catch (SQLException e)
            {
                throw new RuntimeException(e.getMessage(), e);
            }
        }
    
        @Override
        public Object getObject(int columnIndex)
        {
            try
            {
                return rs.getObject(columnIndex);
            }
            catch (SQLException e)
            {
                throw new RuntimeException(e.getMessage(), e);
            }
        }
    
        // 其它getXXX方法...
    
        @Override
        public int getColumnCount()
        {
            try
            {
                ResultSetMetaData metaData = rs.getMetaData();
                return metaData.getColumnCount();
            }
            catch (SQLException e)
            {
                throw new RuntimeException(e.getMessage(), e);
            }
        }
    
        @Override
        public String getColumnLabel(int index)
        {
            try
            {
                ResultSetMetaData metaData = rs.getMetaData();
                return metaData.getColumnLabel(index);
            }
            catch (SQLException e)
            {
                throw new RuntimeException(e.getMessage(), e);
            }
        }
    
        @Override
        public Row getCurrentRow()
        {
            return this;
        }
    
        @Override
        public boolean next()
        {
            try
            {
                return rs.next();
            }
            catch (SQLException e)
            {
                throw new RuntimeException(e.getMessage(), e);
            }
        }
    }
    

    RecordAdapterForResultSet类将ResultSet包装成了RecordRow,同时封装了异常处理。

    修改query方法

    query方法做出的修改如下:

    public class JdbcUtils
    {
        ...
        public static <T> T query(String sql, RecordMapper<T> recordMapper, Object... params)
        {
            ResultSet rs = null;
            PreparedStatement stmt = null;
            Connection conn = null;
            try
            {
                conn = getConnection();
                stmt = createPreparedStatement(conn, sql, params);
                rs = stmt.executeQuery();
                return recordMapper.map(new RecordAdapterForResultSet(rs));
            }
            catch (Exception e)
            {
                throw new RuntimeException(e.getMessage(), e);
            }
            finally
            {
                close(rs, stmt, conn);
            }
        }
        ...
    }
    

    其中,try块中最后一行由

    return resultSetMapper.map(rs);
    

    改成了

    return recordMapper.map(new RecordAdapterForResultSet(rs));
    

    总结

    到这里,对JDBC查询操作的封装就结束了。下一篇文章将介绍JDBC更新操作的封装。

  • 相关阅读:
    JavaScript
    关于setInterval()你所不知道的地方
    JavaScript面向对象和原型函数
    GET和POST有什么区别?及为什么网上的多数答案都是错的
    10个最常见的 HTML5 面试题及答案
    Ajax我选择这样入门
    前端应当了解的Web缓存知识
    JavaScript
    Linux rhel7 下MySQL5.7.18详细安装文档
    思科交换机配置DHCP的四个方面
  • 原文地址:https://www.cnblogs.com/baiyuxuan/p/14329620.html
Copyright © 2011-2022 走看看