Spring JDBC最佳实践(1)

霸气de小男生 提交于 2019-11-30 03:21:17

原文地址:https://my.oschina.net/u/218421/blog/38513

Spring提供了两种使用JDBC API的最佳实践,一种是以JdbcTemplate为核心的基于Template的JDBC的使用方式,另一种则是在JdbcTemplate基础之上的构建的基于操作对象的JDBC的使用方式。

基于Template的JDBC的使用方式
该使用方式的最初设想和原型,需要追溯到Rod Johnson在03年出版的Expert One-on-One J2EE Design and Development,在该书的Practical Data Access(数据访问实践)中,Rod针对JDBC使用中的一些问题提出了一套改进的实践原型,并最终将该原型完善后在Spring框架中发布。

JDBC的尴尬
JDBC作为Java平台的访问关系数据库的标准,其成功是 有目共睹的。几乎所有java平台的数据访问,都直接或者间接的使用了JDBC,它是整个java平台面向关系数据库进行数据访问的基石。
作为一个标准,无疑JDBC是很成功的,但是要说JDBC在使用过程当中多么的受人欢迎,则不尽然了。JDBC主要是面向较为底层的数据库操作,所以在设计的过程当中 ,比较的贴切底层以提供尽可能多的功能特色。从这个角度来说,JDBC API的设计无可厚非。可是,过于贴切底层的API的设计,对于开发人员则未必是一件好事。即使执行一个最简单的查询,开发人员也要按照API的规矩写上一大堆雷同的代码,如果不能合理的封装使用JDBC API,在项目中使用JDBC访问数据所出现的问题估计会使人发疯!
对于通常的项目开发来说,如果层次划分很明确,数据访问逻辑一般应该在DAO层中实现。根据功能模块的划分,可能每个开发人员都会分得或多或少的实现相应的DAO的任务,假设开发人员A在分得了DAO实现任务后进行开发,他或许开发了如下所示的代码:

package com.google.spring.jdbc;

import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;

import javax.sql.DataSource;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class DaoWithA implements IDao
{

    private final Log logger = LogFactory.getLog(DaoWithA.class);
    private DataSource dataSource = null;
    
    public DataSource getDataSource()
    {
        return dataSource;
    }

    public void setDataSource(DataSource dataSource)
    {
        this.dataSource = dataSource;
    }

    @Override
    public int updateSomething(String sql)
    {
        int count;
        Connection conn = null;
        Statement stmt = null;
        try
        {
            conn = getDataSource().getConnection();
            stmt = conn.createStatement();
            count = stmt.executeUpdate(sql);
            stmt.close();
            stmt = null;
        } 
        catch (SQLException e)
        {
            throw new RuntimeException(e);
            
        }
        finally
        {
            if(stmt!=null)
            {
                try
                {
                    stmt.close();
                } 
                catch (SQLException ex)
                {
                    logger.warn("fail to close statement:"+ex);
                }
            }
            if(conn!=null)
            {
                try
                {
                    conn.close();
                } 
                catch (Exception ex)
                {
                    logger.warn("failed to close Connection:"+ex);
                }
            }
        }
        return count;
    }

}

 

而B所负责的DAO的实现中,可能也有类似的更新的操作。无疑,B也要像A这样,在他的DAO实现类中写一大堆同样的JDBC代码,类似的情况还可能扩展到C、D等开发人员。如果每个开发人员都能严格的按照JDBC的编程规范开发还好,但是事实是,一个团队中的开发人员是有差别的。
这其实只是API的使用过程中的一个插曲,当你看到应用程序中成百的使用JDBC实现类的时候,会发现如下的问题:
1、Statement使用完没有关闭,而是想着让Connection关闭的时候一并关闭,可是并非所有的驱动程序都有这样的行为。
2、创建了多个ResultSet或者Statement,只清理了最外层的,忽视了里层的。

3、忘记关闭Connection。
JDBC规范在指定数据库访问异常的时候也没有能够进行的很彻底:
1、将异常类型定义为SQLException是一个值得商榷的地方。
2、SQLExcpetion没有采用将具体的异常情况子类化,以进一步抽象不同的数据访问的情况,而是采用ErrorCode的方式来区分访问过程中所出现的不同异常情况,其实这也没什么,只要能区分出具体的错误就行,但是JDBC规范却把ErrorCode的规范留给了数据库提供商,这导致了不同的数据库供应商对应了不同的ErrorCode,进而应用程序在捕获到SQLException后,还要看当前用的是什么数据库。
针对以上问题,Spring提供了相应的解决方案帮助我们提高开发效率!

为了解决JDBC API在实际使用中的各种尴尬的局面,spring提出了org.springframework.jdbc.core.JdbcTemplate作为数据访问的Helper类。JdbcTemplate是整个spring数据抽象层提供的所有JDBC API最佳实践的基础,框架内其它更加方便的Helper类以及更高层次的抽象,全部的构建于JdbcTemplate之上。抓住了JdbcTemplate,就抓住了spring框架JDBC API最佳实践的核心。
概括的说,JdbcTemplate主要关注一下两个事情:
1、封装所有的基于JDBC的数据访问的代码,以统一的格式和规范来使用JDBC API。所有的基于JDBC API的数据访问全部通过JdbcTemplate,从而避免了容易出错的数据访问方式。
2、对SQLException所提供的异常信息在框架内进行统一的转译,将基于JDBC的数据访问异常纳入Spring自身的异常层次之中,统一了数据接口的定义,简化了客户端代码对数据访问异常的处理。
Spring主要是通过模板方法对基于JDBC的数据访问代码进行统一的封装,所以我们可先看下模板方法:
模板方法主要是用于对算法的行为或者逻辑进行封装,即如果多个类中存在相似的算法逻辑或者行为逻辑,可以将这些逻辑提取到模板方法中实现,然后让相应的子类根据需要实现某些自定义的逻辑。
举个例子,所有的汽车,不管是宝马还是大众,他们的驾驶流程基本上是固定的。实际上,除了少数的实现细节有所不同之外,大部分的流程是相同的,基本上是如下所示的流程说明:
1、点火启动
2、踩刹车,挂前进的档位(不同的车在这一步会存在差异)
3、放下手动控制器(手刹)
4、踩油门启动车辆运行
此时,我们可以声明一个模板方法类,将确定的行为以模板的形式定义,而将不同的行为留给相应的子类来实现:

package com.google.spring.jdbc;

public abstract class Vehicle
{
    
    public final void drive()
    {
        startTheEnginee();//启动
        putIntoGear(); //前进
        looseHandBrake();//放下手刹
        stepOnTheGasAndGo();//踩油门前进
    }
    
    protected abstract void putIntoGear();
    
    private void startTheEnginee()
    {
        
    }
    
    private void looseHandBrake()
    {
        
    }
    
    private void stepOnTheGasAndGo()
    {
        
    }
}

drive()方法就是我们的模板方法,它被声明为final,表示该类是不能被子类重写的,车辆的自动挡和手动挡是不同的,所以留给了子类去实现:

package com.google.spring.jdbc;

public class VehicleAT extends Vehicle
{

    @Override
    protected void putIntoGear()
    {
        //挂前进档位

    }

}
package com.google.spring.jdbc;

public class VehicleMT extends Vehicle
{

    @Override
    protected void putIntoGear()
    {
        //踩离合器 挂前进档位

    }

}

这样,每个子类实现特有的逻辑就可以了。

 JdbcTemplate的演化
如果回头看一下最初的使用JDBC API进行数据访问的代码。就会发现,不管这些代码是谁负责的,也不管数据访问的逻辑如何,除了小部分的差异之外,所有的这些代码几乎都是按照同一个流程走下来的,如下:
1、conn=getDataSource().getConnection();
2、stmt=conn.createStatement()或者ps=conn.prepareStatement();
3、stmt.executeUpdate(sql)或者ps.executeUpdate()  或者进行相应的查询。
4、stmt.close()  stmt=null
5、catch处理数据库访问异常
6、关闭数据库连接避免连接泄露导致系统崩溃
对于多个DAO中充斥着几乎相同的JDBC API的使用代码,我们也可以采用模板方法,多这些代码进行重构,避免因个人操作不当所出现的种种问题,我们要做的,就是将一些公共的行为提取到模板方法中去,而特有的操作,比如每次执行不同的更新,或者对不同的查询结果进行不同的处理,则放入具体的子类中,这样,我们就有个JdbcTemplate的雏形:

package com.google.spring.jdbc;

import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;

import javax.sql.DataSource;

import org.springframework.dao.DataAccessException;


public abstract class JdbcTemplate
{
    private DataSource dataSource;
    
    
    public DataSource getDataSource()
    {
        return dataSource;
    }


    public void setDataSource(DataSource dataSource)
    {
        this.dataSource = dataSource;
    }


    public final Object execute(String sql)
    {
        Connection conn = null;
        Statement stmt = null;
        try
        {
            conn = this.getDataSource().getConnection();
            stmt = conn.createStatement();
            Object retValue = this.executeWithStatement(stmt, sql);
            return retValue;
        }
        catch (SQLException e)
        {
            throw new RuntimeException(e);
        }
        finally
        {
            closeStatement(stmt);
            closeConnection(conn);
        }
    }
    
    protected abstract Object executeWithStatement(Statement stmt,String sql);
    
    private final DataAccessException translateSQLException(SQLException e)
    {
        DataAccessException dataAccessException = null;
        //进行相应的转译
        return dataAccessException;
    }
    
    private final void closeStatement(Statement stmt)
    {
        //关闭Statement
    }
    
    private final void closeConnection(Connection conn)
    {
        //关闭Connection
    }
}

这样处理之后,JDBC代码的使用有了规范。但是,只使用模板方法还不足以提供方便的Helper类。顶着abstract的帽子,每次使用都要进行相应的子类化,这也太不靠谱了。所以,spring中的JdbcTemplate除了引入了模板方法之外,还引入了相应的Callback,避免了每次都子类化,比如,当引入了StatementCallback接口以后:

package com.google.spring.jdbc;

import java.sql.Statement;

public interface StatementCallback
{
    public Object doWithStatement(Statement stmt) throws SQLException;
}

这样这个真正的Helper类就存在了,如下所示:

package com.google.spring.jdbc;

import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;

import javax.sql.DataSource;

import org.springframework.dao.DataAccessException;


public  class JdbcTemplate
{
    private DataSource dataSource;
    
    
    public DataSource getDataSource()
    {
        return dataSource;
    }


    public void setDataSource(DataSource dataSource)
    {
        this.dataSource = dataSource;
    }


    public final Object execute(StatementCallback callback)
    {
        Connection conn = null;
        Statement stmt = null;
        try
        {
            conn = this.getDataSource().getConnection();
            stmt = conn.createStatement();
            Object retValue = callback.doWithStatement(stmt);
            return retValue;
        }
        catch (SQLException e)
        {
            throw new RuntimeException(e);
        }
        finally
        {
            closeStatement(stmt);
            closeConnection(conn);
        }
    }
    
    
    
    private final DataAccessException translateSQLException(SQLException e)
    {
        DataAccessException dataAccessException = null;
        //进行相应的转译
        return dataAccessException;
    }
    
    private final void closeStatement(Statement stmt)
    {
        //关闭Statement
    }
    
    private final void closeConnection(Connection conn)
    {
        //关闭Connection
    }
}

要在相应的DAO中使用该JdbcTemplate,只需要根据情况提供参数和相应的callback就可以了,如下所示:

final String sql = "update";
        JdbcTemplate jdbcTemplate = new JdbcTemplate();
        BasicDataSource dataSource = new BasicDataSource();
        jdbcTemplate.setDataSource(dataSource);
        //对dataSource进行setter操作
        StatementCallback callback = new StatementCallback()
        {
            public Object doWithStatement(Statement stmt) throws SQLException
            {
                return new Integer(stmt.executeUpdate(sql)) ;
            }
        };
        
        jdbcTemplate.execute(callback);

这样,开发人员只需要关注与数据访问逻辑相关的东西,JDBC底层的细节不需要再考虑了。
上述是spring中JdbcTemplate的中心思想,实际上,JdbcTemplate在实现上要考虑很多的东西,继承层次如下:

 

 org.springframework.jdbc.core.JdbcOperations接口定义了JdbcTemplate可以使用的JDBC操作集合,该接口提供的操作声明,从查询到更新无所不有。
JdbcTemplate的直接父类是JdbcAccessor,这是一个抽象类,主要为子类提供一些公用的属性:

 

 DataSource:javax.sql.DataSource是JDBC2.0之后引入的接口定义,用来替代java.sql.DriverManager的数据库连接方式,它的角色可以看做是JDBC的连接工厂,所以,基本上现在它应该作为获取数据库资源的统一接口。

SQLExceptionTranslator:JdbcTemplate委托此类进行异常的转译。

JdbcTemplate中的模板方法可分为如下的四组:
面向Connection的模板方法:
通过ConnectionCallback接口所公开的Connection进行数据访问

import java.sql.Connection;
import java.sql.SQLException;

import org.springframework.dao.DataAccessException;

public interface ConnectionCallback
{
    Object doInConnection(Connection con) throws SQLException, DataAccessException;
}
public Object execute(ConnectionCallback action) throws DataAccessException {
        Assert.notNull(action, "Callback object must not be null");

        Connection con = DataSourceUtils.getConnection(getDataSource());
        try {
            Connection conToUse = con;
            if (this.nativeJdbcExtractor != null) {
                // Extract native JDBC Connection, castable to OracleConnection or the like.
                conToUse = this.nativeJdbcExtractor.getNativeConnection(con);
            }
            else {
                // Create close-suppressing Connection proxy, also preparing returned Statements.
                conToUse = createConnectionProxy(con);
            }
            return action.doInConnection(conToUse);         }
        catch (SQLException ex) {
            // Release Connection early, to avoid potential connection pool deadlock
            // in the case when the exception translator hasn't been initialized yet.
            DataSourceUtils.releaseConnection(con, getDataSource());
            con = null;
            throw getExceptionTranslator().translate("ConnectionCallback", getSql(action), ex);
        }
        finally {
            DataSourceUtils.releaseConnection(con, getDataSource());
        }
    }

可以随意操作Connection。
面向Statement的模板方法:
该模板方法主要处理基于SQL的数据访问请求。该组模板方法通过org.springframework.jdbc.core.StatementCallback回调接口,对外公开java.sql.Statement的操作句柄。该方式缩小了回调接口内的权限范围,但是提高了API使用上的安全性和便捷性。
面向PreparedStatement的模板方法:
对于使用包含查询参数的SQL请求来说,使用PreparedStatement可以让我们免于SQL注入的攻击,而在使用PreparedStatement之前,需要根据传入的包含参数的SQL对其进行创建,所以,面向PreparedStatement的模板方式会通过org.springframework.jdbc.core.PreparedStatementCreator的回调接口公开Connection以允许PreparedStatement的创建。PreparedStatement创建之后,会公开org.springframework.jdbc.core.PreparedStatementCallback回调接口,以支持其使用PreparedStatement进行数据访问。
面向CallableStatement的模板方法:
JDBC支持使用CallableStatement进行数据库存储过程的访问,面向CallableStatement的的模板方法会通过org.springframework.jdbc.core.CallableStatementCreator公开的Connection用于创建调用存储过程的CallableStatement。之后,再通过org.springframework.jdbc.core.CallableStatementCallback公开的CallableStatement的操作句柄,实现基于存储过程的数据访问。
每一组的模板方法都 有一个核心的方法实现,其它的属于同一组的重载的模板方法,会调用这个核心的方法来完成最终的工作。以面向Statement的模板方法为例,使用StatementCallback回调接口作为方法参数的execute方法是这组的核心代码:

public Object execute(StatementCallback action) throws DataAccessException {
        Assert.notNull(action, "Callback object must not be null");

        Connection con = DataSourceUtils.getConnection(getDataSource());
        Statement stmt = null;
        try {
            Connection conToUse = con;
            if (this.nativeJdbcExtractor != null &&
                    this.nativeJdbcExtractor.isNativeConnectionNecessaryForNativeStatements()) {
                conToUse = this.nativeJdbcExtractor.getNativeConnection(con);
            }
            stmt = conToUse.createStatement();
            applyStatementSettings(stmt);
            Statement stmtToUse = stmt;
            if (this.nativeJdbcExtractor != null) {
                stmtToUse = this.nativeJdbcExtractor.getNativeStatement(stmt);
            }
            Object result = action.doInStatement(stmtToUse);
            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.
            JdbcUtils.closeStatement(stmt);
            stmt = null;
            DataSourceUtils.releaseConnection(con, getDataSource());
            con = null;
            throw getExceptionTranslator().translate("StatementCallback", getSql(action), ex);
        }
        finally {
            JdbcUtils.closeStatement(stmt);
            DataSourceUtils.releaseConnection(con, getDataSource());
        }
    }

其它模板方法会根据自身的签名,构建相应的StatementCallback实例以调用回调接口中公开的方法,例如:

public void execute(final String sql) throws DataAccessException {
        if (logger.isDebugEnabled()) {
            logger.debug("Executing SQL statement [" + sql + "]");
        }

        class ExecuteStatementCallback implements StatementCallback, SqlProvider {
            public Object doInStatement(Statement stmt) throws SQLException {
                stmt.execute(sql);
                return null;
            }
            public String getSql() {
                return sql;
            }
        }
        execute(new ExecuteStatementCallback());
    }

同一组内的模板方法,可以根据使用的方便性进行增加,只要在实现的时候,将相应的条件加以对应,改组的回调接口进行封装,最终调用当前组的核心模板方法即可。
下面来逐一看下这些方法:
public void execute(final String sql):

public void execute(final String sql) throws DataAccessException {
        if (logger.isDebugEnabled()) {
            logger.debug("Executing SQL statement [" + sql + "]");
        }

        class ExecuteStatementCallback implements StatementCallback, SqlProvider {
            public Object doInStatement(Statement stmt) throws SQLException {
                stmt.execute(sql);
                return null;
            }
            public String getSql() {
                return sql;
            }
        }
        execute(new ExecuteStatementCallback());
    }

 

根据传入的静态SQL语句进行更新,无返回值,使用的是Statement

 public Object execute(String callString, CallableStatementCallback action) throws DataAccessException:

public Object execute(String callString, CallableStatementCallback action) throws DataAccessException {
        return execute(new SimpleCallableStatementCreator(callString), action);
    }

内部调的是

public Object execute(CallableStatementCreator csc, CallableStatementCallback action)
            throws DataAccessException {

        Assert.notNull(csc, "CallableStatementCreator must not be null");
        Assert.notNull(action, "Callback object must not be null");
        if (logger.isDebugEnabled()) {
            String sql = getSql(csc);
            logger.debug("Calling stored procedure" + (sql != null ? " [" + sql  + "]" : ""));
        }

        Connection con = DataSourceUtils.getConnection(getDataSource());
        CallableStatement cs = null;
        try {
            Connection conToUse = con;
            if (this.nativeJdbcExtractor != null) {
                conToUse = this.nativeJdbcExtractor.getNativeConnection(con);
            }
            cs = csc.createCallableStatement(conToUse);
            applyStatementSettings(cs);
            CallableStatement csToUse = cs;
            if (this.nativeJdbcExtractor != null) {
                csToUse = this.nativeJdbcExtractor.getNativeCallableStatement(cs);
            }
            Object result = action.doInCallableStatement(csToUse);
            handleWarnings(cs);
            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.
            if (csc instanceof ParameterDisposer) {
                ((ParameterDisposer) csc).cleanupParameters();
            }
            String sql = getSql(csc);
            csc = null;
            JdbcUtils.closeStatement(cs);
            cs = null;
            DataSourceUtils.releaseConnection(con, getDataSource());
            con = null;
            throw getExceptionTranslator().translate("CallableStatementCallback", sql, ex);
        }
        finally {
            if (csc instanceof ParameterDisposer) {
                ((ParameterDisposer) csc).cleanupParameters();
            }
            JdbcUtils.closeStatement(cs);
            DataSourceUtils.releaseConnection(con, getDataSource());
        }
    }

JdbcTemplate在此提供了一个内部类SimpleCallableStatementCreator:

private static class SimpleCallableStatementCreator implements CallableStatementCreator, SqlProvider {

        private final String callString;

        public SimpleCallableStatementCreator(String callString) {
            Assert.notNull(callString, "Call string must not be null");
            this.callString = callString;
        }

        public CallableStatement createCallableStatement(Connection con) throws SQLException {
            return con.prepareCall(this.callString);
        }

        public String getSql() {
            return this.callString;
        }
    }

根据传入的sql语句创建一CallableStatement

public Object execute(String sql, PreparedStatementCallback action) throws DataAccessException

public Object execute(String sql, PreparedStatementCallback action) throws DataAccessException {
        return execute(new SimplePreparedStatementCreator(sql), action);
    }

可知其内部调用了
execute(new SimplePreparedStatementCreator(sql), action)方法。

public Object execute(PreparedStatementCreator psc, PreparedStatementCallback action)
            throws DataAccessException {

        Assert.notNull(psc, "PreparedStatementCreator must not be null");
        Assert.notNull(action, "Callback object must not be null");
        if (logger.isDebugEnabled()) {
            String sql = getSql(psc);
            logger.debug("Executing prepared SQL statement" + (sql != null ? " [" + sql + "]" : ""));
        }

        Connection con = DataSourceUtils.getConnection(getDataSource());
        PreparedStatement ps = null;
        try {
            Connection conToUse = con;
            if (this.nativeJdbcExtractor != null &&
                    this.nativeJdbcExtractor.isNativeConnectionNecessaryForNativePreparedStatements()) {
                conToUse = this.nativeJdbcExtractor.getNativeConnection(con);
            }
            ps = psc.createPreparedStatement(conToUse);
            applyStatementSettings(ps);
            PreparedStatement psToUse = ps;
            if (this.nativeJdbcExtractor != null) {
                psToUse = this.nativeJdbcExtractor.getNativePreparedStatement(ps);
            }
            Object result = action.doInPreparedStatement(psToUse);
            handleWarnings(ps);
            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.
            if (psc instanceof ParameterDisposer) {
                ((ParameterDisposer) psc).cleanupParameters();
            }
            String sql = getSql(psc);
            psc = null;
            JdbcUtils.closeStatement(ps);
            ps = null;
            DataSourceUtils.releaseConnection(con, getDataSource());
            con = null;
            throw getExceptionTranslator().translate("PreparedStatementCallback", sql, ex);
        }
        finally {
            if (psc instanceof ParameterDisposer) {
                ((ParameterDisposer) psc).cleanupParameters();
            }
            JdbcUtils.closeStatement(ps);
            DataSourceUtils.releaseConnection(con, getDataSource());
        }
    }

在创建PreparedStatementCreator实现类的时候,JdbcTemplate为其默认提供了一个SimplePreparedStatementCreator内部静态类,可根据传入的SQL语句创建一个PreparedStatement  代码如下:

private static class SimplePreparedStatementCreator implements PreparedStatementCreator, SqlProvider {

        private final String sql;

        public SimplePreparedStatementCreator(String sql) {
            Assert.notNull(sql, "SQL must not be null");
            this.sql = sql;
        }

        public PreparedStatement createPreparedStatement(Connection con) throws SQLException {
            return con.prepareStatement(this.sql);
        }

        public String getSql() {
            return this.sql;
        }
    }

其它的模仿方法与此类似,可以触类旁通!

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!