使用JDBC进行数据访问
前言
需要引入Spring JDBC模块
<!--Spring JDBC--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${spring.version}</version> </dependency>
一、选择JDBC数据库访问方式
- JdbcTemplate是经典的也是最流行的 Spring JDBC方法。
- NamedParameterJdbcTemplate包装了一个JdbcTemplate来提供命名参数,而不是传统的JDBC?占位符。当一个SQL语句有多个参数时,这种方法提供了更好的文档和易用性。
- SimpleJdbcInsert 和 SimpleJdbcCall 将优化数据库元数据,以限制必要的配置量。这种方法简化了编码,因此只需提供表或过程的名称,并提供与列名匹配的参数映射。只有当数据库提供足够的元数据时,这才有效。如果数据库不提供此元数据,则必须提供参数的显式配置。
- RDBMS对象,包括MappingSqlQuery、SqlUpdate和StoredProcedure,要求您在初始化数据访问层时创建可重用的线程安全对象。这种方法是在JDO Query之后建模的,在JDO Query中定义查询字符串、声明参数并编译查询。一旦您这样做了,execute方法就可以用不同的参数值被多次调用。
二、Spring JDBC包目录
Spring框架的JDBC模块由四个不同的包组成:
org.springframework.jdbc.core
包含JdbcTemplate类及其各种回调接口,以及各种相关类。
子包org.springframework.jdbc.core.simple包含SimpleJdbcInsert和SimpleJdbcCall类。
子包org.springframework.jdbc.core.namedparam包含NamedParameterJdbcTemplate和相关的支持类。
org.springframework.jdbc.datasource
包含一个用于轻松访问数据源的实用程序类和各种简单的数据源实现,您可以使用这些实现在javaee容器外测试和运行未修改的JDBC代码。
子包org.springframework.jdbc.datasource.embedded提供对使用Java数据库引擎(如HSQL、H2和Derby)创建嵌入式数据库的支持。
org.springframework.jdbc.object
包含将RDBMS查询、更新和存储过程表示为线程安全、可重用的对象的类。
org.springframework.jdbc.support
提供了SQLException转换功能和一些实用程序类。在JDBC处理期间抛出的异常被定义在org.springframework.dao(在spring-tx中)。这意味着使用Spring JDBC抽象层的代码不需要实现JDBC或RDBMS特定的错误处理。
三、使用JDBC核心类控制基本的JDBC处理和错误处理
本小节介绍如何使用JDBC核心类来控制基本的JDBC处理,包括错误处理。
使用JdbcTemplate
JdbcTemplate是JDBC核心包中的中心类。它处理资源的创建和释放,这有助于避免常见错误,例如忘记关闭连接。它执行核心JDBC的基本任务(例如语句创建和执行),让应用程序代码提供SQL和提取结果。
- 运行SQL查询
- 更新语句和存储过程调用
- 对ResultSet实例执行迭代并提取返回的参数值。
- 捕获JDBC异常并将其转换为在org.springframework.dao
创建JdbcTemplate只需要将DataSource提供它就行。
下面提供了一些JdbcTemplate用法的示例:
查询(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);
查找并填充单个实体对象
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匿名内部类中的一个,并将它们提取到单个类(通常是静态嵌套类),然后DAO方法可以根据需要引用该类。例如,最好按以下方式编写前面的代码片段:
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; } }
更新(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,该方法通常用于DDL语句。它有很多重载方法,这些变量采用回调接口、绑定变量数组等。
创建一个表:
this.jdbcTemplate.execute("create table mytable (id integer, name varchar(100))");
调用存储过程
this.jdbcTemplate.update( "call SUPPORT.REFRESH_ACTORS_SUMMARY(?)", Long.valueOf(unionId));
JdbcTemplate实践
JdbcTemplate类的实例在配置后是线程安全的。这一点很重要,因为这意味着您可以配置JdbcTemplate的单个实例,然后将此共享引用安全地注入到多个dao中。JdbcTemplate是有状态的,因为它维护对数据源的引用,但这种状态不是会话状态。
使用JdbcTemplate类(以及相关联的NamedParameterJdbcTemplate类)的一个常见做法是在Spring配置文件中配置一个数据源,然后依赖关系将该共享数据源bean注入到DAO类中。JdbcTemplate是在数据源的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... }
以下示例显示了相应的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 https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://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注解类(这使得它成为组件扫描的候选对象),并用@Autowired为DataSource setter方法添加注解。
@Repository public class JdbcCorporateEventDao implements CorporateEventDao { @Autowired private JdbcTemplate jdbcTemplate; }
如果您使用Spring的JdbcDaoSupport 类,并且您的各种JDBC支持的DAO类都是从它扩展的,那么您的子类从JdbcDaoSupport 类继承一个setDataSource(..)方法。您可以选择是否从此类继承。JdbcDaoSupport类只是为了方便起见而提供的。
无论您选择使用上述哪种模板初始化样式,每次运行SQL时都很少需要创建JdbcTemplate类的新实例。一旦配置好,JdbcTemplate实例就是线程安全的。如果您的应用程序访问多个数据库,那么您可能需要多个JdbcTemplate实例,这需要多个数据源,并且随后需要多个不同配置的JdbcTemplate实例。
使用NamedParameterJdbcTemplate
NamedParameterJdbcTemplate类添加了对使用命名参数编程的JDBC语句的支持(这也是它与JdbcTemplate最大的区别),而不是仅使用经典占位符('?')。NamedParameterJdbcTemplate类包装了一个JdbcTemplate并委托给包装好的JdbcTemplate来完成它的大部分工作。
基于MapSqlParameterSource的NamedParameterJdbcTemplate
@Autowired private NamedParameterJdbcTemplate namedParameterJdbcTemplate; public int countOfActorsByFirstName(String firstName) { String sql = "select count(*) from T_ACTOR where first_name = :first_name"; SqlParameterSource namedParameters = new MapSqlParameterSource("first_name", firstName); return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class); }
基于类型Map的NamedParameterJdbcTemplate
@Autowired private NamedParameterJdbcTemplate namedParameterJdbcTemplate; 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); return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class); }
与NamedParameterJdbcTemplate(存在于同一个Java包中)相关的一个很好的特性是SqlParameterSource接口。SqlParameterSource是NamedParameterJdbcTemplate的命名参数值源。MapSqlParameterSource类是一个简单的实现,它是一个java.util.Map,其中键是参数名,值是参数值。
下面的示例是一个典型的JavaBean:
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... }
以下示例使用NamedParameterJdbcTemplate返回JavaBean中的成员计数:
@Autowired private NamedParameterJdbcTemplate namedParameterJdbcTemplate; 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); }
记住NamedParameterJdbcTemplate类包装了一个经典的JdbcTemplate模板。如果您需要访问包装好的JdbcTemplate实例来访问只存在于JdbcTemplate类中的功能,那么可以使用getJdbcOperations()方法通过JdbcOperations接口访问包装好的JdbcTemplate。
使用SQLExceptionTranslator
SQLExceptionTranslator是一个接口,实现它的类可以在SQLExceptions和Spring自己的类之间进行转换org.springframework.dao.DataAccessException。
SQLErrorCodeSQLExceptionTranslator是默认情况下使用的SQLExceptionTranslator的实现。此实现使用特定的供应商代码。它比SQLState实现更精确。
基于JavaBean类型的名为SQLErrorCodes的错误代码类是由SQLErrorCodesFactory创建并填充,它是一个根据名为sql-error-codes.xml的配置文件的内容创建SQLErrorCodes的工厂。xml文件由供应商代码填充,并基于从DatabaseMetaData获取的DatabaseProductName。将使用您正在使用的实际数据库的代码。
SQLErrorCodeSQLExceptionTranslator按以下顺序应用匹配规则:
- 由子类实现的任何自定义转换。通常,使用提供的具体SQLErrorCodeSQLExceptionTranslator,因此此规则不适用。它只适用于实际提供了子类实现的情况。
- 作为SQLErrorCodes类的customSqlExceptionTranslator属性提供的SQLExceptionTranslator接口的任何自定义实现。
- 将搜索CustomSQLErrorCodesTranslation类(为SQLErrorCodes类的customTranslations属性提供)的实例列表以查找匹配项。
- 应用错误代码匹配。
- 使用回退转换器。SQLExceptionSubclassTranslator是默认的回退转换器。如果此转换不可用,下一个后备转换器是SQLStateSQLExceptionTranslator。
你可以扩展SQLErrorCodeSQLExceptionTranslator,如下例所示:
public class CustomSQLErrorCodesTranslator extends SQLErrorCodeSQLExceptionTranslator { protected DataAccessException customTranslate(String task, String sql, SQLException sqlex) { if (sqlex.getErrorCode() == -12345) { return new DeadlockLoserDataAccessException(task, sqlex); } return null; } }
在前面的例子中,特定的错误代码(-12345)被转换,而其他错误则由默认的转换器实现来转换。要使用此自定义转换器,必须通过setExceptionTranslator方法将其传递给JdbcTemplate,并且必须将此JdbcTemplate用于需要此转换器的所有数据访问处理。以下示例显示如何使用此自定义转换器:
private JdbcTemplate jdbcTemplate; public void setDataSource(DataSource dataSource) { // create a JdbcTemplate and set data source this.jdbcTemplate = new JdbcTemplate(); this.jdbcTemplate.setDataSource(dataSource); // create a custom translator and set the DataSource for the default translation lookup CustomSQLErrorCodesTranslator tr = new CustomSQLErrorCodesTranslator(); tr.setDataSource(dataSource); this.jdbcTemplate.setExceptionTranslator(tr); } public void updateShippingCharge(long orderId, long pct) { // use the prepared JdbcTemplate for this update this.jdbcTemplate.update("update orders" + " set shipping_charge = shipping_charge * ? / 100" + " where id = ?", pct, orderId); }
获取自增的ID
update()方法支持检索数据库生成的主键。这种支持是jdbc3.0标准的一部分。该方法将PreparedStatementCreator作为其第一个参数,这是指定所需insert语句的方式。另一个参数是KeyHolder,它包含从更新成功返回时生成的主键。以下示例适用于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
四、控制数据库连接
使用DriverManagerDataSource
Spring通过数据源获得到数据库的连接。数据源是JDBC规范的一部分,是一个通用的连接工厂。应用程序池管理允许应用程序对事务池问题和代码池进行隐藏。
当你使用Spring的JDBC层时,你可以从JNDI获取数据源,也可以使用第三方提供的连接池实现来配置自己的数据源。流行的实现是DBCP和C3P0。Spring发行版中的实现仅用于测试目的,不提供池。
配置DriverMangerDataSource如下:
DriverManagerDataSource dataSource = new DriverManagerDataSource(); dataSource.setDriverClassName("org.hsqldb.jdbcDriver"); dataSource.setUrl("jdbc:hsqldb:hsql://localhost:"); dataSource.setUsername("sa"); dataSource.setPassword("");
XML的配置如下:
<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的基本连接和配置
DBCP配置:
<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"/>
C3P0配置:
<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"/>
使用DataSourceUtils
DataSourceUtils类是一个方便而强大的辅助类,它提供静态方法来从JNDI获取连接,并在必要时关闭连接。它支持线程绑定连接,例如DataSourceTransactionManager。
实现SmartDataSource
SmartDataSource接口应该由可以提供到关系数据库的连接的类来实现。它扩展了DataSource接口。
继承AbstractDataSource
AbstractDataSource是Spring数据源实现的一个抽象基类。它实现了所有数据源实现通用的代码。如果编写自己的数据源实现,则应该扩展AbstractDataSource类。
使用SingleConnectionDataSource
SingleConnectionDataSource类是SmartDataSource接口的实现,它包装了一个在每次使用后未关闭的连接。这不支持多线程。
如果任何客户端代码在假定池连接的情况下调用close(如使用持久性工具时),则应将suppressClose属性设置为true。此设置返回包装物理连接的关闭禁止代理。请注意,你不能再将其强制转换为本机Oracle连接或类似对象。
SingleConnectionDataSource主要是一个测试类。例如,它与一个简单的JNDI环境结合使用,可以在应用服务器外部轻松测试代码。与DriverManager DataSource不同,它始终重用相同的连接,避免过度创建物理连接。
使用DriverManagerDataSource
DriverManagerDataSource类是标准数据源接口的实现,该接口通过bean属性配置普通JDBC驱动程序,并每次返回一个新连接。
这个实现对于java EE容器之外的测试和独立环境非常有用,可以作为spring IoC容器中的数据源bean,也可以与简单的JNDI环境结合使用。任何支持数据源的持久性代码都可以工作。使用JavaBean风格的连接池(比如commons-dbcp)非常容易,即使在测试环境中,使用这样的连接池几乎总是优于DriverManagerDataSource。
使用TransactionAwareDataSourceProxy
TransactionWareDatasourceProxy是目标数据源的代理。代理将目标数据源包装起来,以增加对Spring管理事务的感知。在这方面,它类似于事务性JNDI数据源,由javaEE服务器提供。
很少需要使用这个类,除非已经存在的代码必须被调用并传递给标准的JDBC数据源接口实现。在这种情况下,你仍然可以使用这些代码,同时让这些代码参与Spring管理的事务。通常情况下,最好使用更高级别的资源管理抽象来编写自己的新代码,例如JdbcTemplate或DataSourceUtils。
使用DataSourceTransactionManager
DataSourceTransactionManager类是单个JDBC数据源的PlatformTransactionManager实现。它将指定数据源的JDBC连接绑定到当前正在执行的线程,可能允许每个数据源有一个线程连接。
通过检索JDBC连接需要应用程序代码DataSourceUtils.getConnection(数据源)而不是javaEE的标准DataSource.getConnection。它不受约束地抛出org.springframework.dao异常而不是选中的SQLExceptions。所有的框架类(比如JdbcTemplate)都隐式地使用这个策略。如果不与此事务管理器一起使用,则查找策略的行为与普通策略完全相同。因此,它可以在任何情况下使用。
DataSourceTransactionManager类支持自定义的隔离级别和超时,这些级别和超时将应用于适当的JDBC语句查询超时。为了支持后者,应用程序代码必须使用JdbcTemplate或调用DataSourceUtils.applyTransactionTimeout(..)方法。
在单数据源情况下,可以使用此实现而不是JtaTransactionManager,因为它不需要容器支持JTA。如果您坚持所需的连接查找模式,那么在这两者之间切换只是一个配置问题。JTA不支持自定义隔离级别。
五、JDBC批量操作
如果你将多个调用批处理到同一个准备好的语句中,大多数JDBC驱动程序都可以提高性能。通过将更新分组为批,可以限制到数据库的往返次数。
JdbcTemplate批量操作
通过实现BatchPreparedStatementSetter的两个方法,并将该实现作为batchUpdate方法调用中的第二个参数传入,从而完成JdbcTemplate批处理。可以使用getBatchSize方法提供当前批的大小。可以使用setValues方法为准备好的语句的参数设置值。调用此方法的次数是在getBatchSize调用中指定的次数。以下示例基于列表中的条目更新actor表,整个列表用作批处理:
public class JdbcActorDao implements ActorDao { @Autowired private JdbcTemplate jdbcTemplate; public int[] batchUpdate(final List<Actor> actors) { return this.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(); } }); } // ... additional methods }
如果处理更新流或从文件中读取数据流,则可能有一个处理大小,但最后一批可能没有该数量的条目。在本例中,你可以使用InterruptibleBatchPreparedStatementSetter接口,该接口允许你在输入源耗尽后中断批处理。isBatchExhausted方法允许你发出批处理结束的信号。
对象列表的批处理
JdbcTemplate和NamedParameterJdbcTemplate都提供了提供批处理更新的另一种方法。不是实现特殊的批处理接口,而是以列表的形式提供调用中的所有参数值。框架循环这些值并使用内部准备好的语句设置器。API会有所不同,具体取决于你是否使用命名参数。对于命名参数,你提供一个SqlParameterSource数组,批处理的每个成员一个条目。你可以使用SqlParameterSourceUtils.createBatch创建这个数组的方便方法,传入一个bean风格的对象数组(getter方法对应的参数)、key为字符串的Map实例(相应的参数作为值),或者两者的混合。
以下示例显示了使用命名参数的批处理更新:
public class JdbcActorDao implements ActorDao { @Autowired private NamedParameterTemplate namedParameterJdbcTemplate; public int[] batchUpdate(List<Actor> actors) { return this.namedParameterJdbcTemplate.batchUpdate( "update t_actor set first_name = :firstName, last_name = :lastName where id = :id", SqlParameterSourceUtils.createBatch(actors)); } // ... additional methods }
对于使用经典语句的SQL语句?占位符,则传入一个包含更新值的对象数组的列表。对于SQL语句中的每个占位符,此对象数组必须有一个条目,并且它们的顺序必须与在SQL语句中定义的顺序相同。
下面的示例与前面的示例相同,只是它使用了经典的JDBC?占位符:
public class JdbcActorDao implements ActorDao { @Autowired private JdbcTemplate jdbcTemplate; public int[] batchUpdate(final List<Actor> actors) { List<Object[]> batch = new ArrayList<Object[]>(); for (Actor actor : actors) { Object[] values = new Object[] { actor.getFirstName(), actor.getLastName(), actor.getId()}; batch.add(values); } return this.jdbcTemplate.batchUpdate( "update t_actor set first_name = ?, last_name = ? where id = ?", batch); } // ... additional methods }
我们前面描述的所有批处理更新方法都返回一个int数组,其中包含每个批处理条目的受影响行数。此计数由JDBC驱动程序报告。如果计数不可用,JDBC驱动程序将返回一个值-2。
具有多个批次的批处理操作
前面的批处理更新示例每一批处理的量太大,以至于需要将它们拆分为几个较小的批操作。你可以通过多次调用batchUpdate方法来使用前面提到的方法来实现这一点,但是现在有了一个更方便的方法。除了SQL语句外,此方法还需要一个包含参数的对象集合、每个批处理要进行的更新次数以及一个ParameterizedPreparedStatementSetter 来设置准备语句的参数值。框架循环提供的值,并将更新调用分成指定大小的批。
以下示例显示了使用批大小为100的批处理更新:
public class JdbcActorDao implements ActorDao { @Autowired private JdbcTemplate jdbcTemplate; public int[][] batchUpdate(final Collection<Actor> actors) { int[][] updateCounts = jdbcTemplate.batchUpdate( "update t_actor set first_name = ?, last_name = ? where id = ?", actors, 100, new ParameterizedPreparedStatementSetter<Actor>() { public void setValues(PreparedStatement ps, Actor argument) throws SQLException { ps.setString(1, argument.getFirstName()); ps.setString(2, argument.getLastName()); ps.setLong(3, argument.getId().longValue()); } }); return updateCounts; } // ... additional methods }
此调用的批处理更新方法返回一个int数组,其中包含每个批处理的数组项,以及每次更新受影响行数的数组。第一级数组的长度表示执行的批处理数,第二级数组的长度表示该批处理中的更新次数。每个批处理中的更新数量应为为所有批处理提供的批大小(最后一个可能较少的批除外),具体取决于提供的更新对象的总数。每个update语句的更新计数是JDBC驱动程序报告的。如果计数不可用,JDBC驱动程序将返回一个值-2。
六、用SimpleJdbc类简化JDBC操作
SimpleJdbcInsert和SimpleJdbcCall类通过利用JDBC驱动程序检索的数据库元数据来提供简化的配置。
使用SimpleJdbcInsert插入数据
SimpleJdbcInsert包含最少的配置选项。你应该在数据访问层的初始化方法中实例化SimpleJdbcInsert。对于本例,初始化方法是setDataSource方法。你不需要将SimpleJdbcInsert类的子类化。相反,您你以创建一个新实例并使用withTableName方法设置表名。此类的配置方法遵循simplejbcinsert实例的链式操作,允许你链式调用所有配置方法。以下示例仅使用一种配置方法:
public class JdbcActorDao implements ActorDao { private JdbcTemplate jdbcTemplate; private SimpleJdbcInsert insertActor; public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); this.insertActor = new SimpleJdbcInsert(dataSource).withTableName("t_actor"); } public void add(Actor actor) { Map<String, Object> parameters = new HashMap<String, Object>(3); parameters.put("id", actor.getId()); parameters.put("first_name", actor.getFirstName()); parameters.put("last_name", actor.getLastName()); insertActor.execute(parameters); } // ... additional methods }
这里使用的execute方法需要一个java.util.Map作为它唯一的参数。这里需要注意的一点是,用于映射的键必须与数据库中定义的表的列名相匹配。这是因为我们读取元数据来构造实际的insert语句。
使用SimpleJdbcInsert获取自增ID
这个示例使用与前一个示例相同的insert,但是它没有传递id,而是检索自动生成的键并将其设置在新的Actor对象上。当它创建SimpleJdbcInsert时,除了指定表名之外,它还使用usingGeneratedKeyColumns方法指定生成的键列的名称。
public class JdbcActorDao implements ActorDao { private JdbcTemplate jdbcTemplate; private SimpleJdbcInsert insertActor; public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); this.insertActor = new SimpleJdbcInsert(dataSource) .withTableName("t_actor") .usingGeneratedKeyColumns("id"); } public void add(Actor actor) { Map<String, Object> parameters = new HashMap<String, Object>(2); parameters.put("first_name", actor.getFirstName()); parameters.put("last_name", actor.getLastName()); Number newId = insertActor.executeAndReturnKey(parameters); actor.setId(newId.longValue()); } // ... additional methods }
当使用第二种方法运行insert时,主要的区别是不向Map实例添加id,而是调用executeAndReturnKey方法。
这将返回一个包含数值类型的java.lang.Number对象实例。你不能依赖所有数据库来返回特定的Java类。java.lang.Number是你可以依赖的基类。如果有多个自动生成的列或生成的值不是数字,则可以使用从executeAndReturnKeyHolder方法返回的KeyHolder。
为SimpleJdbcInsert指定列
可以通过使用usingColumns方法指定列名列表来限制插入的列,如下例所示:
public class JdbcActorDao implements ActorDao { private JdbcTemplate jdbcTemplate; private SimpleJdbcInsert insertActor; public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); this.insertActor = new SimpleJdbcInsert(dataSource) .withTableName("t_actor") .usingColumns("first_name", "last_name") .usingGeneratedKeyColumns("id"); } public void add(Actor actor) { Map<String, Object> parameters = new HashMap<String, Object>(2); parameters.put("first_name", actor.getFirstName()); parameters.put("last_name", actor.getLastName()); Number newId = insertActor.executeAndReturnKey(parameters); actor.setId(newId.longValue()); } // ... additional methods }
使用SqlParameterSource提供参数值
使用Map来提供参数值很好,但它不是使用最方便的类。Spring提供了SqlParameterSource接口的两个实现,你可以改用它们。第一个是BeanPropertySqlParameterSource,如果你有一个JavaBean兼容的类包含你的值,那么这是一个非常方便的类。它使用相应的getter方法来提取参数值。以下示例演示如何使用BeanPropertySqlParameterSource:
public class JdbcActorDao implements ActorDao { private JdbcTemplate jdbcTemplate; private SimpleJdbcInsert insertActor; public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); this.insertActor = new SimpleJdbcInsert(dataSource) .withTableName("t_actor") .usingGeneratedKeyColumns("id"); } public void add(Actor actor) { SqlParameterSource parameters = new BeanPropertySqlParameterSource(actor); Number newId = insertActor.executeAndReturnKey(parameters); actor.setId(newId.longValue()); } // ... additional methods }
另一个选项是MapSqlParameterSource,它类似于Map,但提供了一个更方便的addValue方法,可以进行链式调用。
public class JdbcActorDao implements ActorDao { private JdbcTemplate jdbcTemplate; private SimpleJdbcInsert insertActor; public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); this.insertActor = new SimpleJdbcInsert(dataSource) .withTableName("t_actor") .usingGeneratedKeyColumns("id"); } public void add(Actor actor) { SqlParameterSource parameters = new MapSqlParameterSource() .addValue("first_name", actor.getFirstName()) .addValue("last_name", actor.getLastName()); Number newId = insertActor.executeAndReturnKey(parameters); actor.setId(newId.longValue()); } // ... additional methods }
使用SimpleJdbcCall调用存储过程
SimpleJdbcCall类使用数据库中的元数据来查找输入和输出参数的名称,这样就不必显式地声明它们。如果您愿意声明参数,或者您的参数(如数组或结构)没有自动映射到Java类,则可以声明参数。示例是一个简单的过程,它只从MySQL数据库返回VARCHAR和DATE格式的标量值。示例过程读取指定的actor条目,并以out参数的形式返回first_name、last_name和birth_date列。
CREATE PROCEDURE read_actor ( IN in_id INTEGER, OUT out_first_name VARCHAR(100), OUT out_last_name VARCHAR(100), OUT out_birth_date DATE) BEGIN SELECT first_name, last_name, birth_date INTO out_first_name, out_last_name, out_birth_date FROM t_actor where id = in_id; END;
in_id 包含你要查找的id,out参数返回从表中读取的数据。
你可以用类似于声明SimpleJdbcInsert的方式声明SimpleJdbcCall。你应该在数据访问层的初始化方法中实例化和配置类。与StoredProcedure类相比,你不需要创建子类,也不需要声明可以在数据库元数据中查找的参数。
下面的SimpleJDBCall配置示例使用前面的存储过程(除了数据源之外,唯一的配置选项是存储过程的名称):
public class JdbcActorDao implements ActorDao { private JdbcTemplate jdbcTemplate; private SimpleJdbcCall procReadActor; public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); this.procReadActor = new SimpleJdbcCall(dataSource) .withProcedureName("read_actor"); } public Actor readActor(Long id) { SqlParameterSource in = new MapSqlParameterSource() .addValue("in_id", id); Map out = procReadActor.execute(in); Actor actor = new Actor(); actor.setId(id); actor.setFirstName((String) out.get("out_first_name")); actor.setLastName((String) out.get("out_last_name")); actor.setBirthDate((Date) out.get("out_birth_date")); return actor; } // ... additional methods }
提示:这里就先简单介绍下,更加详细的内容可以参考官方文档(非不得已的情况下建议不要使用存储过程)。
如何定义SqlParameters
要为SimpleJdbc类和RDBMS操作类定义参数,可以使用SqlParameter或其子类之一。为此,通常在构造函数中指定参数名和SQL类型。SQL类型是通过使用java.sql.Types常量。
new SqlParameter("in_id", Types.NUMERIC), new SqlOutParameter("out_first_name", Types.VARCHAR),
七、将JDBC操作建模为Java对象
org.springframework.jdbc.object包含你以更面向对象的方式访问数据库的类。例如,您可以执行查询并将结果作为一个列表返回,该列表包含业务对象,列数据映射到业务对象的属性。你还可以运行存储过程和运行update、delete和insert语句。
许多Spring开发人员认为直接用JdbcTemplate来编写一个DAO方法更方便(除非你认为有用,可以去了解下)。
了解SqlQuery
SqlQuery是一个可重用的线程安全类,它封装了SQL查询。子类必须实现newRowMapper(..)方法,以提供一个RowMapper实例,该实例可以为每行创建一个对象,该对象是通过迭代执行查询期间创建的ResultSet获得的。SqlQuery类很少直接使用,因为MappingSqlQuery子类为将行映射到Java类提供了更方便的实现。扩展SqlQuery的其他实现包括MappingSqlQueryWithParameters和UpdatableSqlQuery。
八、参数和数据值处理的常见问题
在Spring框架的JDBC所提供的不同方法中,存在参数和数据值的常见问题。本小节介绍如何解决这些问题。
为参数提供SQL类型信息
通常,Spring根据传入的参数类型确定参数的SQL类型。可以显式地提供设置参数值时要使用的SQL类型。这有时是正确设置空值所必需的。
您可以通过多种方式提供SQL类型信息:
- 可以使用SqlParameterValue类包装需要此附加信息的参数值。为此,为每个值创建一个新实例,并在构造函数中传入SQL类型和参数值。也可以为数值提供可选的缩放参数。
- 对于使用命名参数的方法,可以使用SqlParameterSource类、BeanPropertySqlParameterSource或MapSqlParameterSource。为这两个方法注册的SQL参数的任何类型的值。
处理BLOB和CLOB对象
你可以在数据库中存储图像、其他二进制数据和大块文本。这些大对象对于二进制数据称为blob(二进制大对象),对于字符数据称为clob(字符大对象)。在Spring中,您可以通过直接使用JdbcTemplate来处理这些大型对象,也可以在使用RDBMS对象和SimpleJdbc类提供的高级抽象时处理这些大对象。所有这些方法都使用LobHandler接口的实现来实际管理LOB(大对象)数据。LobHandler通过getLobCreator方法提供对LobCreator类的访问,该类用于创建要插入的新LOB对象。
LobCreator和LobHandler为LOB输入和输出提供以下支持:
- BLOB
- byte[]: getBlobAsBytes 和 setBlobAsBytes
- InputStream: getBlobAsBinaryStream 和 setBlobAsBinaryStream
- CLOB
- String: getClobAsString 和 setClobAsString
- InputStream: getClobAsAsciiStream 和 setClobAsAsciiStream
- Reader: getClobAsCharacterStream 和 setClobAsCharacterStream
下一个示例演示如何创建和插入BLOB。稍后我们将演示如何从数据库中读回它。
此示例使用JdbcTemplate和AbstractLobCreatingPreparedStatementCallback的实现。它实现了一个方法setValues。此方法提供了一个LobCreator,用于设置sqlinsert语句中LOB列的值。
对于本例,我们假设存在一个变量lobHandler,它已经设置为DefaultLobHandler的实例。通常通过依赖注入来设置该值(当然也可以直接new DefaultLobHandler())。
以下示例演示如何创建和插入BLOB:
final File blobIn = new File("spring2004.jpg"); final InputStream blobIs = new FileInputStream(blobIn); final File clobIn = new File("large.txt"); final InputStream clobIs = new FileInputStream(clobIn); final InputStreamReader clobReader = new InputStreamReader(clobIs); jdbcTemplate.execute( "INSERT INTO lob_table (id, a_clob, a_blob) VALUES (?, ?, ?)", new AbstractLobCreatingPreparedStatementCallback(lobHandler) { protected void setValues(PreparedStatement ps, LobCreator lobCreator) throws SQLException { ps.setLong(1, 1L); lobCreator.setClobAsCharacterStream(ps, 2, clobReader, (int)clobIn.length()); lobCreator.setBlobAsBinaryStream(ps, 3, blobIs, (int)blobIn.length()); } } ); blobIs.close(); clobReader.close();
传入lobHandler(在本例中)是一个普通的DefaultLobHandler。
使用方法setClobAsCharacterStream传入CLOB的内容。
使用方法setBlobAsBinaryStream传入BLOB的内容。
如果对DefaultLobHandler.getLobCreator()返回的LobCreator调用setBlobAsBinaryStream、SetClobasAsiIStream或setClobAsCharacterStream方法,你可以选择为contentLength参数指定负值。如果指定的内容长度为负,则DefaultLobHandler将使用不带长度参数的set stream方法的jdbc4.0变体。否则,它将指定的长度传递给驱动程序。
从数据库读取LOB数据同样使用具有相同实例变量lobHandler和对DefaultLobHandler的引用的JdbcTemplate。下面的示例演示如何执行此操作:
List<Map<String, Object>> l = jdbcTemplate.query("select id, a_clob, a_blob from lob_table", new RowMapper<Map<String, Object>>() { public Map<String, Object> mapRow(ResultSet rs, int i) throws SQLException { Map<String, Object> results = new HashMap<String, Object>(); String clobText = lobHandler.getClobAsString(rs, "a_clob"); results.put("CLOB", clobText); byte[] blobBytes = lobHandler.getBlobAsBytes(rs, "a_blob"); results.put("BLOB", blobBytes); return results; } });
使用getClobAsString方法检索CLOB的内容。
使用getBlobAsBytes方法检索BLOB的内容。
提示:clob和blob字段
(1) 不同数据库中对应clob,blob的类型如下:
MySQL中:clob对应text,blob对应blob
DB2/Oracle中:clob对应clob,blob对应blob
(2) domain中对应的类型:
clob对应String,blob对应byte[]
clob对应java.sql.Clob,blob对应java.sql.Blob
(3) hibernate配置文件中对应类型:
clob-->clob ,blob-->binary
也可以直接使用数据库提供类型,例如:oracle.sql.Clob,oracle.sql.Blob
传入in子句的值列表
SQL标准允许根据包含变量值列表的表达式选择行。一个典型的例子是select * from T_ACTOR where id in(1,2,3)。JDBC标准对准备好的语句不直接支持此变量列表。不能声明可变数量的占位符。您需要一系列具有所需数量占位符的变体,或者需要在知道需要多少占位符后动态生成SQL字符串。NamedParameterJdbcTemplate和JdbcTemplate中提供的命名参数支持采用后一种方法。可以将值作为java.util.List基本体对象。此列表用于在语句执行期间插入所需的占位符并传入值。
除了值列表中的基本值外,还可以创建java.util.List对象数组的。此列表可以支持为in子句定义多个表达式,例如 select * from T_ACTOR where(id,last_name)in((1,'Johnson'),(2,'Harrop'\))。当然,这需要你的数据库支持此语法。
传递许多值时要小心。JDBC标准并不保证可以为in-expression列表使用100个以上的值。各种数据库都超过了这个数字,但它们通常对允许的值数量有一个硬限制。例如,Oracle的限制是1000。
处理存储过程调用的复杂类型
九、嵌入式数据库支持
org.springframework.jdbc.datasource.embedded提供对嵌入式Java数据库引擎的支持。对HSQL、H2和Derby的支持是本机提供的。您还可以使用可扩展的API来插入新的嵌入式数据库类型和数据源实现。
为什么使用嵌入式数据库
嵌入式数据库在项目的开发阶段非常有用,因为它具有轻量级的特性。其优点包括易于配置、快速启动、可测试性以及在开发过程中快速改进SQL的能力。
使用spring xml创建嵌入式数据库
如果要在Spring ApplicationContext中将嵌入式数据库实例公开为bean,可以使用Spring jdbc命名空间中的embedded database标记:
<jdbc:embedded-database id="dataSource" generate-name="true"> <jdbc:script location="classpath:schema.sql"/> <jdbc:script location="classpath:test-data.sql"/> </jdbc:embedded-database>
前面的配置创建了一个嵌入的HSQL数据库,该数据库由类路径下的schema.sql和test-data.sql 创建,同时为嵌入式数据库分配一个唯一生成的名称。嵌入式数据库被放到Spring IoC容器中,可以根据需要注入到数据访问对象中。
以编程方式创建嵌入式数据库
EmbeddedDatabaseBuilder类为以编程方式构造嵌入式数据库提供了一个流畅的API。当需要在独立环境或独立集成测试中创建嵌入式数据库时,可以使用此选项,如示例所示:
EmbeddedDatabase db = new EmbeddedDatabaseBuilder() .generateUniqueName(true) .setType(H2) .setScriptEncoding("UTF-8") .ignoreFailedDrops(true) .addScript("schema.sql") .addScripts("user_data.sql", "country_data.sql") .build(); // perform actions against the db (EmbeddedDatabase extends javax.sql.DataSource) db.shutdown()
也可以使用EmbeddedDatabaseBuilder通过Java配置创建嵌入式数据库,如下例所示:
@Configuration public class DataSourceConfig { @Bean public DataSource dataSource() { return new EmbeddedDatabaseBuilder() .generateUniqueName(true) .setType(H2) .setScriptEncoding("UTF-8") .ignoreFailedDrops(true) .addScript("schema.sql") .addScripts("user_data.sql", "country_data.sql") .build(); } }
选择嵌入式数据库类型
本小节介绍如何从Spring支持的三个嵌入式数据库中选择一个。
使用HSQL
Spring支持hsql1.8.0及更高版本。如果没有显式指定类型,HSQL是默认的嵌入式数据库。要显式指定HSQL,请将嵌入式数据库标记的type属性设置为HSQL。如果使用构建器API,请使用EmbeddedDatabaseType.HSQL。
使用H2
Spring支持H2数据库。要启用H2,请将嵌入式数据库标记的type属性设置为H2。如果使用builder API,请调用setType(EmbeddedDatabaseType.H2)方法。
使用Derby
Spring支持ApacheDerby10.5及更高版本。要启用Derby,请将嵌入式数据库标记的type属性设置为Derby。如果使用构建器API,请使用EmbeddedDatabaseType.DERBY。
用嵌入式数据库测试数据访问逻辑
嵌入式数据库提供了一种测试数据访问代码的轻量级方法。当嵌入式数据库不需要跨测试类重用时,可以单独new一个实例。但是,如果希望创建在测试类中共享的嵌入式数据库,请考虑使用Spring TestContext Framework 框架,并将嵌入式数据库配置为Spring ApplicationContext中的bean。以下列表显示了测试模板:
public class DataAccessIntegrationTestTemplate { private EmbeddedDatabase db; @Before public void setUp() { // creates an HSQL in-memory database populated from default scripts // classpath:schema.sql and classpath:data.sql db = new EmbeddedDatabaseBuilder() .generateUniqueName(true) .addDefaultScripts() .build(); } @Test public void testDataAccess() { JdbcTemplate template = new JdbcTemplate(db); template.query( /* ... */ ); } @After public void tearDown() { db.shutdown(); } }
为嵌入式数据库生成唯一名称
如果开发团队的测试套件无意中尝试重新创建同一数据库的其他实例,那么在使用嵌入式数据库时经常会遇到错误。如果一个XML配置文件或@configuration类负责创建一个嵌入式数据库,然后在同一个测试套件(即在同一个JVM进程中)内的多个测试场景中重用相应的配置,则很容易发生这种情况。
这些错误的根本原因是Spring的EmbeddedDatabaseFactory(由<jdbc:embedded-database>元素和EmbeddedDatabaseBuilder配置类)将嵌入式数据库的名称设置为testdb(如果没有另外指定)。对于<jdbc:embedded-database>,通常为嵌入式数据库分配一个与bean的id相等的名称(类似于dataSource)。因此,后续创建嵌入式数据库的尝试不会产生新的数据库。相反,相同的JDBC连接URL被重用,并且尝试创建新的嵌入式数据库实际上指向有相同配置创建的现有嵌入式数据库。
为了解决这个常见问题,springframework4.2支持为嵌入式数据库生成唯一的名称。要使用生成的名称,请使用以下选项之一。
- EmbeddedDatabaseFactory.setGenerateUniqueDatabaseName()
- EmbeddedDatabaseBuilder.generateUniqueName()
- <jdbc:embedded-database generate-name="true" … >
在创建嵌入式数据库时建议生成唯一名称。
扩展嵌入式数据库支持
你可以通过两种方式扩展Spring JDBC嵌入式数据库支持:
- 实现EmbeddedDatabaseConfigurer以支持新的嵌入式数据库类型。
- 实现DataSourceFactory 以支持新的DataSource实现,例如管理嵌入数据库连接的连接池。
十、初始化数据源
org.springframework.jdbc.datasource.init包提供对已存在数据源的初始化的支持。嵌入式数据库支持为应用程序创建和初始化数据源提供了一个选项。但是,有时可能需要初始化在某个服务器上运行的实例。
使用spring xml初始化数据库
如果要初始化数据库,并且可以提供对数据源bean的引用,可以使用spring jdbc命名空间中的initialize-database标签
<jdbc:initialize-database data-source="dataSource"> <jdbc:script location="classpath:com/foo/sql/db-schema.sql"/> <jdbc:script location="classpath:com/foo/sql/db-test-data.sql"/> </jdbc:initialize-database>
前面的示例针对数据库运行两个指定的脚本。第一个脚本创建一个schema,第二个脚本是导入一些测试数据。脚本位置也可以是带有通配符的模式,这些通配符是Spring中用于资源的常用Ant样式(例如,classpath*:/com/foo/**/sql)/*-数据.sql). 如果使用模式,则脚本将按其URL或文件名的词法顺序运行。
数据库初始值设定项的默认行为是无条件运行提供的脚本。这可能并不总是你想要的。例如,如果你对已包含测试数据的数据库运行脚本。遵循先创建表然后插入数据的通用模式(如前所示),可以降低意外删除数据的可能性。如果表已经存在,第一步将失败。
但是,为了更好地控制现有数据的创建和删除,XML名称空间提供了一些附加选项。第一个是用于打开和关闭初始化的标志。你可以根据环境进行设置(例如从系统属性或环境bean中提取布尔值)。
以下示例从系统属性获取值:
<jdbc:initialize-database data-source="dataSource" enabled="#{systemProperties.INITIALIZE_DATABASE}"> <jdbc:script location="..."/> </jdbc:initialize-database>
控制初始化时执行错误时忽略一些操作
<jdbc:initialize-database data-source="dataSource" ignore-failures="DROPS"> <jdbc:script location="..."/> </jdbc:initialize-database>