JDBC是一种用于执行SQL语句的Java API,可以用于多种关系数据库提供统一访问,它是一组Java语言编写的类和接口组成的。JDBCwei开发人员提供一个标准的API,据此可以构建更高级的工具和接口。使数据库开发人员能够用纯Java API编写数据库应用程序,并且可以跨平台。并且不受数据库供应商的限制。
JDBC连接数据库的流程及原理如下。
- 在开发环境中加载指定数据库连接的驱动程序。
- 在java程序中加载驱动程序。在Java程序中可以通过“Class.forName(“指定数据库的驱动程序”)”的方式来加载添加到开发环境中的驱动程序、
- 创建数据库连接对象。通过DriverManager类创建数据库连接对象Connection。DriverManager类作用于Java驱动程序和JDBC驱动程序之间,用于检查所加载的驱动程序是否可以建立连接,然后通过getConnection方法根据数据库URL、用户名和密码,创建一个JDBC Connection对象。
- 创建Statement对象。Statement类的主要是用于执行静态SQL语句并返回它锁生成结果集对象。通过Connection对象的createStatement()方法创建一个Statement对象。
- 调用Statement对象的相关方法执行相对应的SQL语句,通过executeUpdate()方法来对数据进行更新,包括插入和删除等操作,通过调用executeQuery()方法进行数据的查询,而查询结果会得到ResultSet对象,ResultSet对象表示执行查询执行查询数据库返回的数据的集合,ResultSet对象可以指向当前数据行的指针。通过next()方法,使得指针指向下一行,然后将数据以列号或者字段名取出,如果当next()方法返回null,则表示下一行没有数据存在
- 关闭数据库连接,使用完数据库或者不需要访问数据库时,通过Connection的close()方法及时关闭数据库连接。
8.1 Spring连接数据库程序实现(JDBC)
Spring中的JDBC连接与直接使用JDBC去连接还是有所差别的,Spring对JDBC做了大量的封装,消除了冗余代码,使得开发量大大减少。
- 创建数据库表机构
- 创建对应数据表的PO
- 创建表与实体的映射
- 创建数据操作接口
- 创建数据操作接口实现类
- 创建Spring配置文件
8.2 save/update功能的实现
下面开始分析Spring中对JDBC的支持,首先寻找整个功能的切入点,其中还所有的数据库操作都是以其内部属性jdbcTemplate为基础的,这个jdbcTemplate可以作为源码分析出切入点,来看看是如何被初始化的。
jdbcTemplate的初始化是从setDataSource函数开始的,DataSource实例通过参数注入,DataSource的创建过程是引入第三方的连接池,DataSource是整个数据库操作的基础,里面封装了整个数据库的连接信息。首先以保存实体类为例:
jdbcTemplate.update(...);
对于保存一个实体类来讲,在操作中只需要提供SQL语句以及语句中对应的参数和参数类型,其他的操作有Spring完成,这些工作包括:
update(sql,newArgTypePrepareStatementSetter(arg,argTypes));
update(new simplePrepareStatementCreator(sql),pss)
进入update方法后,Spring并不急于进入核心处理操作,而是做足准备工作,使用ArgTypePrepareStatementSetter对参数和参数类型进行封装,同时又使用simplePrepareStatementCreator对sql语句进行封装。
进过封装后便可以进入核心的数据处理逻辑了
return execute(psc,new prepareStatementCallback<Integer>())
从代码可以知道execute方法是最基础的操作,而其他操作比如update、query等方法则是传入不同的prepareStatementCallback参数来执行不同的逻辑。
8.2.1 基础方法execute
execute作为数据库操作的核心入口,将大多数数据库操作相同的步骤统一封装,而将个性化操作使用参数prepareStatementCallback进行回调。
// 获取数据库连接
Connection con = DataSourceUtils.getConnection(getDateSource());
// 应用用户设定的输入参数
applyStatementSettings(ps);
// 调用回调函数
T result = action.doInPrepareStatement(psToUse);
handleWarnings(ps);
// 释放数据库连接避免当异常转换器没有初始化时出现潜在的连接池死锁
DataSourceUtils.releaseConnection(con,getDateSource());
- 获取数据库连接,获取数据库连接也并非直接使用dataSource.getConnection()方法这么简单,同样考虑了诸多情况
// 当前线程支持同步
// 在事务中使用同一数据库连接
// 记录数据库连接
在数据库连接方面,Springh主要考虑的是关于事务方法的处理,基于事务处理的特殊性,Spring需要保证线程中的数据库操作都是使用同一个事务连接。
- 应用用户设定的输入参数,setFetchSize最主要是为了减少网络交互数设计的。访问ResultSet时,如果它每次只从服务器上读取一行数据,则会产生大量的开销。setFetchSize的意思就是当调用rs.next时,ResultSet会一次性从服务器上取得多行数据回来,这样下次rs.next时,它可以直接从内从中获取数据而不需要网络交互,提高效率,这个设置可能被某些JDBC驱动忽视,而且设置过大也会造成内存使用量的上升。setMaxRows将此Statement对象生成的所有ResultSet对象可以包含的最大行数限制设置为给定数。
- 调用回调函数,处理一些通过方法外的个性化处理也就是prepareStatementCallback类型的参数的doInPrepareStatement方法的回调。
- 警告处理,这用到了一个类SQLWarning,SQLWarning提供关于数据库访问警告信息的异常。这些警告直接链接到导致报告警告信息的异常。警告可以从Connection,Statement和ResultSet对象上获取。试图在已经关闭的连接上获取警告将导致抛出异常。类似的,试图在已经关闭的语句上或已经关闭的结果集上获取警告也将导致抛出异常。注意,关闭语句时还会关闭它可能生成的结果集。很多人不理解什么情况下会产生警告而不是异常,最常见的警告DataTruncation:DataTruncation直接继承SQLWarning,由于某种原因以外的截断数据值时会以DataTruncation的形式报告异常。对于警告的处理方法并不是直接抛出异常,出现警告很可能会出现数据错误,但是,并不影响程序执行,所有用户可以自己设置处理警告的方式,如默认的是忽略警告,当出现警告时只打印警告日志,而另一种方式直接抛出异常
- 释放资源,数据库的连接释放并不是直接调用了Connection的API中的close方法,考虑到存在事务的情况,如果当前线程存在事务,那么说明在当前线程中存在公用数据库连接,在这种情况下直接使用ConnectionHolder中的released方法进行连接数减一,而不是真正的释放。
8.2.2 update中的回调函数
PrepareStatementCallback作为一个接口,其中有一个函数doInPrepareStatement,这个函数用于通用方法execute的时候无法处理的一些个性化处理方法,在update函数中的实现:
pss.setValue(ps);
其中用于真正执行SQL的ps.executeUpdate没有太多需要讲解,因为平时直接使用JDBC方式进行调用的时候会经常使用此方法。但是对于设置输入参数的函数pss.setValue(ps),首先,所有的操作都是以pss.setValue(ps)为入口的,这个pss所代表的类就是ArgTypePrepareStatementSetter。其中setValue如下:
// 遍历每个参数以作类型匹配及转换
// 如果是集合类则需要进入集合类内部递归解析集合内部属性
// 解析当前属性
doSetValue(ps,parameterPosition,this.argType[i],arg);
对单个参数及类型的匹配处理:
setParameterValueInternal(...);
8.3 query功能的实现
query功能的实现主要是jdbcTemplate.query(…);跟踪jdbcTemplate中的query方法。
query(sql,newArgTypePrepareStatementSetter(arg,argTypes),res);
query(new simplePrepareStatementCreator(sql),pss,res);
rs = ps.executeQuery();
return res.extractData(rsToUse);
可以看出整体套路与update差不多,只不过回调类PrepareStatementCallback的实现中使用了ps.executeQuery()执行查询操作,而且在返回方法上也做了一些额外的处理。
res.extractData(rsToUse)方法负责将结果进行封装并转换成POJO,res代表的类为RowMapperResultSetExtractor,而在构造RowMapperResultSetExtractor的时候又将自定义的rowMapper设置进去。
之前降了update方法以及query方法,使用这两个函数实例的SQL都是带参数的,也就是带有“?”的,那么还有另一种情况是不带“?”的,在Spring中使用的是另外一种处理方式。
return execute(new QueryStatementCallback());
与之前的query方法最大的不同是少了参数及参数类型的传递,自然少了PrepareStatementSetter类型的封装,既然少了PrepareStatementSetter类型的传入,调用execute方法自然也会有所改变。
这个execute和之前的execute并无太大差别,都是做一些常规处理,注入获取连接,释放连接等,但是有一个地方不一样,就是statement的创建,这里直接使用connection创建,而带有参数的SQL使用的是PrepareStatementCreator来创建的。一个是普通的statement,另一个PrepareStatement,两者的区别是:
- PrepareStatement接口继承了Statement,该实例包含已编译的SQL语句。这就是语句“准备好”。包含于PrepareStatement对象中的SQL语句可具有一个或多个IN参数。IN参数的值在SQL语句创建时未被指定,相反的,该语句为每个IN参数保留一个问号作为占位符。每个问号的值必须在该语句执行之前,通过适当的setXXX方法保存。
- 由于PrepareStatement对象已经编译过了,所以其执行速度要快于Statement对象,因此,多次执行的SQL语句经常创建为PrepareStatement对象,以提高效率。
作为Statement的子类,PrepareStatement继承了Statement的所有功能。另外它还添加了一整套方法,用于设置发送给数据库以代替IN参数占位符的值。同时三种execute,executeQuery()和executeUpdate已被更改以使之不在需要参数。这些方法的Statement形式不应用于PrepareStatement对象。
8.4 queryForObject
Spring中不仅仅提供了query基础方法,还在此基础上做了封装,提供了不同类型的query方法
以queryForObject为例
return queryForObject(...)
其实最大的不同还是对于RowMapper的使用。SingleColumnRowMapper类中的mapRow:
// 验证返回结果数
// 抽取第一个结果进行处理
// 转换到对应类型
return (T)converValueToRequiredType(result,this.requiredType);
对应的类型转换函数:
// 如果是Number类型转换原始Number类型的实体到Number类
// 如果是Number类型转换String类型的值到Number类