spring源码深度解析——数据库连接JDBC(2)

spring源码深度解析——数据库连接JDBC(2)

1. query方法

在之前的数据库连接JDBC(1)中我们分析了insert/update方法,接下来我们分析下query功能的实现。在之前的示例中我们用了List users = this.jdbcTemplate.query(“select * from t_user”,new UserRowMapper());来进行数据库查询操作,我们查询下JdbcTemplate中query方法的实现:

public <T> List<T> query(String sql, Object[] args, int[] argTypes, RowMapper<T> rowMapper) throws DataAccessException {
    return result(query(sql, args, argTypes, new RowMapperResultSetExtractor<>(rowMapper)));
}
public <T> T query(String sql, Object[] args, int[] argTypes, ResultSetExtractor<T> rse) throws DataAccessException {
    return query(sql, newArgTypePreparedStatementSetter(args, argTypes), rse);
}
public <T> T query(String sql, @Nullable PreparedStatementSetter pss, ResultSetExtractor<T> rse) throws DataAccessException {
    return query(new SimplePreparedStatementCreator(sql), pss, rse);
}
public <T> T query(
        PreparedStatementCreator psc, @Nullable final PreparedStatementSetter pss, final ResultSetExtractor<T> rse)
        throws DataAccessException {

    Assert.notNull(rse, "ResultSetExtractor must not be null");
    logger.debug("Executing prepared SQL query");

    return execute(psc, new PreparedStatementCallback<T>() {
        @Override
        @Nullable
        public T doInPreparedStatement(PreparedStatement ps) throws SQLException {
            ResultSet rs = null;
            try {
                if (pss != null) {
                    pss.setValues(ps);
                }
                rs = ps.executeQuery();
                return rse.extractData(rs);
            }
            finally {
                JdbcUtils.closeResultSet(rs);
                if (pss instanceof ParameterDisposer) {
                    ((ParameterDisposer) pss).cleanupParameters();
                }
            }
        }
    });
}

上面的代码我们通过层层跟踪,发现其实现逻辑与update相似,只不过在回调类PreparedStatementCallback的实现中使用的是ps.executeQuery()执行查询操作,而且返回方法上也做了一些额外的处理。
在上面最后的query方法中使用了rse.extractData(rs)对返回结果进行了处理,将结果封装成POJO,这里的rse对应的类型是RowMapperResultSetExtractor,其方法的具体实现如下:

public List<T> extractData(ResultSet rs) throws SQLException {
    List<T> results = (this.rowsExpected > 0 ? new ArrayList<>(this.rowsExpected) : new ArrayList<>());
    int rowNum = 0;
    while (rs.next()) {
        results.add(this.rowMapper.mapRow(rs, rowNum++));
    }
    return results;
}

此方法中并没有复杂的逻辑,只是对返回结果遍历并使用rowMapper进行转换。
在上面的update与query方法的分析中我们都是分析了带有参数的方法,在jdbcTemplate中还存在无输入参数”?”的方法,他们与上面带有“?”的方法有什么不同呢?我们就以query方法来看下具体实现:

public <T> List<T> query(String sql, RowMapper<T> rowMapper) throws DataAccessException {
    return result(query(sql, new RowMapperResultSetExtractor<>(rowMapper)));
}
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
        @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());
}

我们可以看到这里直接调用了execute方法。继续分析这里的execute方法:

public <T> T execute(StatementCallback<T> action) 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 {
        JdbcUtils.closeStatement(stmt);
        DataSourceUtils.releaseConnection(con, getDataSource());
    }
}

初看方法的实现发现与之前的execute方法没有区别,但仔细观察发现这里是直接使用大量statement,而不是之前的PreparedStatement,这两者有什么区别呢?
PreparedStatement是用来执行SQL查询语句的API之一,Java提供了 Statement、PreparedStatement 和 CallableStatement三种方式来执行查询语句,其中 Statement 用于通用查询, PreparedStatement 用于执行参数化查询,而 CallableStatement则是用于存储过程。同时PreparedStatement还经常会在Java面试被提及,譬如:Statement与PreparedStatement的区别以及如何避免SQL注入式攻击?这篇教程中我们会讨论为什么要用PreparedStatement?使用PreparedStatement有什么样的优势?PreparedStatement又是如何避免SQL注入攻击的?
PreparedStatement是java.sql包下面的一个接口,用来执行SQL语句查询,通过调用connection.preparedStatement(sql)方法可以获得PreparedStatment对象。数据库系统会对sql语句进行预编译处理(如果JDBC驱动支持的话),预处理语句将被预先编译好,这条预编译的sql查询语句能在将来的查询中重用,这样一来,它比Statement对象生成的查询速度更快。
预处理语句的优势
PreparedStatement提供了诸多好处,企业级应用开发中强烈推荐使用PreparedStatement来做SQL查询,下面列出PreparedStatement的几点优势。
(1)PreparedStatement可以写动态参数化的查询
用PreparedStatement你可以写带参数的sql查询语句,通过使用相同的sql语句和不同的参数值来做查询比创建一个不同的查询语句要好,下面是一个参数化查询:

SELECT interest_rate FROM loan WHERE loan_type=?

现在你可以使用任何一种loan类型如:”personal loan”,”home loan” 或者”gold loan”来查询,这个例子叫做参数化查询,因为它可以用不同的参数调用它,这里的”?”就是参数的占位符。
(2)PreparedStatement比 Statement 更快
使用 PreparedStatement 最重要的一点好处是它拥有更佳的性能优势,SQL语句会预编译在数据库系统中。执行计划同样会被缓存起来,它允许数据库做参数化查询。使用预处理语句比普通的查询更快,因为它做的工作更少(数据库对SQL语句的分析,编译,优化已经在第一次查询前完成了)。为了减少数据库的负载,生产环境中的JDBC代码你应该总是使用PreparedStatement 。值得注意的一点是:为了获得性能上的优势,应该使用参数化sql查询而不是字符串追加的方式。
(3)PreparedStatement可以防止SQL注入式攻击
如果你是做Java web应用开发的,那么必须熟悉那声名狼藉的SQL注入式攻击。去年Sony就遭受了SQL注入攻击,被盗用了一些Sony play station(PS机)用户的数据。在SQL注入攻击里,恶意用户通过SQL元数据绑定输入。
在使用参数化查询的情况下,数据库系统(eg:MySQL)不会将参数的内容视为SQL指令的一部分来处理,而是在数据库完成SQL指令的编译后,才套用参数运行,因此就算参数中含有破坏性的指令,也不会被数据库所运行。
补充:避免SQL注入的第二种方式:
在组合SQL字符串的时候,先对所传入的参数做字符取代(将单引号字符取代为连续2个单引号字符,因为连续2个单引号字符在SQL数据库中会视为字符中的一个单引号字符。
(4)比起凌乱的字符串追加似的查询,PreparedStatement查询可读性更好、更安全。

2. queryForObject

spring中不仅为我们提供了query方法,还对query方法进行了各种封装,如下图所示:
这里写图片描述
我们就以queryForObject为例来进行分析下看spring是如何对结果进行封装的,如下是方法的实现:

public <T> T queryForObject(String sql, Class<T> requiredType) throws DataAccessException {
    return queryForObject(sql, getSingleColumnRowMapper(requiredType));
}
public <T> T queryForObject(String sql, RowMapper<T> rowMapper) throws DataAccessException {
    List<T> results = query(sql, rowMapper);
    return DataAccessUtils.nullableSingleResult(results);
}

我们看到上面的方法中的区别就是对于rowMapper的使用,其使用了SingleColumnRowMapper的实例,我们看下mapRow方法在此类中的具体实现:

public T mapRow(ResultSet rs, int rowNum) throws SQLException {
    // Validate column count.
    ResultSetMetaData rsmd = rs.getMetaData();
    int nrOfColumns = rsmd.getColumnCount();
    if (nrOfColumns != 1) {
        throw new IncorrectResultSetColumnCountException(1, nrOfColumns);
    }
    // Extract column value from JDBC ResultSet.
    Object result = getColumnValue(rs, 1, this.requiredType);
    if (result != null && this.requiredType != null && !this.requiredType.isInstance(result)) {
        // Extracted value does not match already: try to convert it.
        try {
            return (T) convertValueToRequiredType(result, this.requiredType);
        }
        catch (IllegalArgumentException ex) {
            throw new TypeMismatchDataAccessException(
                    "Type mismatch affecting row number " + rowNum + " and column type '" +
                    rsmd.getColumnTypeName(1) + "': " + ex.getMessage());
        }
    }
    return (T) result;
}

在上面的方法中使用convertValueToRequiredType进行类型转换,继续跟踪代码:

protected Object convertValueToRequiredType(Object value, Class<?> requiredType) {
    if (String.class == requiredType) {
        return value.toString();
    }
    else if (Number.class.isAssignableFrom(requiredType)) {
        if (value instanceof Number) {
            // Convert original Number to target Number class.
            return NumberUtils.convertNumberToTargetClass(((Number) value), (Class<Number>) requiredType);
        }
        else {
            // Convert stringified value to target Number class.
            return NumberUtils.parseNumber(value.toString(),(Class<Number>) requiredType);
        }
    }
    else if (this.conversionService != null && this.conversionService.canConvert(value.getClass(), requiredType)) {
        return this.conversionService.convert(value, requiredType);
    }
    else {
        throw new IllegalArgumentException(
                "Value [" + value + "] is of type [" + value.getClass().getName() +
                "] and cannot be converted to required type [" + requiredType.getName() + "]");
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值