Table of Contents
概述
java原生JDBC
JdbcTemplate
jdbcTemplate查询query
jdbcTemplate更新操作update/delete...
JdbcTemplate的query底层是如何实现的?
JdbcTemplate最佳实践
数据库连接和DataSource
总结
本篇博客基于java8.
概述
Spring框架提供了可扩展的SQL数据库的支持。然而spring的东西使用久了之后,如果不去了解背后的原理的话可能越来越迷糊,本篇博客的目的是为了 分析spring框架如何实现的和各种数据库在各种场景下的连接配置的问题。我们将从最原生java数据库连接讲起。
java原生JDBC
相信不少学习java的朋友在第一次接触数据库的时候,一定都接触过最原生的数据库连接,以mysql为例,大致代码按如下:
try{
//加载JDBC驱动程序:
Class.forName("com.mysql.jdbc.Driver");
String url = "jdbc:mysql://127.0.0.1:3306/dbname";
String username = "";
String password = "";
// 创建与MySQL数据库的连接类的实例
Connection conn = DriverManager.getConnection(url, username, password);
// 用conn创建Statement对象类实例
Statement sql_statement = conn.createStatement();
// 执行sql
ResultSet result = sql_statement.executeQuery(query);
//处理结果
...
}catch (Exception e) {
e.printStackTrace();
} finally {
if (conn != null) {
// 关闭连接
try {
conn.close();
System.out.println("Database connection terminated");
} catch (Exception e) { /* ignore close errors */
}
}
}
上面的代码就是纯jdbc的连接数据库代码,只需安装jdk导入java 的原生包就可以写出来,不需要使用任何第三方的包。
上述代码主要有下面这几个步骤:
- 加载JDBC驱动程序
- 创建数据库的连接(需要用户名密码等信息)
- 创建一个Statement
- 执行SQL语句(处理结果)
- 处理异常,关闭连接
上面的代码可以看出一些问题,比如模版代码过多,对于一个用户来说,他可能只是想执行一条sql语句,并不想关心其他连接,异常处理,加载驱动的问题。我们可以很容易想到的是,如果有一个方法或者一个工具,让用户可以只需要指定好数据库连接信息,就可以执行各种数据库操作,岂不是非常好吗?这就是框架开发这想做的事情。
JdbcTemplate
JdbcTemplate是spring提供的最经典也是最流行的jdbc实现方式之一。现在除了JdbcTemplate之外,还有SimpleJdbcInsert,SimpleJdbcCall两个更新的实现方式。
JdbcTemplate是“最底层”的方法,所有其他spring的方法在他们的包装之下都使用了JdbcTemplate。
JdbcTemplate是spring JDBC core这个包中的重点类。 它处理资源的创建和释放,帮助我们避免常见错误,例如上面直接使用java原生代码而忘记关闭连接。 它执行核心JDBC工作流的基本任务(例如语句创建和执行),应用程序代码只需要提供SQL并提取结果。 JdbcTemplate类主要包含下面这些功能:
- 执行SQL查询语句
- 执行更新语句和存储过程调用
- 对ResultSet实例执行迭代并提取返回的参数值。
- 捕获JDBC异常并将它们转换为org.springframework.dao packag中定义的通用的,信息量更大的异常层次结构。
使用jdbcTemplate需要配置datasource。使用spring ioc容器的话,则可以把datasource配置为一个bean,直接让数据库操作的时候引用即可。spring文档特别指出:应始终将DataSource配置为Spring IoC容器中的bean。这说明spring对于ioc容器中的datasource bean提供了非常多方便的功能。
我们可以使用jdbcTemplate来执行查询,更新,以及一些其他操作。如下:
jdbcTemplate查询query
下面的例子是返回数据的数量
int rowCount = this.jdbcTemplate.queryForObject("select count(*) from t_actor", Integer.class);
下面的查询是返回一个字符串:
String lastName = this.jdbcTemplate.queryForObject(
"select last_name from t_actor where id = ?",
new Object[]{1212L}, String.class);
也可以查询并填充单个对象:
Actor actor = this.jdbcTemplate.queryForObject(
"select first_name, last_name from t_actor where id = ?",
new Object[]{1212L},
new RowMapper<Actor>() {
public Actor mapRow(ResultSet rs, int rowNum) throws SQLException {
Actor actor = new Actor();
actor.setFirstName(rs.getString("first_name"));
actor.setLastName(rs.getString("last_name"));
return actor;
}
});
查询并填充一系列对象:
List<Actor> actors = this.jdbcTemplate.query(
"select first_name, last_name from t_actor",
new RowMapper<Actor>() {
public Actor mapRow(ResultSet rs, int rowNum) throws SQLException {
Actor actor = new Actor();
actor.setFirstName(rs.getString("first_name"));
actor.setLastName(rs.getString("last_name"));
return actor;
}
});
在应用程序当中为了分离逻辑,我们常常将上述代码的RowMapper逻辑提出去,变成如下两个方法:
public List<Actor> findAllActors() {
return this.jdbcTemplate.query( "select first_name, last_name from t_actor", new ActorMapper());
}
private static final class ActorMapper implements RowMapper<Actor> {
public Actor mapRow(ResultSet rs, int rowNum) throws SQLException {
Actor actor = new Actor();
actor.setFirstName(rs.getString("first_name"));
actor.setLastName(rs.getString("last_name"));
return actor;
}
}
值得一提的是,上述代码在spring batch的读数据环节也有应用。
jdbcTemplate更新操作update/delete...
下面是一个insert插入的例子:
this.jdbcTemplate.update(
"insert into t_actor (first_name, last_name) values (?, ?)",
"Leonor", "Watling");
下面是一个更新update的例子:
this.jdbcTemplate.update(
"update t_actor set last_name = ? where id = ?",
"Banjo", 5276L);
下面是一个删除的例子:
this.jdbcTemplate.update(
"delete from actor where id = ?",
Long.valueOf(actorId));
事实上上述三个操作我们可以看到调用的都是jdbcTemplate的同一个名字的方法,只是sql语句等参数不同。
JdbcTemplate的query底层是如何实现的?
前面我们讲了使用JdbcTempplate我们可以执行update,query等操作,那么JdbcTempplate在底层是如何实现这些操作的呢?其实这个问题稍微有经验的开发中很容易就能答出,其底层必然是借助java原生的jdbc实现的。
在JdbcTempplate的源代码当中,我们可以找到JdbcTempplate的query的核心逻辑的实现如下。
@Override
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) {
// 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());
}
}
其大致包含这些步骤: 使用DataSourceUtils获取数据库connection,根据sql获取Statement,执行sql语句,处理异常,在finally代码块中关闭资源,包括关闭Statement已经释放数据库连接。
我们可以基本认为其核心实现就是借助原生jdbc的。
此外获取connection的底层代码实现如下:
public static Connection doGetConnection(DataSource dataSource) throws SQLException {
Assert.notNull(dataSource, "No DataSource specified");
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();
}
// Else we either got no holder or an empty thread-bound holder here.
logger.debug("Fetching JDBC Connection from DataSource");
Connection con = dataSource.getConnection();
if (TransactionSynchronizationManager.isSynchronizationActive()) {
logger.debug("Registering transaction synchronization for JDBC Connection");
// Use same Connection for further JDBC actions within the transaction.
// Thread-bound object will get removed by synchronization at transaction completion.
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;
}
可以看到,connection是通过DataSource实例去获取的,而DataSource里面则配置有数据库连接的url,username,password等信息。
Update的实现也是同理的,只是其调用的是PrepareStatement的executeUpdate方法而非executeQuery方法。
JdbcTemplate最佳实践
使用jdbcTemplate的好处有线程安全,配置简单,只需一次配置即可等有点。在使用jdbcTemplate的时候也有最佳实践可以遵循。
使用JdbcTemplate类时的常见做法是在Spring配置文件中配置DataSource,然后将共享的DataSource的bean依赖注入到DAO类中。 JdbcTemplate是在DataSource的setter中创建的。 这意味着DAO类有似于以下的代码:
public class JdbcCorporateEventDao implements CorporateEventDao {
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
// JDBC-backed implementations of the methods on the CorporateEventDao follow...
}
对应的datasource的xml配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<bean id="corporateEventDao" class="com.example.JdbcCorporateEventDao">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<context:property-placeholder location="jdbc.properties"/>
</beans>
在使用springboot开发的实际体验中,我们常常用java注解配置以及application.properties配置文件代替上述内容,但是其内容都是一样的,必定有驱动名,url,用户名,密码等信息,这就是和我们使用原生jdbc代码去连数据库有联系的地方。无论使用何种方式,该有的一个也不会少。
无论您选择使用上述哪种模板初始化样式,每次要运行SQL时,很少有需要创建JdbcTemplate类的新实例。 配置完成后,JdbcTemplate实例是线程安全的。 如果我们应用程序访问多个数据库,则可能需要多个JdbcTemplate实例(例如做数据迁移,就是一个典型的场景),这需要多个DataSource,随后需要多个不同配置的JdbcTemplate实例。
关于JdbcTemplate的内容我们暂时了解到这里,他当然还提供了很多其他方法,可以在需要使用时直接查看其文档或者接口即可。
数据库连接和DataSource
我们在上面已极多次提到了DataSource,可以猜出它是spring用来管理数据库连接的,现在,我们专门来研究一下dataSource的内容。
Spring通过DataSource获取与数据库的连接。 DataSource是java定义的JDBC规范的一部分,是一个通用的连接工厂。 它允许容器或框架从应用程序代码中隐藏连接池和事务管理问题。在大型项目中,作为开发人员,则无需了解有关如何连接到数据库的详细信息。 这是设置数据源的管理员的责任。
使用Spring的JDBC层时,可以从JNDI获取数据源,也可以使用第三方提供的连接池实现配置自己的数据源。 流行的实现是Apache Jakarta Commons DBCP和C3P0。 Spring发行版中的实现仅用于测试目的,不提供池。
以下示例是如何在Java中配置DriverManagerDataSource类型的datasource的例子:
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("org.hsqldb.jdbcDriver");
dataSource.setUrl("jdbc:hsqldb:hsql://localhost:");
dataSource.setUsername("sa");
dataSource.setPassword("");
可以看到配置的就是连接数据库必须的信息。
下面是一个DBCP实现的DataSource配置
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<context:property-placeholder location="jdbc.properties"/>
虽然实现类不一样,其他其要求的配置都是一样的。
除了上述的配置方式外,我们还有下面这些方法配置datasource
- 使用
DataSourceUtils类提供的功能,
实现SmartDataSource
- 继承
AbstractDataSource
- 使用
SingleConnectionDataSource
使用DriverManagerDataSource
使用TransactionAwareDataSourceProxy
上述方式就不一一展开,我们可以在使用时根据需求选择。
总结
最后做个总结,我们不难看出,无论使用spring提供的什么方式去进行数据库操作,我们始终需要去配置datasouce,而无论使用哪种实现的datasource我们也必须提供和配置对应的和原生jdbc连接一样的数据库连接信息,springboot通过jar包可以帮我们决定驱动,但是url,用户名,密码仍然要我们提供。我们不难想到其实现必然要做原生jdbc连接做的事情。
而使用spring的提供的数据库连接的好处也是显而易见的,很多数据库连接的问题,它都帮我们管理解决了,如关闭连接等。