JDBC(Java Data Base Connectivity, Java 数据库连接),是一种用于执行 SQL语句的 JAVA API,可以为多种关系数据库提供统一访问,它由一组用 Java 语言编写的类和接口组成,JDBC为数据库开发提供了一个标准的API,据此可以构建理高级的工具和接口,使数据库开发人员能够用纯JAVA API编写数据库应用程序,并且可跨平台运行,并且不受数据库供应商的限制。
JDBC连接数据库流程及其原理如下。
- 1.在开发环境中加载指定数据库驱动程序,接下来的实验中,使用数据库是 MySQL ,所以需要去加载 MYSQL支持的 JDBC 的驱动程序
将下载得到的驱动程序加载进开发环境。 - 2 在 javat程序中加载驱动程序,在 java 程序中,可以通过"Class.forName(“指定数据库驱动程序”)"的方式来加载添加到开发环境的驱动程序,例如加载 MySQL 的数据驱动程序的代码为 Class.forName(“com.mysql.jdbc.Driver”);
- 3 创建数据对象。通过DriverManager 类创建数据库连接对象Connection,DriverManager 类作用于 Java 程序和 JDBC 驱动之间,用于检查所加载的驱动程序是否可以建立连接,然后通过它的 getConnection 方法根据数据库 URL,用户名和密码,创建一个 JDBC Connection 对象,例如,Connection connection= DriverManager.getConnenction(“连接数据库的 URL”,“用户名”,“密码”) ,其中,URL=协义名+IP地址(域名)+端口+数据库名称,用户名和密码是指登陆数据库时所需要的用户名和密码,具体示例创建 MySQL的数据库连接代码如下:
Connection connection = DriverManager.getConnection(“jdbc:mysql//localhost:3306/myuser”,“username”,“password”); - 创建 Statement对象,Statement 类的主要是用于执行静态 SQL 语句来返回它所生成的结果对象,通过 Connection 对象 createStatement()方法可以创建一个 Statement对象,例如:
Statement statement = connection.createStatement(); - 5 调用 Statement 对象的相关方法执行相应的 SQL 语句,通过 execuUpdate()方法来对数据更新,包括插入和删除操作,例如向 staff 表中插入一条数据的代码。
statement.executeUpdate("insert into staff(name,age,sex,address,depart,worklen,age) " + “VALUES (‘tom’,321,‘M’,‘china’,‘Person’,‘3’,‘3000’)”);
通过调用 Statement 对象的 executeQuery()方法进行数据查询,而查询结果会得到 ResultSet对象,ResultSet 对象执行查询数据库后返回的数据的集合,ResultSet 对象具有可以指向当前数据行的指针,通过对象的next()方法,使指针指向下一 行,然后将数据库以列号或者名取出,如果 next()方法返回 null,则表示下一行中没有数据存在,使用示例代码如下:
ResultSet resultSet = statement.executeQuery(“select * from staff”); - 6 关闭数据库连接,使用完数据库或者不需要访问数据库时,通过 Connection 的 close()方法及关闭数据库连接。
Spring 连接数据库程序实现
Spring中的 jdbc连接与直接使用 jdbc 去连接还是有所差别的,Spring 对 JDBC 做了大量的封装,消除了冗余代码,使得开发量大大减少,下面通过一个小例子让大家简单的认识一下 Spring 中 jdbc 的操作。
1.创建数据库表
CREATE TABLE lz_user
(
id
bigint(20) unsigned NOT NULL AUTO_INCREMENT,
is_delete
tinyint(2) DEFAULT ‘0’,
gmt_create
datetime DEFAULT CURRENT_TIMESTAMP COMMENT ‘’,
gmt_modified
datetime DEFAULT CURRENT_TIMESTAMP,
username
varchar(32) DEFAULT NULL COMMENT ‘用户名’,
password
varchar(64) DEFAULT NULL COMMENT ‘密码’,
real_name
varchar(64) DEFAULT NULL,
manager_id
int(11) DEFAULT NULL COMMENT ‘管理员id’,
PRIMARY KEY (id
)
) ENGINE=InnoDB AUTO_INCREMENT=462 DEFAULT CHARSET=utf8mb4 COMMENT=’’;
2.创建对应的数据库的 PO
@Data public class User { private Long id; private Integer isDelete; private Date gmtCreate; private Date gmtModified; private String username; private String password; private String realName; private Long managerId; public User(String username, String password) { this.username = username; this.password = password; } }
3.创建表与实体间的映射
public class UserRowMapper implements RowMapper { @Override public Object mapRow(ResultSet rs, int rowNum) throws SQLException { User user = new User( rs.getString("username"), rs.getString("password") ); return user; } }
4.创建数据操作接口
public interface UserService { void save(User user); List<User> getUsers(); List<User> getUsersByName(String username); List<String> queryObjectUsersByName(String username); }
5.创建数据操作接口实现类
public class UserServiceImpl implements UserService { private JdbcTemplate jdbcTemplate; public void setJdbcTemplate(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); } @Override public void save(User user) { jdbcTemplate.update("INSERT INTO lz_user (username, password, real_name, manager_id) VALUES ( ?, ?, ?, ?) ", new Object[]{user.getUsername(), user.getPassword(), user.getRealName(), user.getManagerId()}); } @Override public List<User> getUsers() { return jdbcTemplate.query("select * from lz_user ", new UserRowMapper()); } @Override public List<User> getUsersByName(String username) { return jdbcTemplate.query("select * from lz_user where username = ? ", new Object[]{username},new UserRowMapper()); } @Override public List<String> queryObjectUsersByName(String username) { List<String> list = jdbcTemplate.queryForList("select password from lz_user where username = '" + username + "'", String.class); return list; } }
6.创建Spring 配置文件
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <!--配置数据源--> <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName" value="com.mysql.jdbc.Driver"></property> <property name="url" value="jdbc:mysql://xxx.16.157.xxx:3306/pple_test?characterEncoding=utf-8"></property> <property name="username" value="ldd_biz"></property> <property name="password" value="Hello1234"></property> <!--连接池启动时初始化值--> <property name="initialSize" value="1"></property> <!--最大空闲值,当经过一个高峰时间后,连接池可以慢慢的将一些己经用不到的连接慢慢释放掉一部分,直到 maxIdle 为止--> <property name="maxIdle" value="2"></property> <!--最小空闲值,当空闲的连接数少于阀值时,连接池就会预申请去一些连接,以免洪峰时来不及申请--> <property name="minIdle" value="1"></property> </bean> <!--配置业务 bean--> <bean id="userService" class="com.spring_1_100.test_61_70.test69_database.UserServiceImpl"> <!--向属性jdbcTemplate 注入数据源 dataSource--> <property name="jdbcTemplate" ref="dataSource"></property> </bean> </beans>
7.测试
public class Test69 { public static void main(String[] args) { ApplicationContext ac = new ClassPathXmlApplicationContext("classpath:spring_1_100/config_61_70/spring69.xml"); UserService userService =(UserService) ac.getBean("userService"); List<User> users = userService.getUsersByName("19884189046") ; System.out.println(JSON.toJSONString(users)); } }
结果:
query/sava/update 功能实现
我们以上面的例子为基础开始分析 Spring 中对 jdbc 的支持,首先寻找整个功能的切入点。在示例中,我们可以看到所有的数据库操作都封装在 UserServiceImpl 中,而 UserServiceImpl 中的所有数据操作又是以其内部操作 jdbcTemplate 为基础,这个 jdbcTemplate 可以作为源码分析的切入点。我们一起去看看它是如何实现,又是如何被初始化的。
在 UserServiceImpl中的 jdbcTemplate 的初始化是从 setDataSource 函数开始的,DataSource实例通过参数注入,DataSource 的创建过程是引入了第三方的连接池,这里不做过多的介绍,DataSource 是整个数据库操作的基础,里面封装了整个数据库连接信息,我们首先以查询实体类为例进行代码跟踪。在分析源码之前,我们先来了解一下 jdbc查询是如何实现的。请看下面示例。
@Test public void query(){ Connection conn = null; PreparedStatement pstemt = null; try { //注册加载jdbc驱动 Class.forName("com.mysql.jdbc.Driver"); //打开连接 conn = DriverManager.getConnection("jdbc:mysql://172.16.157.238:3306/pple_test?characterEncoding=utf-8","ldd_biz","Hello1234"); //创建执行对象 String sql = "select * from lz_user where username = ? "; pstemt = conn.prepareStatement(sql); pstemt.setString(1,"19884189046"); //执行sql语句 ResultSet rs = pstemt.executeQuery(); //展开结果集 while (rs.next()) { System.out.println(rs.getString("username")); System.out.println(rs.getString("password")); } rs.close(); pstemt.close(); conn.close(); } catch (Exception e) { e.printStackTrace(); } }
数据库表数据结构:
public void jdbcUpdate() { Connection conn = null; PreparedStatement pstemt = null; try { //注册加载jdbc驱动 Class.forName("com.mysql.jdbc.Driver"); //打开连接 conn = DriverManager.getConnection("jdbc:mysql://172.16.157.238:3306/pple_test?characterEncoding=utf-8", "ldd_biz", "Hello1234"); //创建执行对象 String sql = "update lz_user set password = ? where username = ? "; pstemt = conn.prepareStatement(sql); pstemt.setString(1, "123456"); pstemt.setString(2,"19884189046"); //执行sql语句 int num = pstemt.executeUpdate(); System.out.println(num); conn.close(); } catch (Exception e) { e.printStackTrace(); } }
结果执行成功,更新1条数据
本身 jdbc 实现数据查询非常简单。
- 注册加载 jdbc 驱动
- 打开连接
- 创建可执行对象
- 执行 sql 语句
- 获取结果集
- 关闭资源
通过这个示例,我们看到,其实query 和 update之间最大区别在execute()方法上,query 是执行executeQuery()方法,update是执行executeUpdate(),而其他的代码是公共的,那我们来看看 Spring 源码是如何实现。是否有代码公用呢?不用想都知道,Spring肯定做了比较好的封装。
JdbcTemplate.java
public <T> List<T> query(String sql, Object[] args, RowMapper<T> rowMapper) throws DataAccessException { //将rowMapper保存到RowMapperResultSetExtractor对象中 return query(sql, args, new RowMapperResultSetExtractor<T>(rowMapper)); }
对于查询实体类而言,在操作中我们只需要提供 SQL语句以及语句中对应的参数和参数类型,其他操作可由 Spring 来完成,这些工作到底包括哪些?进入 jdbcTemplate 的 query 方法
public <T> T query(String sql, Object[] args, ResultSetExtractor<T> rse) throws DataAccessException { //将sql参数args设置到ArgumentPreparedStatementSetter属性中 return query(sql, newArgPreparedStatementSetter(args), rse); }
public <T> T query(String sql, PreparedStatementSetter pss, ResultSetExtractor<T> rse) throws DataAccessException { //将 sql 保存到SimplePreparedStatementCreator对象属性中 return query(new SimplePreparedStatementCreator(sql), pss, rse); }
进入到方法后,Spring 并不急着处理操作,而是做足准备,使用ArgumentPreparedStatementSetter对参数进行封装,同时又使用SimplePreparedStatementCreator对 SQL语句进行封装。
经过了数据封装后便可以进入核心的数据处理代码了。
public <T> T query( PreparedStatementCreator psc, final PreparedStatementSetter pss, final ResultSetExtractor<T> rse) throws DataAccessException { return execute(psc, new PreparedStatementCallback<T>() { @Override public T doInPreparedStatement(PreparedStatement ps) throws SQLException { ResultSet rs = null; try { if (pss != null) { //设置 PreparedStatement需要的参数 pss.setValues(ps); } rs = ps.executeQuery(); ResultSet rsToUse = rs; if (nativeJdbcExtractor != null) { rsToUse = nativeJdbcExtractor.getNativeResultSet(rs); } return rse.extractData(rsToUse); } finally { //释放ResultSet JdbcUtils.closeResultSet(rs); if (pss instanceof ParameterDisposer) { ((ParameterDisposer) pss).cleanupParameters(); } } } }); }
在回调类PreparedStatementCallback的实现中使用的是 ps.executeQuery()执行查询操作,而且在返回方法上也做了一些额外的处理。
public <T> T execute(PreparedStatementCreator psc, PreparedStatementCallback<T> action) throws DataAccessException { Connection con = DataSourceUtils.getConnection(getDataSource()); PreparedStatement ps = null; try { Connection conToUse = con; if (this.nativeJdbcExtractor != null && this.nativeJdbcExtractor.isNativeConnectionNecessaryForNativePreparedStatements()) { conToUse = this.nativeJdbcExtractor.getNativeConnection(con); } //创建 ParepareStatement ps = psc.createPreparedStatement(conToUse); //设置超时时间,fetchSize,maxRows applyStatementSettings(ps); PreparedStatement psToUse = ps; //有时候必要会对数据库clob、 blob数据型进行操作,再加上spring 环境不得不要启用NativeJdbcExtractor 来帮助完成相关工作。 //为什么使用NativeJdbcExtractor 这篇博客中对nativeJdbcExtractor在简单的介绍 if (this.nativeJdbcExtractor != null) { psToUse = this.nativeJdbcExtractor.getNativePreparedStatement(ps); } //调用回调函数,执行 execute()方法 T result = action.doInPreparedStatement(psToUse); handleWarnings(ps); return result; } catch (SQLException ex) { //释放数据库连接避免当异常转换器没有被初始化的时候出现潜在的连接死锁 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(); } //关闭 Statement JdbcUtils.closeStatement(ps); //con.close(); DataSourceUtils.releaseConnection(con, getDataSource()); } }
以上方法对常用操作进行了封装,包括以下几项内容。
1.获取数据库连接
获取数据库连接也并非直接使用Connection con = dataSource.getConnection();方法这么简单,同样也考虑到了诸多情况。
DataSourceUtils.java
public static Connection doGetConnection(DataSource dataSource) throws SQLException { ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource); if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) { conHolder.requested(); if (!conHolder.hasConnection()) { logger.debug("Fetching resumed JDBC Connection from DataSource"); conHolder.setConnection(dataSource.getConnection()); } return conHolder.getConnection(); } Connection con = dataSource.getConnection(); //当前线程支持同步 if (TransactionSynchronizationManager.isSynchronizationActive()) { //在事务中使用同一数据库连接 ConnectionHolder holderToUse = conHolder; if (holderToUse == null) { holderToUse = new ConnectionHolder(con); } else { holderToUse.setConnection(con); } //记录数据库连接 holderToUse.requested(); TransactionSynchronizationManager.registerSynchronization( new ConnectionSynchronization(holderToUse, dataSource)); holderToUse.setSynchronizedWithTransaction(true); if (holderToUse != conHolder) { TransactionSynchronizationManager.bindResource(dataSource, holderToUse); } } return con; }
在数据库连接方面,Spring 主要考虑的是关于事务方面的处理,基于事务的处理的特殊性,Spring 需要保证线程中的数据库操作是在同一个事务连接。
JdbcTemplate.java
protected void applyStatementSettings(Statement stmt) throws SQLException { int fetchSize = getFetchSize(); if (fetchSize >= 0) { //设置第一次拉取的最大条数,默认值为10,Fetch相当于读缓存,如果使用setFetchSize设置Fetch Size为10000, //本地缓存10000条记录,每次执行rs.next,只是内存操作,不会有数据库网络消耗,效率就会高些。但需要 //注意的是,Fetch Size值越高则占用内存越高,要避免出现OOM错误。 stmt.setFetchSize(fetchSize); } int maxRows = getMaxRows(); if (maxRows >= 0) { //setMaxRows方法的话是取得最大行,最大以后的数据会被丢掉。设置这个参数虽然可以避免报内存错误, //不过在很多场合没法使用,因为查询的结果肯定想完整的抽取出来的情况很多。这个方法和limit类似。 stmt.setMaxRows(maxRows); } //设置超时时间 DataSourceUtils.applyTimeout(stmt, getDataSource(), getQueryTimeout()); }
setFetchSize 最主要是为了减少网络交互次数设计的,访问 ReresultSet 时,如果它每次只从服务器上读取一行数据,则会产生大量的开销,setFetchSize 的意思是当调用 rs.next时,ResultSet 会一次性从服务器上取得多少行数据回来,这样下次 rs.next 时,它可以直接从内存中获取数据而不需要网络交互,提高了效率,这个设置可能会被某些 JDBC 驱动忽略,而且设置过大也会造成内存上升。
setMaxRows 将此 Statement对象生成的所有ResultSet对象可又包含的最大行数据限制设置为给定数。
public void setValues(PreparedStatement ps) throws SQLException { if (this.args != null) { //遍历所有的参数 for (int i = 0; i < this.args.length; i++) { Object arg = this.args[i]; doSetValue(ps, i + 1, arg); } } }
对单个参数及类型做匹配处理
ArgumentPreparedStatementSetter.java
protected void doSetValue(PreparedStatement ps, int parameterPosition, Object argValue) throws SQLException { if (argValue instanceof SqlParameterValue) { SqlParameterValue paramValue = (SqlParameterValue) argValue; StatementCreatorUtils.setParameterValue(ps, parameterPosition, paramValue, paramValue.getValue()); } else {//如果参数的类型不是SqlParameterValue类型 StatementCreatorUtils.setParameterValue(ps, parameterPosition, SqlTypeValue.TYPE_UNKNOWN, argValue); } }
StatementCreatorUtils.java
public static void setParameterValue(PreparedStatement ps, int paramIndex, int sqlType, Object inValue) throws SQLException { setParameterValueInternal(ps, paramIndex, sqlType, null, null, inValue); }
private static void setParameterValueInternal(PreparedStatement ps, int paramIndex, int sqlType, String typeName, Integer scale, Object inValue) throws SQLException { String typeNameToUse = typeName; int sqlTypeToUse = sqlType; Object inValueToUse = inValue; //如果方法参数是SqlParameterValue类型 if (inValue instanceof SqlParameterValue) { SqlParameterValue parameterValue = (SqlParameterValue) inValue; if (logger.isDebugEnabled()) { logger.debug("Overriding type info with runtime info from SqlParameterValue: column index " + paramIndex + ", SQL type " + parameterValue.getSqlType() + ", type name " + parameterValue.getTypeName()); } if (parameterValue.getSqlType() != SqlTypeValue.TYPE_UNKNOWN) { sqlTypeToUse = parameterValue.getSqlType(); } if (parameterValue.getTypeName() != null) { typeNameToUse = parameterValue.getTypeName(); } inValueToUse = parameterValue.getValue(); } if (logger.isTraceEnabled()) { logger.trace("Setting SQL statement parameter value: column index " + paramIndex + ", parameter value [" + inValueToUse + "], value class [" + (inValueToUse != null ? inValueToUse.getClass().getName() : "null") + "], SQL type " + (sqlTypeToUse == SqlTypeValue.TYPE_UNKNOWN ? "unknown" : Integer.toString(sqlTypeToUse))); } if (inValueToUse == null) { setNull(ps, paramIndex, sqlTypeToUse, typeNameToUse); } else { setValue(ps, paramIndex, sqlTypeToUse, typeNameToUse, scale, inValueToUse); } }
private static void setValue(PreparedStatement ps, int paramIndex, int sqlType, String typeName, Integer scale, Object inValue) throws SQLException { //如果是SqlTypeValue类型 if (inValue instanceof SqlTypeValue) { ((SqlTypeValue) inValue).setTypeValue(ps, paramIndex, sqlType, typeName); } //如果是SqlValue类型 else if (inValue instanceof SqlValue) { ((SqlValue) inValue).setValue(ps, paramIndex); } //varchar,nvarchar,longvarchar,longnvarchar 类型 else if (sqlType == Types.VARCHAR || sqlType == Types.NVARCHAR || sqlType == Types.LONGVARCHAR || sqlType == Types.LONGNVARCHAR) { ps.setString(paramIndex, inValue.toString()); } //类型是 Clob,或者 NClob 类型 else if ((sqlType == Types.CLOB || sqlType == Types.NCLOB) && isStringValue(inValue.getClass())) { String strVal = inValue.toString(); if (strVal.length() > 4000) { // Necessary for older Oracle drivers, in particular when running against an Oracle 10 database. // Should also work fine against other drivers/databases since it uses standard JDBC 4.0 API. try { if (sqlType == Types.NCLOB) { ps.setNClob(paramIndex, new StringReader(strVal), strVal.length()); } else { ps.setClob(paramIndex, new StringReader(strVal), strVal.length()); } return; } catch (AbstractMethodError err) { logger.debug("JDBC driver does not implement JDBC 4.0 'setClob(int, Reader, long)' method", err); } catch (SQLFeatureNotSupportedException ex) { logger.debug("JDBC driver does not support JDBC 4.0 'setClob(int, Reader, long)' method", ex); } } // Fallback: regular setString binding ps.setString(paramIndex, strVal); } // decimal ,numeric 类型 else if (sqlType == Types.DECIMAL || sqlType == Types.NUMERIC) { if (inValue instanceof BigDecimal) { ps.setBigDecimal(paramIndex, (BigDecimal) inValue); } else if (scale != null) { ps.setObject(paramIndex, inValue, sqlType, scale); } else { ps.setObject(paramIndex, inValue, sqlType); } } // date 类型 else if (sqlType == Types.DATE) { if (inValue instanceof java.util.Date) { if (inValue instanceof java.sql.Date) { ps.setDate(paramIndex, (java.sql.Date) inValue); } else { ps.setDate(paramIndex, new java.sql.Date(((java.util.Date) inValue).getTime())); } } else if (inValue instanceof Calendar) { Calendar cal = (Calendar) inValue; ps.setDate(paramIndex, new java.sql.Date(cal.getTime().getTime()), cal); } else { ps.setObject(paramIndex, inValue, Types.DATE); } } // time 类型 else if (sqlType == Types.TIME) { if (inValue instanceof java.util.Date) { if (inValue instanceof java.sql.Time) { ps.setTime(paramIndex, (java.sql.Time) inValue); } else { ps.setTime(paramIndex, new java.sql.Time(((java.util.Date) inValue).getTime())); } } else if (inValue instanceof Calendar) { Calendar cal = (Calendar) inValue; ps.setTime(paramIndex, new java.sql.Time(cal.getTime().getTime()), cal); } else { ps.setObject(paramIndex, inValue, Types.TIME); } } // timestamp 类型 else if (sqlType == Types.TIMESTAMP) { if (inValue instanceof java.util.Date) { if (inValue instanceof java.sql.Timestamp) { ps.setTimestamp(paramIndex, (java.sql.Timestamp) inValue); } else { ps.setTimestamp(paramIndex, new java.sql.Timestamp(((java.util.Date) inValue).getTime())); } } else if (inValue instanceof Calendar) { Calendar cal = (Calendar) inValue; ps.setTimestamp(paramIndex, new java.sql.Timestamp(cal.getTime().getTime()), cal); } else { ps.setObject(paramIndex, inValue, Types.TIMESTAMP); } } //如果是不知道类型 else if (sqlType == SqlTypeValue.TYPE_UNKNOWN || (sqlType == Types.OTHER && "Oracle".equals(ps.getConnection().getMetaData().getDatabaseProductName()))) { //如果是 String 类型,调用 preparestatement.setString()方法,第一个参数是? 的索引值, //但是在 jdbc 中,索引是以1起始值 if (isStringValue(inValue.getClass())) { ps.setString(paramIndex, inValue.toString()); } else if (isDateValue(inValue.getClass())) { ps.setTimestamp(paramIndex, new java.sql.Timestamp(((java.util.Date) inValue).getTime())); } else if (inValue instanceof Calendar) { Calendar cal = (Calendar) inValue; ps.setTimestamp(paramIndex, new java.sql.Timestamp(cal.getTime().getTime()), cal); } else { ps.setObject(paramIndex, inValue); } }//其他类型,直接设置为 Object 类型 else { ps.setObject(paramIndex, inValue, sqlType); } }
RowMapperResultSetExtractor.java
public List<T> extractData(ResultSet rs) throws SQLException { List<T> results = (this.rowsExpected > 0 ? new ArrayList<T>(this.rowsExpected) : new ArrayList<T>()); int rowNum = 0; while (rs.next()) { results.add(this.rowMapper.mapRow(rs, rowNum++)); } return results; }
rse.extractData(rsToUse);方法负责将结果进行封装并转换至对象,rse 当前代表的类为 RowMapperResultSetExtractor,而在构造RowMapperResultSetExtractor的时候,我们又自定义了 rowMapper 设置进去,上面的代码,也没有什么复杂的逻辑,只是对返回的结果集遍历,并以此使用 rowMapper 进行转换。调用下面代码进行转换。
UserRowMapper.java
public Object mapRow(ResultSet rs, int rowNum) throws SQLException { User user = new User( rs.getString("username"), rs.getString("password") ); return user; }
JdbcUtils.java
public static void closeResultSet(ResultSet rs) { if (rs != null) { try { rs.close(); } catch (SQLException ex) { logger.trace("Could not close JDBC ResultSet", ex); } catch (Throwable ex) { logger.trace("Unexpected exception on closing JDBC ResultSet", ex); } } }
JdbcTemplate.java
protected void handleWarnings(Statement stmt) throws SQLException { //当设置为忽略警告时只尝试打印日志 if (isIgnoreWarnings()) { if (logger.isDebugEnabled()) { //如果日志开启的情况下打印日志 SQLWarning warningToLog = stmt.getWarnings(); while (warningToLog != null) { logger.debug("SQLWarning ignored: SQL state '" + warningToLog.getSQLState() + "', error code '" + warningToLog.getErrorCode() + "', message [" + warningToLog.getMessage() + "]"); warningToLog = warningToLog.getNextWarning(); } } } else { handleWarnings(stmt.getWarnings()); } }
这里用到了一个类SQLWarning,SQLWarning 提供了相关数据库访问警告信息异常。这些警告直接链接导致报告警告方法所在的对象,警告可以从 Connection,Statement 和 ResultSet 对象中获得,试图在己经关闭的连接上获取警告将导致抛出异常,类似的,试图在己经关闭的语句上或己经关闭的结果集上获取警告也将导致抛出异常。
很多人不是很理解什么情况下会产生警告而不是异常,在这里给读者提示个最常见的警告 DataTruncation:DataTruncation 直接继承 SQLWaring,由于某种原因意外的截断数据值会以 DataTruncation 警告形式报告异常。
对于警告的处理方式并不是直接抛出异常,出现警告很可能出现数据错误,但是,并不一定会影响程序执行,所以,用户可以自己设置处理警告的方式,如默认的是忽略警告,当出现警告时只打印警告日志,而另一种方式只直接抛出异常。
JdbcUtils.java
public static void closeStatement(Statement stmt) { if (stmt != null) { try { stmt.close(); } catch (SQLException ex) { logger.trace("Could not close JDBC Statement", ex); } catch (Throwable ex) { logger.trace("Unexpected exception on closing JDBC Statement", ex); } } }
资源释放
数据库连接并不是直接调用 Connection 的 close方法,考虑到存在事务的情况,如果当前线程存在事务,那么说明当前线程中存在共用的数据库连接,在这种情况下,直接使用 ConnectionHolder 中的 released方法进行连接数减一,而不是真正的释放连接。
public static void releaseConnection(Connection con, DataSource dataSource) { try { doReleaseConnection(con, dataSource); } catch (SQLException ex) { logger.debug("Could not close JDBC Connection", ex); } catch (Throwable ex) { logger.debug("Unexpected exception on closing JDBC Connection", ex); } } public static void doReleaseConnection(Connection con, DataSource dataSource) throws SQLException { if (con == null) { return; } if (dataSource != null) { //当前线程中存在事务的情况下说明存在共用的数据库连接直接使用 ConnectionHolder 中的 //released 方法进行连接数减一而不是真正的释放连接 ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource); if (conHolder != null && connectionEquals(conHolder, con)) { // It's the transactional Connection: Don't close it. conHolder.released(); return; } } logger.debug("Returning JDBC Connection to DataSource"); doCloseConnection(con, dataSource); }
之前讲的 query 方法是带有参数的,也是是带有?的,那么还有一种情况就是不带? 的,这种处理方式是怎样的呢?请看下面代码。
public <T> List<T> query(String sql, RowMapper<T> rowMapper) throws DataAccessException { return query(sql, new RowMapperResultSetExtractor<T>(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 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() { return sql; } } return execute(new QueryStatementCallback()); }
与之前的 query 方法最大的不同就是少了参数及参数类型的传递,自然也少了 PreparedStatementSetter 类型的封装,既然少了PreparedStatementSetter类型的传入,调用 execute 方法自然也会有所改变了。
public <T> T execute(StatementCallback<T> 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); } T result = action.doInStatement(stmtToUse); handleWarnings(stmt); return result; } catch (SQLException ex) { 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()); } }
这个 execute 与之前的 execute 并没有太大差别,都是做一些常规的处理,诸如获取连接,释放连接,但是,有一个不一样的地方,就是 statement 的创建,这里直接使用 connection 创建,而带有参数的 SQL 使用的是 PrepareStatementCreator来创建,一个是普通的 Statement,另一个是 Preparestatement,两者究竟是何区别呢?
Preparestatement接口继承 Statement,并与之在两方面有所不同。
- Preparestatement实例包含己经编译的 SQL 语句,这就是使语句"准备好",包含于 PreparedStatement 对象中的 SQL 语句可具有一个或多个 in 参数,in 参数的值在 SQL 语句创建时未被指定,相反的,该语句为每个 in 参数保留了一个问号("?"),作为占位符,每个问号的值都必需在该语句执行前,通过适当的 setXXX()方法来提供。
- 由于 PreparedStatement 对象己经预编译过,所以其执行速度要快于 Statement 对象,因此,此次执行的 SQL语句经常创建为 PreparedStatement 对象,以提高效率。
作为Statement 的子类,PrepareStatement 继承了 Statement的所有功能,另外,它还添加了一整套方法,用于设置发送给数据库以取代 IN参数占位符的值,同时,三种方法 execute,executeQuery和 executeUpdate 己经被更改以使之不再需要参数,这些方法的 Statement形式(接受 SQL 语句的参数)形式,不应该用于 PreparedStatement 对象。
接下来,我们继续来分析 update/save的操作。
JdbcTemplate.java
public int update(String sql, Object... args) throws DataAccessException { return update(sql, newArgPreparedStatementSetter(args)); }
public int update(String sql, PreparedStatementSetter pss) throws DataAccessException { return update(new SimplePreparedStatementCreator(sql), pss); }
update/save,和 query 一样,将sql 参数封装到ArgPreparedStatementSetter中,将 sql 封装到SimplePreparedStatementCreator中。这一块和 query()基本上是一模一样。不同的是下面PreparedStatementCallback()回调方法,将 query()方法中的executeQuery()改成了 executeUpdate(),这里也印证在源码分析之前,就说过,在 Spring中,肯定将公共的代码给封装好了。
protected int update(final PreparedStatementCreator psc, final PreparedStatementSetter pss) throws DataAccessException { return execute(psc, new PreparedStatementCallback<Integer>() { @Override public Integer doInPreparedStatement(PreparedStatement ps) throws SQLException { try { if (pss != null) { //设置prepareStatement参数 pss.setValues(ps); } //执行插入或更新 int rows = ps.executeUpdate(); if (logger.isDebugEnabled()) { logger.debug("SQL update affected " + rows + " rows"); } return rows; } finally { if (pss instanceof ParameterDisposer) { ((ParameterDisposer) pss).cleanupParameters(); } } } }); }
对于executeUpdate()方法,我也不做过多讲解,因为我们平常使用 JDBC 也是这样使用,感兴趣的同学可以自行研究一下。
queryForObject
我们以 queryForObject 为例,来讨论一下 Spring 是如何返回结果的基础上进行封装的。
public <T> T queryForObject(String sql, RowMapper<T> rowMapper) throws DataAccessException { List<T> results = query(sql, rowMapper); return DataAccessUtils.requiredSingleResult(results); } public <T> T queryForObject(String sql, Class<T> requiredType) throws DataAccessException { return queryForObject(sql, getSingleColumnRowMapper(requiredType)); }
其实最大的不同还是对于 RowMapper 的使用,SingleColumnRowMapper类中的 mapRow:
public T mapRow(ResultSet rs, int rowNum) throws SQLException { ResultSetMetaData rsmd = rs.getMetaData(); //获取 sql 返回的列数,如果列数不为1,抛出异常 int nrOfColumns = rsmd.getColumnCount(); if (nrOfColumns != 1) { throw new IncorrectResultSetColumnCountException(1, nrOfColumns); } //根据请求值类型获取结果集中第一个参数的值 Object result = getColumnValue(rs, 1, this.requiredType); if (result != null && this.requiredType != null && !this.requiredType.isInstance(result)) { 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; }
protected Object getColumnValue(ResultSet rs, int index, Class<?> requiredType) throws SQLException { if (requiredType != null) { return JdbcUtils.getResultSetValue(rs, index, requiredType); } else { return getColumnValue(rs, index); } }
public static Object getResultSetValue(ResultSet rs, int index, Class<?> requiredType) throws SQLException { if (requiredType == null) { return getResultSetValue(rs, index); } Object value; if (String.class == requiredType) { return rs.getString(index); } else if (boolean.class == requiredType || Boolean.class == requiredType) { value = rs.getBoolean(index); } else if (byte.class == requiredType || Byte.class == requiredType) { value = rs.getByte(index); } else if (short.class == requiredType || Short.class == requiredType) { value = rs.getShort(index); } else if (int.class == requiredType || Integer.class == requiredType) { value = rs.getInt(index); } else if (long.class == requiredType || Long.class == requiredType) { value = rs.getLong(index); } else if (float.class == requiredType || Float.class == requiredType) { value = rs.getFloat(index); } else if (double.class == requiredType || Double.class == requiredType || Number.class == requiredType) { value = rs.getDouble(index); } else if (BigDecimal.class == requiredType) { return rs.getBigDecimal(index); } else if (java.sql.Date.class == requiredType) { return rs.getDate(index); } else if (java.sql.Time.class == requiredType) { return rs.getTime(index); } else if (java.sql.Timestamp.class == requiredType || java.util.Date.class == requiredType) { return rs.getTimestamp(index); } else if (byte[].class == requiredType) { return rs.getBytes(index); } else if (Blob.class == requiredType) { return rs.getBlob(index); } else if (Clob.class == requiredType) { return rs.getClob(index); } else { if (getObjectWithTypeAvailable) { try { return rs.getObject(index, requiredType); } catch (AbstractMethodError err) { logger.debug("JDBC driver does not implement JDBC 4.1 'getObject(int, Class)' method", err); } catch (SQLFeatureNotSupportedException ex) { logger.debug("JDBC driver does not support JDBC 4.1 'getObject(int, Class)' method", ex); } catch (SQLException ex) { logger.debug("JDBC driver has limited support for JDBC 4.1 'getObject(int, Class)' method", ex); } } return getResultSetValue(rs, index); } return (rs.wasNull() ? null : value); }
根据上面的代码,我们可以看到,Spring 先根据用户传入的requiredType类型,从结果集中获取相对应类型,如果确实没有相应的类型,则从结果集中获取 Object类型。
下面这种情况,如果返回值类型和用户传入类型不匹配,进行强制类型转换,而强制类型转换也只对 number 类型和 String 类型进行转换。从代码的结构上来看,SingleColumnRowMapper并没有实现复杂的功能,只是对普通数据类型做了简单的转换。并不能够传入对象类型,从而像 MyBatis 一样返回对象,但是对于查询简单的,比如查询某表的总行数,还是比较方便。
protected Object convertValueToRequiredType(Object value, Class<?> requiredType) { if (String.class == requiredType) { return value.toString(); } //转换原始类型 Number 类型的实例到 Number 类型的实例 else if (Number.class.isAssignableFrom(requiredType)) { if (value instanceof Number) { return NumberUtils.convertNumberToTargetClass(((Number) value), (Class<Number>) requiredType); } else { //转换 String类型的值到 Number类型 return NumberUtils.parseNumber(value.toString(),(Class<Number>) requiredType); } } else { throw new IllegalArgumentException( "Value [" + value + "] is of type [" + value.getClass().getName() + "] and cannot be converted to required type [" + requiredType.getName() + "]"); } }
流程:
1)query(sql, args, new RowMapperResultSetExtractor(rowMapper)):创建RowMapperResultSetExtractor ,查询结果集映射 1)query(sql, newArgPreparedStatementSetter(args), rse): 创建ArgumentPreparedStatementSetter,sql 参数值封装 1)query(new SimplePreparedStatementCreator(sql), pss, rse): 创建SimplePreparedStatementCreator,prepareStatement 简单的封装器 1)execute(): 执行 1)getConnection(): 获取数据库连接 2)createPreparedStatement(): 创建 prepareStatement 3)applyStatementSettings():配置参数设置 1)setFetchSize():最主要是为了减少网络交互次数设计的,访问 ReresultSet 时,如果它每次只从服务器上读取一行数据,则会产生大量的开销,setFetchSize 的意思是当调用 rs.next时,ResultSet 会一次性从服务器上取得多少行数据回来,这样下次 rs.next 时,它可以直接从内存中获取数据而不需要网络交互,提高了效率,这个设置可能会被某些 JDBC 驱动忽略,而且设置过大也会造成内存上升。 2)setMaxRows():将此 Statement对象生成的所有ResultSet对象可又包含的最大行数据限制设置为给定数。 3)applyTimeout(): 超时时间 4)doInPreparedStatement(): 1)setValues(): 设置 prepareStatement 参数值 2)executeQuery(): 执行查询 3)extractData(): 返回结果集参数封装 4)rs.close(): 关闭ResultSet 5)handleWarnings(): 处理警告信息 6)stmt.close(): 关闭 prepareStatement 7)con.close():关闭数据库连接 1)update(sql, newArgPreparedStatementSetter(args)):创建ArgPreparedStatementSetter封装 sql 参数 1)update(new SimplePreparedStatementCreator(sql), pss):创建SimplePreparedStatementCreator对象,封装 sql 1)execute(): 执行 1)getConnection(): 获取数据库连接 2)createPreparedStatement(): 创建 prepareStatement 3)applyStatementSettings():配置参数设置 1)setFetchSize():最主要是为了减少网络交互次数设计的,访问 ReresultSet 时,如果它每次只从服务器上读取一行数据,则会产生大量的开销,setFetchSize 的意思是当调用 rs.next时,ResultSet 会一次性从服务器上取得多少行数据回来,这样下次 rs.next 时,它可以直接从内存中获取数据而不需要网络交互,提高了效率,这个设置可能会被某些 JDBC 驱动忽略,而且设置过大也会造成内存上升。 2)setMaxRows():将此 Statement对象生成的所有ResultSet对象可又包含的最大行数据限制设置为给定数。 3)applyTimeout(): 超时时间 4)doInPreparedStatement(): 1)setValues(): 设置 prepareStatement 参数值 2)executeUpdate(): 执行查询 3)extractData(): 返回结果集参数封装 4)rs.close(): 关闭ResultSet 5)handleWarnings(): 处理警告信息 6)stmt.close(): 关闭 prepareStatement 7)con.close():关闭数据库连接
其实在查询和更新操作中,区别只在于execute()方法上,查询是调用executeQuery()方法,更新是调用了executeUpdate()方法,查询多了一个结果集的封装步骤,在 Spring JDBC 这一块,其实还是很简单的。Spring 没有做过多的封装。有兴趣的同学还是自己打断点调试一下,更加有感觉。
本文的 github 地址是https://github.com/quyixiao/spring_tiny/tree/master/src/main/java/com/spring_1_100/test_61_70/test69_database