美文网首页
浅谈Spring JdbcTemplate模板方法设计模式

浅谈Spring JdbcTemplate模板方法设计模式

作者: dd_123 | 来源:发表于2021-07-25 18:23 被阅读0次
  • 以下例子代码可在github或者在gitee下载
    github:代码链接
    gitee:代码链接
  • 有关模板方法设计模式,可以参看前面写的文章:模板方法模式&lambda重构模板方法模式
  • 本文主要分为四部分:(1)JDBC规范与连接MySQL(2)JdbcTemplate连接MySQL(3)分析JdbcTemplate模板方法设计模式(4)手写简单版JdbcTemplate示例模板方法设计模式
  • 示例代码可以通过启动项目调用JdbcController类originalJdbc方法(原生jdbc连接MySQL)、jdcbTemplate方法(jdbc连接MySQL)、defineJcbcTemplate方法(手写jdbcTemplate)查看结果。
  • 代码结构


    项目结构.png
一、JDBC规范与JDBC连接MySQL数据库

(1)JDBC的全称是Java Database Connectivity,设计初衷是提供一套能够应用于各种数据库的统一标准,这套标准需要不同数据库厂家之间共同遵守,并提供各自的实现方案供 JDBC应用程序调用,JDBC规范架构图如下图所示。


JDBC规范架构图.png

(2)JDBC 规范中的核心编程对象包括 DriverManger、DataSource、Connection、Statement,及 ResultSet。

  • DriverManger,负责加载各种不同的驱动程序(Driver)
  • DataSource主要是从数据库连接池中获取数据库连接对象Connection,如果没有DataSource和数据库连接池ConnectionPoolDataSource,通过DriverManger获取数据库连接对象Connection,建立连接和销毁连接的过程产生的开销较大,故需要有数据库连接池和DataSource,与线程池类似。
  • Connection数据库连接对象,可以理解为会话,代表一个数据库连接,负责完成与数据库直连的通信。
  • Statement负责执行sql语句。
  • ResultSet执行sql之后的结果。

(3)Jdbc连接MySQL数据库查询id为1的学生

  • 领域对象Student类
public class Student {

    private Long id;

    private String name;

    private Integer age;
}
  • 添加c3p0 datasource作为数据库连接池
        <dependency>
            <groupId>c3p0</groupId>
            <artifactId>c3p0</artifactId>
            <version>0.9.1.2</version>
        </dependency>
  • 连接MySQL数据库
        try {
            //获取连接
            Connection connection = dataSource.getConnection();
            //执行查询
            PreparedStatement preparedStatement = connection.prepareStatement("select * from student where id = 1");
            //获取执行结果
            ResultSet resultSet = preparedStatement.executeQuery();
            if (resultSet.next()) {
                Student student = new Student();
                student.setId(resultSet.getLong("id"));
                student.setName(resultSet.getString("name"));
                student.setAge(resultSet.getInt("age"));
                log.info("student:{}", JSONUtil.toJsonStr(student));
            }
            //关闭资源
            preparedStatement.close();
            resultSet.close();
            connection.close();
        } catch (Exception e) {
            log.error("e:", e);
        }

(4)基于 JDBC 规范进行数据库访问的整个开发流程:
创建DataSource -> 获取Connection -> 创建Statment -> 执行sql语句 -> 处理ResultSet -> 关闭资源对象。
(5)用jdbc查询数据库的过程中,主要有两个部分,一部分是需要准备和释放资源,另一部分是处理结果,对于业务来说,我们只需要处理数据库查询结果,而准备和释放资源,是多余的部分且重复的,如果用jdbc在实际开发中,每次都需要写额外的代码,故Spring框架的JdbcTemplate工具应运而生。

二、JdbcTemplate连接MySQL
  • 添加jdbc starter起步依赖
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
  • yaml文件配置MySQL数据库连接池
spring:
  datasource:
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/exercisegroup?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
  • 注入jdbcTemplate模板工具,使用jdbcTemplate进行数据库查询,queryForObject方法第二个参数是一个函数式接口,接口参数是传入jdbc查询结果ResultSet,返回处理结果,这部分交由调用者处理,我写了rowMapper处理此次查询结果。
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    public void jdcbTemplate() {
        //省去建立数据库连接与连接释放资源
        Student student = jdbcTemplate.queryForObject("select * from student where id = 1", this::rowMapper);
        log.info("student:{}", JSONUtil.toJsonStr(student));
    }

    private Student rowMapper(ResultSet resultSet, int rowNum) {
        Student student = new Student();
        try {
            student.setId(resultSet.getLong("id"));
            student.setName(resultSet.getString("name"));
            student.setAge(resultSet.getInt("age"));
            return student;
        } catch (SQLException e) {
            log.error("e:", e);
        }
        return null;
    }

使用jdbcTemplate可以看到只需写sql,然后对执行结果进行处理,省去了建立数据库连接与连接释放资源的重复操作,其中用的是模板方法设计模式,把重复的动作封装起来,需要处理的部分开放接口,让调用者去实现。

三、JdbcTemplate模板方法设计模式与回调方法
  • RowMapper函数式接口,方法入参是jdbc查询结果ResultSet,返回的是我们实现完该函数式接口的返回结果T,在jdbcTemplate中充当回调方法,最后是调用mapRow方法,以此返回处理完的结果。
@FunctionalInterface
public interface RowMapper<T> {
    @Nullable
    T mapRow(ResultSet rs, int rowNum) throws SQLException;
}
  • JdbcTemplate的queryForObject方法
    public <T> T queryForObject(String sql, RowMapper<T> rowMapper) throws DataAccessException {
        List<T> results = query(sql, rowMapper);
        return DataAccessUtils.nullableSingleResult(results);
    }
  • 继续点进queryForObject方法的query(sql, rowMapper),这里是再封装一次RowMapper到RowMapperResultSetExtractor,可以回头再看,主要是封装遍历处理完的结果,方法是extractData
    @Override
    public <T> List<T> query(String sql, RowMapper<T> rowMapper) throws DataAccessException {
        return result(query(sql, new RowMapperResultSetExtractor<>(rowMapper)));
    }
  • 继续点进query(sql, new RowMapperResultSetExtractor<>(rowMapper))),这里QueryStatementCallback实现StatementCallback接口,核心逻辑是stmt.executeQuery(sql)执行sql,rse.extractData(rs)处理结果,即上一步说到的处理结果,最后调用的是execute(new QueryStatementCallback(), true),把实现好StatementCallback接口的QueryStatementCallback类传进去,jdbcTemplate最核心的方法是execute。
    @Override
    @Nullable
    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 + "]");
        }

        /**
         * Callback to execute the query.
         */
        class QueryStatementCallback implements StatementCallback<T>, SqlProvider {
            @Override
            @Nullable
            public T doInStatement(Statement stmt) throws SQLException {
                ResultSet rs = null;
                try {
                    rs = stmt.executeQuery(sql);
                    return rse.extractData(rs);
                }
                finally {
                    JdbcUtils.closeResultSet(rs);
                }
            }
            @Override
            public String getSql() {
                return sql;
            }
        }

        return execute(new QueryStatementCallback(), true);
    }
  • 继续点进execute方法,可以看到跟步骤一用jdbc连接数据库一样,可以回顾步骤一的基于 JDBC 规范进行数据库访问的整个开发流程,也是创建DataSource,获取Connection对象连接,创建Statment,关闭资源对象,这些额外重复的操作都在execute方法封装起来了,这也是模板方法设计模式的思想,把重复的、额外的操作封装起来,定义好开放接口,交由子类实现,在execute方法中需要实现的接口方法是action.doInStatement(stmt),上一步的StatementCallback已经实现好了该接口类的doInStatement方法,其余都是封装好的,准备资源以及释放资源。
private <T> T execute(StatementCallback<T> action, boolean closeResources) throws DataAccessException {
        Assert.notNull(action, "Callback object must not be null");

        Connection con = DataSourceUtils.getConnection(obtainDataSource());
        Statement stmt = null;
        try {
            stmt = con.createStatement();
            applyStatementSettings(stmt);
            T result = action.doInStatement(stmt);
            handleWarnings(stmt);
            return result;
        }
        catch (SQLException ex) {
            // Release Connection early, to avoid potential connection pool deadlock
            // in the case when the exception translator hasn't been initialized yet.
            String sql = getSql(action);
            JdbcUtils.closeStatement(stmt);
            stmt = null;
            DataSourceUtils.releaseConnection(con, getDataSource());
            con = null;
            throw translateException("StatementCallback", sql, ex);
        }
        finally {
            if (closeResources) {
                JdbcUtils.closeStatement(stmt);
                DataSourceUtils.releaseConnection(con, getDataSource());
            }
        }
    }
四、运用模板方法手写简单版JdbcTemplate
  • DefineJcbcTemplate自定义jdbcTemplate接口
public interface DefineJcbcTemplate {

    <T> T queryForObject(String sql, DefineRowMapper<T> defineRowMapper);

}
  • DefineRowMapper函数式接口,回调作用,处理jdbc查询结果ResultSet,返回泛型T对象
@FunctionalInterface
public interface DefineRowMapper <T>{

    T mapRow(ResultSet rs) throws SQLException;
}
  • DefineJcbcTemplateImpl实现类,该实现类的连接数据库、释放资源等操作都是从步骤一负责粘贴进来的,唯一不同的是把处理结果解耦出来了,定义好处理结果的接口DefineRowMapper,交由调用者去实现对结果的处理,最后回调该接口的方法mapRow并返回结果,其他的把连接数据库、释放资源封装起来,这样一来不用每次进行数据库查询都需要连接数据库、释放资源。
@Service
@Slf4j
public class DefineJcbcTemplateImpl implements DefineJcbcTemplate {

    @Autowired
    private DataSource dataSource;

    @Override
    public <T> T queryForObject(String sql, DefineRowMapper<T> defineRowMapper) {
        //一部分是准备和释放资源以及执行 SQL 语句,另一部分则是处理 SQL 执行结果
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;
        try {
            //创建dataSource,获取连接
            connection = dataSource.getConnection();
            //执行查询
            preparedStatement = connection.prepareStatement(sql);
            //获取执行结果
            resultSet = preparedStatement.executeQuery();
            return defineRowMapper.mapRow(resultSet);
        } catch (Exception e) {
            log.error("e:", e);
        } finally {
            //关闭资源
            if (preparedStatement != null) {
                try {
                    preparedStatement.close();
                } catch (SQLException e) {
                    log.error("e:", e);
                }
            }
            if (resultSet != null) {
                try {
                    resultSet.close();
                } catch (SQLException e) {
                    log.error("e:", e);
                }
            }
            if (connection != null) {
                try {
                    connection.close();
                } catch (SQLException e) {
                    log.error("e:", e);
                }
            }
        }
        return null;
    }
}
  • 最后是调用自定义的jdbcTemplate的queryForObject查询数据库。
    @Override
    public void defineJcbcTemplate() {
        defineJcbcTemplate.queryForObject("select * from student where id = 1", this::defineRowMapper);
    }

    private Student defineRowMapper(ResultSet resultSet) {
        try {
            //ResultSet是一个结果集,想读出来,必须要next方法才行
            if (resultSet.next()) {
                Student student = new Student();
                student.setId(resultSet.getLong("id"));
                student.setName(resultSet.getString("name"));
                student.setAge(resultSet.getInt("age"));
                log.info("student:{}", JSONUtil.toJsonStr(student));
                return student;
            }
        } catch (SQLException e) {
            log.error("e:", e);
        }
        return null;
    }

相关文章

网友评论

      本文标题:浅谈Spring JdbcTemplate模板方法设计模式

      本文链接:https://www.haomeiwen.com/subject/zyzsmltx.html