1、Spring框架JDBC的介绍
Spring JDBC - who does what?
动作 | Spring | 你 |
定义连接参数 | 是 | |
打开连接 | 是 | |
指定SQL语句 | 是 | |
声明参数,提供参数值 | 是 | |
准备、执行语句 | 是 | |
迭代结果(如果有) | 是 | |
操作每个迭代 | 是 | |
处理任何异常 | 是 | |
处理事务 | 是 | |
关闭连接、语句、结果集 | 是 |
一句话,Spring框架负责所有的低级别细节操作。
1.1、选择JDBC数据库访问的路径
所有的路径都需要兼容JDBC 2.0的驱动,部分特性可能需要JDBC 3.0 的驱动。
JdbcTemplate 是经典的Spring JDBC 路径,最流行。最低级的路径,所有其他的底层都是用一个JdbcTemplate。
NamedParameterJdbcTemplate 封装了一个JdbcTemplate,使用带名字的参数取代了传统的JDBC 占位符(?)。该路径,提供了更好的文档,当你有很多参数时,更易于使用。
SimpleJdbcInsert和SimpleJdbcCall 优化了数据库元数据,限制了必要数据的数量。该路径简化了编码,你只要提供表名或procedure并提供参数和列匹配的映射即可。这只有在数据库提供了足够的元数据时才起作用。否则需要显式的提供足够的数据。
RDBM Objects 包括 MappingSqlQuery、SqlUpdate和StoredProcedure,要求在数据访问层初始化时创建可复用的、线程安全的对象。
1.2、package层级
Spring框架的JDBC抽象框架由四个不同的包组成:core、datasource、object以及support。
org.springframework.jdbc.core 包含JdbcTemplate类和其各种回调接口,以及一组相关的类。子包jdbc.core.simple包含了SimpleJdbcInsert 和 SimpleJdbcCall类。子包jdbc.core.namedparam 包含了NamedParameterJdbcTemplate类和相关支持类。
jdbc.datasource包含了简化DataSource使用的工具类,以及各种简单的DataSource实现(可以用于Java EE 容器之外的测试和运行)。子包jdbc.datasource.embedded支持使用Java数据库引擎创建内嵌的数据库,如HSQL、H2、Derby。
jdbc.object包含了代表RDBMS查询、更新,以及stored-procedure(存储过程)。
This approach is modeled by JDO, although objects returned by queries are naturally disconnected from the database. This higher level of JDBC abstraction depends on the lower-level abstraction in the org.springframework.jdbc.core
package.
The org.springframework.jdbc.support
package provides SQLException
translation functionality and some utility classes. Exceptions thrown during JDBC processing are translated to exceptions defined in the org.springframework.dao
package. This means that code using the Spring JDBC abstraction layer does not need to implement JDBC or RDBMS-specific error handling. All translated exceptions are unchecked, which gives you the option of catching the exceptions from which you can recover while allowing other exceptions to be propagated to the caller.
2、使用JDBC核心类来控制基本的JDBC处理和错误处理
2.1、JdbcTemplate
处理资源的创建和释放。执行core JDBC工作流的基本任务,例如语句的创建和执行,让应用代码只需要提供SQL和抽取结果即可。
当你使用JdbcTemplate时,你只需要实现回调接口们。PreparedStatementCreator回调接口会根据给定的Connection和SQL以及任何必要的参数来创建预处理语句。CallableStatementCreator接口也是一样,只不过是创建callable语句。RowCallbackHandler接口会从ResultSet的每一行中提取值。
JdbcTemplate能在DAO实现内使用,使用DataSource实例化。
在Spring IoC容器中,DataSource总是需要被配置的!
JdbcTemplate使用例子:
本部分提供了一些JdbcTemplate的使用例子。没有详尽的列出JdbcTemplate的所有功能,具体见javadoc。
查询(SELECT)
int rowCount = this.jdbcTemplate.queryForObject("select count(*) from t_actor", Integer.class);
int countOfActorsNamedJoe = this.jdbcTemplate.queryForObject( "select count(*) from t_actor where first_name = ?", Integer.class, "Joe");
String lastName = this.jdbcTemplate.queryForObject( "select last_name from t_actor where id = ?", new Object[]{1212L}, String.class);
// 将结果映射到domain object 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; } });
// 将查询结果映射到一组domain objects。和上面相比,实际上只有等号左边变化了。 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; } });
或者,上面的可以这样写:
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; } }
使用JdbcTemplate更新 (INSERT/UPDATE/DELETE) --可以都使用update(..)
this.jdbcTemplate.update( "insert into t_actor (first_name, last_name) values (?, ?)", "Leonor", "Watling");
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操作--可以使用execute(..)执行任意SQL
this.jdbcTemplate.execute("create table mytable (id integer, name varchar(100))");
调用一个简单的存储过程:
this.jdbcTemplate.update( "call SUPPORT.REFRESH_ACTORS_SUMMARY(?)", Long.valueOf(unionId));
JdbcTemplate 最佳实践
JdbcTemplate 实例 是线程安全的!就是说,你只需要定义一个实例即可,随便注入并使用!
通常的实践是配置一个DataSource,然后将其注入到DAO类中。JdbcTemplate通过setDataSource(..)创建:
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... }
相应配置文件:
<?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>
或者,使用注解配合自动扫描。
@Repository public class JdbcCorporateEventDao implements CorporateEventDao { private JdbcTemplate jdbcTemplate; @Autowired public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); } // JDBC-backed implementations of the methods on the CorporateEventDao follow... }
<?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"> <!-- Scans within the base package of the application for @Component classes to configure as beans --> <context:component-scan base-package="org.springframework.docs.test" /> <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>
如果使用Spring的JdbcDaoSupport类,可以让你的DAO类继承它,然后自动继承了setDataSource(..)方法。
只有在访问多个数据库时才需要创建多个JdbcTemplate实例!
2.2、NamedParameterJdbcTemplate
// some JDBC-backed DAO class... private NamedParameterJdbcTemplate namedParameterJdbcTemplate; public void setDataSource(DataSource dataSource) { this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource); } public int countOfActorsByFirstName(String firstName) { String sql = "select count(*) from T_ACTOR where first_name = :first_name"; SqlParameterSource namedParameters = new MapSqlParameterSource("first_name", firstName); // this return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class); }
// some JDBC-backed DAO class... private NamedParameterJdbcTemplate namedParameterJdbcTemplate; public void setDataSource(DataSource dataSource) { this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource); } public int countOfActorsByFirstName(String firstName) { String sql = "select count(*) from T_ACTOR where first_name = :first_name"; Map<String, String> namedParameters = Collections.singletonMap("first_name", firstName); // 换成map了,一样 return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class); }
还可以,从domain object中获取参数 -- 不是写入domain object!!!如下:
public class Actor { private Long id; private String firstName; private String lastName; public String getFirstName() { return this.firstName; } public String getLastName() { return this.lastName; } public Long getId() { return this.id; } // setters omitted... }
// some JDBC-backed DAO class... private NamedParameterJdbcTemplate namedParameterJdbcTemplate; public void setDataSource(DataSource dataSource) { this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource); } public int countOfActors(Actor exampleActor) { // 这里 // notice how the named parameters match the properties of the above ‘Actor‘ class String sql = "select count(*) from T_ACTOR where first_name = :firstName and last_name = :lastName"; SqlParameterSource namedParameters = new BeanPropertySqlParameterSource(exampleActor); // 这里 return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class); }
一个接口,其实现类能够在SQLExceptions和Spring自己的org.springframework.dao.DataAccessException之间转换。其实现可以是泛型的也可以是具体的。
SQLErrorCodeSQLExceptionTranslator 是默认使用的实现。
2.4、执行语句
执行一个SQL语句只需要少量的代码。
import javax.sql.DataSource; import org.springframework.jdbc.core.JdbcTemplate; public class ExecuteAStatement { private JdbcTemplate jdbcTemplate; public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); } public void doExecute() { this.jdbcTemplate.execute("create table mytable (id integer, name varchar(100))"); } }
2.5、运行查询
一些查询方法返回单个值。使用queryForObject(..)从一行中获取一个计数或特定的值。后者将返回的JDBC类型转成指定的Java类型。如果类型转换无效,会抛出InvalidDataAccessApiUsageException异常。
import javax.sql.DataSource; import org.springframework.jdbc.core.JdbcTemplate; public class RunAQuery { private JdbcTemplate jdbcTemplate; public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); } public int getCount() { // 返回int return this.jdbcTemplate.queryForObject("select count(*) from mytable", Integer.class); } public String getName() { // 返回String return this.jdbcTemplate.queryForObject("select name from mytable", String.class); } }
除了单个结果的查询方法,有几个方法返回一个列表。最泛型的方法是queryForList(..),会返回List<Map<col_name_type, col_value_type>>。如下:
private JdbcTemplate jdbcTemplate; public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); } public List<Map<String, Object>> getList() { return this.jdbcTemplate.queryForList("select * from mytable"); }
返回结果类似这样:
[{name=Bob, id=1}, {name=Mary, id=2}]
2.6、更新数据库
下面的例子示意了更新某个特定主键对应的列。参数值可以通过变参数或数组传入。primitive类型会自动装箱。
import javax.sql.DataSource; import org.springframework.jdbc.core.JdbcTemplate; public class ExecuteAnUpdate { private JdbcTemplate jdbcTemplate; public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); } public void setName(int id, String name) { this.jdbcTemplate.update("update mytable set name = ? where id = ?", name, id); } }
2.7、获取自动生成的键 auto-generated keys -- 就是mybatis的<selectKey>
JDBC 3.0标准支持使用update()方法获取数据库生成的主键。该方法使用一个PreparedStatementCreator作为其第一个参数。另一个参数是一个KeyHolder,其包含了update成功时生成的key。
下面的例子在Oracle中可以正常工作,但在其他平台上可能无法工作。
final String INSERT_SQL = "insert into my_test (name) values(?)"; final String name = "Rob"; KeyHolder keyHolder = new GeneratedKeyHolder(); jdbcTemplate.update( new PreparedStatementCreator() { public PreparedStatement createPreparedStatement(Connection connection) throws SQLException { PreparedStatement ps = connection.prepareStatement(INSERT_SQL, new String[] {"id"}); ps.setString(1, name); return ps; } }, keyHolder); // keyHolder.getKey() now contains the generated key
3、控制数据库连接
3.1、DataSource
Spring通过一个DataSource来获取到数据库的连接。DataSource是JDBC标准的一部分,是一个连接工厂。它允许容器或框架隐藏连接池和事务管理。
当使用Spring的JDBC层,你是从JNDI获取一个data source,或者,你使用第三方连接池实现来配置自己的data source。流行的连接池实现包括DBCP和C3P0. Spring中的实现仅用于测试目的,不提供pooling功能。
本部分使用Spring的DriverMangerDataSource实现和几个其他实现。
DriverMangerDataSource 仅用于测试目的!
DriverManagerDataSource dataSource = new DriverManagerDataSource(); // 实际工作中不要使用这个!!! dataSource.setDriverClassName("org.hsqldb.jdbcDriver"); dataSource.setUrl("jdbc:hsqldb:hsql://localhost:"); dataSource.setUsername("sa"); dataSource.setPassword("");
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <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"/>
下例示意了针对DBCP和C3P0的基本的连接和配置:
<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"/>
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close"> <property name="driverClass" value="${jdbc.driverClassName}"/> <property name="jdbcUrl" value="${jdbc.url}"/> <property name="user" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> <context:property-placeholder location="jdbc.properties"/>
3.2、DataSourceUtils
帮助类,提供了静态方法从JNDI获取连接,以及关闭连接(如有必要)。支持线程绑定的连接,例如DataSourceTransactionManager。
3.3、SmartDataSource
扩展了DataSource接口,允许类使用它查询连接是否应该在某操作之后被关闭。如果你明确会复用一个连接,它会很有效率的。
3.4、AbstractDataSource
是Spring DataSource实现的抽象类。
3.5、SingleConnectionDataSource
是SmartDataSource的实现,封装了一个单独的Connection,每次使用都不会关闭,所以不具备多线程功能。
测试类。
3.6、DriverManagerDataSource
是标准DataSource的实现,通过bean properties配置简单的JDBC驱动,每次返回一个新的Connection!
在测试情况以及Java EE容器之外的环境下很有用。 -- 不要在生产环境中使用!
3.7、TransactionAwareDataSourceProxy
是对目标DataSource的代理,封装了目标DataSource to add awareness of Spring-managed transactions。从这个角度看,类似于一个Java EE服务器提供的事务性的JNDIDataSource。
-- 很少很少用到这个类!
3.8、DataSourceTransactionManager
是PlatformTransactionManager的单JDBC datasources的实现。它将一个特定data source的JDBC连接绑定到当前执行的线程,允许每个data source一个线程。
需要代码使用DataSourceUtils.getConnection(DataSource)代替DataSource.getConnection来获取JDBC连接。它会抛出unchecked org.springframework.dao 异常,而不是SQLExceptions。所有的框架类如JdbcTemplate都隐式的使用这种策略。
DataSourceTransactionManager 支持自定义隔离级别、超时时间(用于每个JDBC语句查询)。为了支持后者,代码中应该使用JdbcTemplate或者DataSourceUtils.applyTransactionTimeout(..)方法来为每个创建的语句设置超时时间。
该实现可以替代JtaTransactionManager -- 在单资源的情况下,因为此时不需要容器支持JTA。二者的切换仅仅是配置的问题。
3.9、NativeJdbcExtractor
有时候,你需要访问供应商独有的JDBC方法,而非标准JDBC API。这时你可以使用NativeJdbcExtractor配置你的JdbcTemplate或者OracleLobHandler。
NativeJdbcExtractor 有不同变体:
- SimpleNativeJdbcExtractor
- C3P0NativeJdbcExtractor
- CommonsDbcpNativeJdbcExtractor
- JBossNativeJdbcExtractor
- WebLogicNativeJdbcExtractor
- WebSphereNativeJdbcExtractor
- XAPoolNativeJdbcExtractor
多数情况下,针对为封装的Connection对象,只需要使用SimpleNativeJdbcExtractor。
4、JDBC 批处理操作
当你的批处理需要调用相同的预处理语句时,多数JDBC驱动提供了性能改进。
4.1、使用JdbcTemplate进行基本的批处理操作
JdbcTemplate批处理需要实现BatchPreparedStatementSetter接口的两个方法,并将其作为第二个参数传入你的batchUpdate(..)方法。使用getBatchSize()方法来提供当前batch的尺寸。使用setValues(..)方法来为预处理语句的参数设置。 该方法会被调用n次 -- 你在getBatchSize()方法中设置的次数。例子:
public class JdbcActorDao implements ActorDao { private JdbcTemplate jdbcTemplate; public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); } public int[] batchUpdate(final List<Actor> actors) { int[] updateCounts = jdbcTemplate.batchUpdate("update t_actor set first_name = ?, " + "last_name = ? where id = ?", new BatchPreparedStatementSetter() { public void setValues(PreparedStatement ps, int i) throws SQLException { ps.setString(1, actors.get(i).getFirstName()); ps.setString(2, actors.get(i).getLastName()); ps.setLong(3, actors.get(i).getId().longValue()); } public int getBatchSize() { return actors.size(); } }); return updateCounts; } // ... additional methods }
如果你在处理一个更新stream或者从文件中读取的stream,可能会预先设置一个batch size,但最后的batch可能不会满足batch size。这种情况下,可以使用InterruptibleBatchPreparedStatementSetter接口,其isBatchExhausted()方法允许你判断批处理的结束。