7.1 概述
7.1.1 JDBC回顾
传统应用程序开发中,进行JDBC编程是相当痛苦的,如下所示:
java代码:
//cn.javass.spring.chapter7. TraditionalJdbcTest @Test public void test() throws Exception { Connection conn = null; PreparedStatement pstmt = null; try { conn = getConnection(); //1.获取JDBC连接 //2.声明SQL String sql = "select * from INFORMATION_SCHEMA.SYSTEM_TABLES"; pstmt = conn.prepareStatement(sql); //3.预编译SQL ResultSet rs = pstmt.executeQuery(); //4.执行SQL process(rs); //5.处理结果集 closeResultSet(rs); //5.释放结果集 closeStatement(pstmt); //6.释放Statement conn.commit(); //8.提交事务 } catch (Exception e) { //9.处理异常并回滚事务 conn.rollback(); throw e; } finally { //10.释放JDBC连接,防止JDBC连接不关闭造成的内存泄漏 closeConnection(conn); } }
以上代码片段具有冗长、重复、容易忘记某一步骤从而导致出错、显示控制事务、显示处理受检查异常等等。
有朋友可能重构出自己的一套JDBC模板,从而能简化日常开发,但自己开发的JDBC模板不够通用,而且对于每一套JDBC模板实现都差不多,从而导致开发人员必须掌握每一套模板。
Spring JDBC提供了一套JDBC抽象框架,用于简化JDBC开发,而且如果各个公司都使用该抽象框架,开发人员首先减少了学习成本,直接上手开发,如图7-1所示。
图7-1 Spring JDBC与传统JDBC编程对比
7.1.2 Spring对JDBC的支持
Spring通过抽象JDBC访问并提供一致的API来简化JDBC编程的工作量。我们只需要声明SQL、调用合适的Spring JDBC框架API、处理结果集即可。事务由Spring管理,并将JDBC受查异常转换为Spring一致的非受查异常,从而简化开发。
Spring主要提供JDBC模板方式、关系数据库对象化方式和SimpleJdbc方式三种方式来简化JDBC编程,这三种方式就是Spring JDBC的工作模式:
- JDBC模板方式:Spring JDBC框架提供以下几种模板类来简化JDBC编程,实现GoF模板设计模式,将可变部分和非可变部分分离,可变部分采用回调接口方式由用户来实现:如JdbcTemplate、NamedParameterJdbcTemplate、SimpleJdbcTemplate。
- 关系数据库操作对象化方式:Spring JDBC框架提供了将关系数据库操作对象化的表示形式,从而使用户可以采用面向对象编程来完成对数据库的访问;如MappingSqlQuery、SqlUpdate、SqlCall、SqlFunction、StoredProcedure等类。这些类的实现一旦建立即可重用并且是线程安全的。
- SimpleJdbc方式:Spring JDBC框架还提供了SimpleJdbc方式来简化JDBC编程,SimpleJdbcInsert 、 SimpleJdbcCall用来简化数据库表插入、存储过程或函数访问。
Spring JDBC还提供了一些强大的工具类,如DataSourceUtils来在必要的时候手工获取数据库连接等。
7.1.4 Spring的JDBC架构
Spring JDBC抽象框架由四部分组成:datasource、support、core、object。如图7-2所示。
图7-2 Spring JDBC架构图
support包:提供将JDBC异常转换为DAO非检查异常转换类、一些工具类如JdbcUtils等。
datasource包:提供简化访问JDBC 数据源(javax.sql.DataSource实现)工具类,并提供了一些DataSource简单实现类从而能使从这些DataSource获取的连接能自动得到Spring管理事务支持。
core包:提供JDBC模板类实现及可变部分的回调接口,还提供SimpleJdbcInsert等简单辅助类。
object包:提供关系数据库的对象表示形式,如MappingSqlQuery、SqlUpdate、SqlCall、SqlFunction、StoredProcedure等类,该包是基于core包JDBC模板类实现。
7.2 JDBC模板类
7.2.1 概述
Spring JDBC抽象框架core包提供了JDBC模板类,其中JdbcTemplate是core包的核心类,所以其他模板类都是基于它封装完成的,JDBC模板类是第一种工作模式。
JdbcTemplate类通过模板设计模式帮助我们消除了冗长的代码,只做需要做的事情(即可变部分),并且帮我们做哪些固定部分,如连接的创建及关闭。
JdbcTemplate类对可变部分采用回调接口方式实现,如ConnectionCallback通过回调接口返回给用户一个连接,从而可以使用该连接做任何事情、StatementCallback通过回调接口返回给用户一个Statement,从而可以使用该Statement做任何事情等等,还有其他一些回调接口如图7-3所示。
图7-3 JdbcTemplate支持的回调接口
Spring除了提供JdbcTemplate核心类,还提供了基于JdbcTemplate实现的NamedParameterJdbcTemplate类用于支持命名参数绑定、 SimpleJdbcTemplate类用于支持Java5+的可变参数及自动装箱拆箱等特性。
7.2.3 传统JDBC编程替代方案
前边我们已经使用过传统JDBC编程方式,接下来让我们看下Spring JDBC框架提供的更好的解决方案。
1)准备需要的jar包并添加到类路径中:
java代码:
//JDBC抽象框架模块 org.springframework.jdbc-3.0.5.RELEASE.jar //Spring事务管理及一致的DAO访问及非检查异常模块 org.springframework.transaction-3.0.5.RELEASE.jar //hsqldb驱动,hsqldb是一个开源的Java实现数据库,请下载hsqldb2.0.0+版本 hsqldb.jar
2)传统JDBC编程替代方案:
在使用JdbcTemplate模板类时必须通过DataSource获取数据库连接,Spring JDBC提供了DriverManagerDataSource实现,它通过包装“DriverManager.getConnection”获取数据库连接,具体DataSource相关请参考【7.5.1控制数据库连接】。
java代码:
package cn.javass.spring.chapter7; import java.sql.ResultSet; import java.sql.SQLException; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowCallbackHandler; import org.springframework.jdbc.datasource.DriverManagerDataSource; public class JdbcTemplateTest { private static JdbcTemplate jdbcTemplate; @BeforeClass public static void setUpClass() { String url = "jdbc:hsqldb:mem:test"; String username = "sa"; String password = ""; DriverManagerDataSource dataSource = new DriverManagerDataSource(url, username, password); dataSource.setDriverClassName("org.hsqldb.jdbcDriver"); jdbcTemplate = new JdbcTemplate(dataSource); } @Test public void test() { //1.声明SQL String sql = "select * from INFORMATION_SCHEMA.SYSTEM_TABLES"; jdbcTemplate.query(sql, new RowCallbackHandler() { @Override public void processRow(ResultSet rs) throws SQLException { //2.处理结果集 String value = rs.getString("TABLE_NAME"); System.out.println("Column TABLENAME:" + value); } }); } }
接下来让我们具体分析一下:
1) jdbc:hsqldb:mem:test:表示使用hsqldb内存数据库,数据库名为“test”。
2) public static void setUpClass():使用junit的@BeforeClass注解,表示在所以测试方法之前执行,且只执行一次。在此方法中定义了DataSource并使用DataSource对象创建了JdbcTemplate对象。JdbcTemplate对象是线程安全的。
3) JdbcTemplate执行流程:首先定义SQL,其次调用JdbcTemplate方法执行SQL,最后通过RowCallbackHandler回调处理ResultSet结果集。
Spring JDBC解决方法相比传统JDBC编程方式是不是简单多了,是不是只有可变部分需要我们来做,其他的都由Spring JDBC框架来实现了。
接下来让我们深入JdbcTemplate及其扩展吧。
7.2.4 JdbcTemplate
首先让我们来看下如何使用JdbcTemplate来实现增删改查。
一、首先创建表结构:
java代码:
//代码片段(cn.javass.spring.chapter7.JdbcTemplateTest) @Before public void setUp() { String createTableSql = "create memory table test" + "(id int GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, " + "name varchar(100))"; jdbcTemplate.update(createTableSql); } @After public void tearDown() { String dropTableSql = "drop table test"; jdbcTemplate.execute(dropTableSql); }
1) org.junit包下的@Before和@After分别表示在测试方法之前和之后执行的方法,对于每个测试方法都将执行一次;
2) create memory table test表示创建hsqldb内存表,包含两个字段id和name,其中id是具有自增功能的主键,如果有朋友对此不熟悉hsqldb可以换成熟悉的数据库。
二、定义测试骨架,该测试方法将用于实现增删改查测试:
java代码:
@Test public void testCURD() { insert(); delete(); update(); select(); }
三、新增测试:
java代码:
private void insert() { jdbcTemplate.update("insert into test(name) values(‘name1‘)"); jdbcTemplate.update("insert into test(name) values(‘name2‘)"); Assert.assertEquals(2, jdbcTemplate.queryForInt("select count(*) from test")); }
四、删除测试:
java代码:
private void delete() { jdbcTemplate.update("delete from test where name=?", new Object[]{"name2"}); Assert.assertEquals(1, jdbcTemplate.queryForInt("select count(*) from test")); }
五、更新测试:
java代码:
private void update() { jdbcTemplate.update("update test set name=‘name3‘ where name=?", new Object[]{"name1"}); Assert.assertEquals(1, jdbcTemplate.queryForInt("select count(*) from test where name=‘name3‘")); }
六、查询测试:
java代码:
private void select() { jdbcTemplate.query("select * from test", new RowCallbackHandler(){ @Override public void processRow(ResultSet rs) throws SQLException { System.out.print("====id:" + rs.getInt("id")); System.out.println(",name:" + rs.getString("name")); } }); }
看完以上示例,大家是否觉得JdbcTemplate简化了我们很多劳动力呢?接下来让我们深入学习一下JdbcTemplate提供的方法。
JdbcTemplate主要提供以下五类方法:
- execute方法:可以用于执行任何SQL语句,一般用于执行DDL语句;
- update方法及batchUpdate方法:update方法用于执行新增、修改、删除等语句;batchUpdate方法用于执行批处理相关语句;
- query方法及queryForXXX方法:用于执行查询相关语句;
- call方法:用于执行存储过程、函数相关语句。
JdbcTemplate类支持的回调类:
- 预编译语句及存储过程创建回调:用于根据JdbcTemplate提供的连接创建相应的语句;
PreparedStatementCreator:通过回调获取JdbcTemplate提供的Connection,由用户使用该Conncetion创建相关的PreparedStatement;
CallableStatementCreator:通过回调获取JdbcTemplate提供的Connection,由用户使用该Conncetion创建相关的CallableStatement;
- 预编译语句设值回调:用于给预编译语句相应参数设值;
PreparedStatementSetter:通过回调获取JdbcTemplate提供的PreparedStatement,由用户来对相应的预编译语句相应参数设值;
BatchPreparedStatementSetter:;类似于PreparedStatementSetter,但用于批处理,需要指定批处理大小;
- 自定义功能回调:提供给用户一个扩展点,用户可以在指定类型的扩展点执行任何数量需要的操作;
ConnectionCallback:通过回调获取JdbcTemplate提供的Connection,用户可在该Connection执行任何数量的操作;
StatementCallback:通过回调获取JdbcTemplate提供的Statement,用户可以在该Statement执行任何数量的操作;
PreparedStatementCallback:通过回调获取JdbcTemplate提供的PreparedStatement,用户可以在该PreparedStatement执行任何数量的操作;
CallableStatementCallback:通过回调获取JdbcTemplate提供的CallableStatement,用户可以在该CallableStatement执行任何数量的操作;
- 结果集处理回调:通过回调处理ResultSet或将ResultSet转换为需要的形式;
RowMapper:用于将结果集每行数据转换为需要的类型,用户需实现方法mapRow(ResultSet rs, int rowNum)来完成将每行数据转换为相应的类型。
RowCallbackHandler:用于处理ResultSet的每一行结果,用户需实现方法processRow(ResultSet rs)来完成处理,在该回调方法中无需执行rs.next(),该操作由JdbcTemplate来执行,用户只需按行获取数据然后处理即可。
ResultSetExtractor:用于结果集数据提取,用户需实现方法extractData(ResultSet rs)来处理结果集,用户必须处理整个结果集;
接下来让我们看下具体示例吧,在示例中不可能介绍到JdbcTemplate全部方法及回调类的使用方法,我们只介绍代表性的,其余的使用都是类似的;
1)预编译语句及存储过程创建回调、自定义功能回调使用:
java代码:
@Test public void testPpreparedStatement1() { int count = jdbcTemplate.execute(new PreparedStatementCreator() { @Override public PreparedStatement createPreparedStatement(Connection conn) throws SQLException { return conn.prepareStatement("select count(*) from test"); }}, new PreparedStatementCallback<Integer>() { @Override public Integer doInPreparedStatement(PreparedStatement pstmt) throws SQLException, DataAccessException { pstmt.execute(); ResultSet rs = pstmt.getResultSet(); rs.next(); return rs.getInt(1); }}); Assert.assertEquals(0, count); }
首先使用PreparedStatementCreator创建一个预编译语句,其次由JdbcTemplate通过PreparedStatementCallback回调传回,由用户决定如何执行该PreparedStatement。此处我们使用的是execute方法。
2)预编译语句设值回调使用:
java代码:
@Test public void testPreparedStatement2() { String insertSql = "insert into test(name) values (?)"; int count = jdbcTemplate.update(insertSql, new PreparedStatementSetter() { @Override public void setValues(PreparedStatement pstmt) throws SQLException { pstmt.setObject(1, "name4"); }}); Assert.assertEquals(1, count); String deleteSql = "delete from test where name=?"; count = jdbcTemplate.update(deleteSql, new Object[] {"name4"}); Assert.assertEquals(1, count); }
通过JdbcTemplate的int update(String sql, PreparedStatementSetter pss)执行预编译sql,其中sql参数为“insert into test(name) values (?) ”,该sql有一个占位符需要在执行前设值,PreparedStatementSetter实现就是为了设值,使用setValues(PreparedStatement pstmt)回调方法设值相应的占位符位置的值。JdbcTemplate也提供一种更简单的方式“update(String sql, Object... args)”来实现设值,所以只要当使用该种方式不满足需求时才应使用PreparedStatementSetter。
3)结果集处理回调:
java代码:
@Test public void testResultSet1() { jdbcTemplate.update("insert into test(name) values(‘name5‘)"); String listSql = "select * from test"; List result = jdbcTemplate.query(listSql, new RowMapper<Map>() { @Override public Map mapRow(ResultSet rs, int rowNum) throws SQLException { Map row = new HashMap(); row.put(rs.getInt("id"), rs.getString("name")); return row; }}); Assert.assertEquals(1, result.size()); jdbcTemplate.update("delete from test where name=‘name5‘"); }
RowMapper接口提供mapRow(ResultSet rs, int rowNum)方法将结果集的每一行转换为一个Map,当然可以转换为其他类,如表的对象画形式。
java代码:
@Test public void testResultSet2() { jdbcTemplate.update("insert into test(name) values(‘name5‘)"); String listSql = "select * from test"; final List result = new ArrayList(); jdbcTemplate.query(listSql, new RowCallbackHandler() { @Override public void processRow(ResultSet rs) throws SQLException { Map row = new HashMap(); row.put(rs.getInt("id"), rs.getString("name")); result.add(row); }}); Assert.assertEquals(1, result.size()); jdbcTemplate.update("delete from test where name=‘name5‘"); }
RowCallbackHandler接口也提供方法processRow(ResultSet rs),能将结果集的行转换为需要的形式。
java代码:
@Test public void testResultSet3() { jdbcTemplate.update("insert into test(name) values(‘name5‘)"); String listSql = "select * from test"; List result = jdbcTemplate.query(listSql, new ResultSetExtractor<List>() { @Override public List extractData(ResultSet rs) throws SQLException, DataAccessException { List result = new ArrayList(); while(rs.next()) { Map row = new HashMap(); row.put(rs.getInt("id"), rs.getString("name")); result.add(row); } return result; }}); Assert.assertEquals(0, result.size()); jdbcTemplate.update("delete from test where name=‘name5‘"); }
ResultSetExtractor使用回调方法extractData(ResultSet rs)提供给用户整个结果集,让用户决定如何处理该结果集。
当然JdbcTemplate提供更简单的queryForXXX方法,来简化开发:
java代码:
//1.查询一行数据并返回int型结果 jdbcTemplate.queryForInt("select count(*) from test"); //2. 查询一行数据并将该行数据转换为Map返回 jdbcTemplate.queryForMap("select * from test where name=‘name5‘"); //3.查询一行任何类型的数据,最后一个参数指定返回结果类型 jdbcTemplate.queryForObject("select count(*) from test", Integer.class); //4.查询一批数据,默认将每行数据转换为Map jdbcTemplate.queryForList("select * from test"); //5.只查询一列数据列表,列类型是String类型,列名字是name jdbcTemplate.queryForList(" select name from test where name=?", new Object[]{"name5"}, String.class); //6.查询一批数据,返回为SqlRowSet,类似于ResultSet,但不再绑定到连接上 SqlRowSet rs = jdbcTemplate.queryForRowSet("select * from test");
3) 存储过程及函数回调:
首先修改JdbcTemplateTest的setUp方法,修改后如下所示:
java代码:
@Before public void setUp() { String createTableSql = "create memory table test" + "(id int GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, " + "name varchar(100))"; jdbcTemplate.update(createTableSql); String createHsqldbFunctionSql = "CREATE FUNCTION FUNCTION_TEST(str CHAR(100)) " + "returns INT begin atomic return length(str);end"; jdbcTemplate.update(createHsqldbFunctionSql); String createHsqldbProcedureSql = "CREATE PROCEDURE PROCEDURE_TEST" + "(INOUT inOutName VARCHAR(100), OUT outId INT) " + "MODIFIES SQL DATA " + "BEGIN ATOMIC " + " insert into test(name) values (inOutName); " + " SET outId = IDENTITY(); " + " SET inOutName = ‘Hello,‘ + inOutName; " + "END"; jdbcTemplate.execute(createHsqldbProcedureSql); }
其中CREATE FUNCTION FUNCTION_TEST用于创建自定义函数,CREATE PROCEDURE PROCEDURE_TEST用于创建存储过程,注意这些创建语句是数据库相关的,本示例中的语句只适用于HSQLDB数据库。
其次修改JdbcTemplateTest的tearDown方法,修改后如下所示:
java代码:
@After public void tearDown() { jdbcTemplate.execute("DROP FUNCTION FUNCTION_TEST"); jdbcTemplate.execute("DROP PROCEDURE PROCEDURE_TEST"); String dropTableSql = "drop table test"; jdbcTemplate.execute(dropTableSql); }
其中drop语句用于删除创建的存储过程、自定义函数及数据库表。
接下来看一下hsqldb如何调用自定义函数:
java代码:
@Test public void testCallableStatementCreator1() { final String callFunctionSql = "{call FUNCTION_TEST(?)}"; List<SqlParameter> params = new ArrayList<SqlParameter>(); params.add(new SqlParameter(Types.VARCHAR)); params.add(new SqlReturnResultSet("result", new ResultSetExtractor<Integer>() { @Override public Integer extractData(ResultSet rs) throws SQLException, DataAccessException { while(rs.next()) { return rs.getInt(1); } return 0; })); Map<String, Object> outValues = jdbcTemplate.call( new CallableStatementCreator() { @Override public CallableStatement createCallableStatement(Connection conn) throws SQLException { CallableStatement cstmt = conn.prepareCall(callFunctionSql); cstmt.setString(1, "test"); return cstmt; }}, params); Assert.assertEquals(4, outValues.get("result")); }
- {call FUNCTION_TEST(?)}:定义自定义函数的sql语句,注意hsqldb {?= call …}和{call …}含义是一样的,而比如mysql中两种含义是不一样的;
- params:用于描述自定义函数占位符参数或命名参数类型;SqlParameter用于描述IN类型参数、SqlOutParameter用于描述OUT类型参数、SqlInOutParameter用于描述INOUT类型参数、SqlReturnResultSet用于描述调用存储过程或自定义函数返回的ResultSet类型数据,其中SqlReturnResultSet需要提供结果集处理回调用于将结果集转换为相应的形式,hsqldb自定义函数返回值是ResultSet类型。
- CallableStatementCreator:提供Connection对象用于创建CallableStatement对象
- outValues:调用call方法将返回类型为Map<String, Object>对象;
- outValues.get("result"):获取结果,即通过SqlReturnResultSet对象转换过的数据;其中SqlOutParameter、SqlInOutParameter、SqlReturnResultSet指定的name用于从call执行后返回的Map中获取相应的结果,即name是Map的键。
注:因为hsqldb {?= call …}和{call …}含义是一样的,因此调用自定义函数将返回一个包含结果的ResultSet。
最后让我们示例下mysql如何调用自定义函数:
java代码:
@Test public void testCallableStatementCreator2() { JdbcTemplate mysqlJdbcTemplate = new JdbcTemplate(getMysqlDataSource); //2.创建自定义函数 String createFunctionSql = "CREATE FUNCTION FUNCTION_TEST(str VARCHAR(100)) " + "returns INT return LENGTH(str)"; String dropFunctionSql = "DROP FUNCTION IF EXISTS FUNCTION_TEST"; mysqlJdbcTemplate.update(dropFunctionSql); mysqlJdbcTemplate.update(createFunctionSql); //3.准备sql,mysql支持{?= call …} final String callFunctionSql = "{?= call FUNCTION_TEST(?)}"; //4.定义参数 List<SqlParameter> params = new ArrayList<SqlParameter>(); params.add(new SqlOutParameter("result", Types.INTEGER)); params.add(new SqlParameter("str", Types.VARCHAR)); Map<String, Object> outValues = mysqlJdbcTemplate.call( new CallableStatementCreator() { @Override public CallableStatement createCallableStatement(Connection conn) throws SQLException { CallableStatement cstmt = conn.prepareCall(callFunctionSql); cstmt.registerOutParameter(1, Types.INTEGER); cstmt.setString(2, "test"); return cstmt; }}, params); Assert.assertEquals(4, outValues.get("result")); } public DataSource getMysqlDataSource() { String url = "jdbc:mysql://localhost:3306/test"; DriverManagerDataSource dataSource = new DriverManagerDataSource(url, "root", ""); dataSource.setDriverClassName("com.mysql.jdbc.Driver"); return dataSource; }
- getMysqlDataSource:首先启动mysql(本书使用5.4.3版本),其次登录mysql创建test数据库(“create database test;”),在进行测试前,请先下载并添加mysql-connector-java-5.1.10.jar到classpath;
- {?= call FUNCTION_TEST(?)}:可以使用{?= call …}形式调用自定义函数;
- params:无需使用SqlReturnResultSet提取结果集数据,而是使用SqlOutParameter来描述自定义函数返回值;
- CallableStatementCreator:同上个例子含义一样;
- cstmt.registerOutParameter(1, Types.INTEGER):将OUT类型参数注册为JDBC类型Types.INTEGER,此处即返回值类型为Types.INTEGER。
- outValues.get("result"):获取结果,直接返回Integer类型,比hsqldb简单多了吧。
最后看一下如何如何调用存储过程:
java代码:
@Test public void testCallableStatementCreator3() { final String callProcedureSql = "{call PROCEDURE_TEST(?, ?)}"; List<SqlParameter> params = new ArrayList<SqlParameter>(); params.add(new SqlInOutParameter("inOutName", Types.VARCHAR)); params.add(new SqlOutParameter("outId", Types.INTEGER)); Map<String, Object> outValues = jdbcTemplate.call( new CallableStatementCreator() { @Override public CallableStatement createCallableStatement(Connection conn) throws SQLException { CallableStatement cstmt = conn.prepareCall(callProcedureSql); cstmt.registerOutParameter(1, Types.VARCHAR); cstmt.registerOutParameter(2, Types.INTEGER); cstmt.setString(1, "test"); return cstmt; }}, params); Assert.assertEquals("Hello,test", outValues.get("inOutName")); Assert.assertEquals(0, outValues.get("outId")); }
- {call PROCEDURE_TEST(?, ?)}:调用存储过程sql;
- params:定义存储过程参数;SqlInOutParameter描述INOUT类型参数、SqlOutParameter描述OUT类型参数;
- CallableStatementCreator:用于创建CallableStatement,并设值及注册OUT参数类型;
- outValues:通过SqlInOutParameter及SqlOutParameter参数定义的name来获取存储过程结果。
JdbcTemplate类还提供了很多便利方法,在此就不一一介绍了,但这些方法是由规律可循的,第一种就是提供回调接口让用户决定做什么,第二种可以认为是便利方法(如queryForXXX),用于那些比较简单的操作。
7.2.4 NamedParameterJdbcTemplate
NamedParameterJdbcTemplate类是基于JdbcTemplate类,并对它进行了封装从而支持命名参数特性。
NamedParameterJdbcTemplate主要提供以下三类方法:execute方法、query及queryForXXX方法、update及batchUpdate方法。
首先让我们看个例子吧:
java代码:
@Test public void testNamedParameterJdbcTemplate1() { NamedParameterJdbcTemplate namedParameterJdbcTemplate = null; //namedParameterJdbcTemplate = // new NamedParameterJdbcTemplate(dataSource); namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(jdbcTemplate); String insertSql = "insert into test(name) values(:name)"; String selectSql = "select * from test where name=:name"; String deleteSql = "delete from test where name=:name"; Map<String, Object> paramMap = new HashMap<String, Object>(); paramMap.put("name", "name5"); namedParameterJdbcTemplate.update(insertSql, paramMap); final List<Integer> result = new ArrayList<Integer>(); namedParameterJdbcTemplate.query(selectSql, paramMap, new RowCallbackHandler() { @Override public void processRow(ResultSet rs) throws SQLException { result.add(rs.getInt("id")); } }); Assert.assertEquals(1, result.size()); SqlParameterSource paramSource = new MapSqlParameterSource(paramMap); namedParameterJdbcTemplate.update(deleteSql, paramSource); }
接下来让我们分析一下代码吧:
1)NamedParameterJdbcTemplate初始化:可以使用DataSource或JdbcTemplate 对象作为构造器参数初始化;
2)insert into test(name) values(:name):其中“:name”就是命名参数;
3) update(insertSql, paramMap):其中paramMap是一个Map类型,包含键为“name”,值为“name5”的键值对,也就是为命名参数设值的数据;
4)query(selectSql, paramMap, new RowCallbackHandler()……):类似于JdbcTemplate中介绍的,唯一不同是需要传入paramMap来为命名参数设值;
5)update(deleteSql, paramSource):类似于“update(insertSql, paramMap)”,但使用SqlParameterSource参数来为命名参数设值,此处使用MapSqlParameterSource实现,其就是简单封装java.util.Map。
NamedParameterJdbcTemplate类为命名参数设值有两种方式:java.util.Map和SqlParameterSource:
1)java.util.Map:使用Map键数据来对于命名参数,而Map值数据用于设值;
2)SqlParameterSource:可以使用SqlParameterSource实现作为来实现为命名参数设值,默认有MapSqlParameterSource和BeanPropertySqlParameterSource实现;MapSqlParameterSource实现非常简单,只是封装了java.util.Map;而BeanPropertySqlParameterSource封装了一个JavaBean对象,通过JavaBean对象属性来决定命名参数的值。
java代码:
package cn.javass.spring.chapter7; public class UserModel { private int id; private String myName; //省略getter和setter }
java代码:
@Test public void testNamedParameterJdbcTemplate2() { NamedParameterJdbcTemplate namedParameterJdbcTemplate = null; namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(jdbcTemplate); UserModel model = new UserModel(); model.setMyName("name5"); String insertSql = "insert into test(name) values(:myName)"; SqlParameterSource paramSource = new BeanPropertySqlParameterSource(model); namedParameterJdbcTemplate.update(insertSql, paramSource); }
可以看出BeanPropertySqlParameterSource使用能减少很多工作量,但命名参数必须和JavaBean属性名称相对应才可以。
7.2.5 SimpleJdbcTemplate
SimpleJdbcTemplate类也是基于JdbcTemplate类,但利用Java5+的可变参数列表和自动装箱和拆箱从而获取更简洁的代码。
SimpleJdbcTemplate主要提供两类方法:query及queryForXXX方法、update及batchUpdate方法。
首先让我们看个例子吧:
java代码:
//定义UserModel的RowMapper package cn.javass.spring.chapter7; import java.sql.ResultSet; import java.sql.SQLException; import org.springframework.jdbc.core.RowMapper; public class UserRowMapper implements RowMapper<UserModel> { @Override public UserModel mapRow(ResultSet rs, int rowNum) throws SQLException { UserModel model = new UserModel(); model.setId(rs.getInt("id")); model.setMyName(rs.getString("name")); return model; } }
java代码:
@Test public void testSimpleJdbcTemplate() { //还支持DataSource和NamedParameterJdbcTemplate作为构造器参数 SimpleJdbcTemplate simpleJdbcTemplate = new SimpleJdbcTemplate(jdbcTemplate); String insertSql = "insert into test(id, name) values(?, ?)"; simpleJdbcTemplate.update(insertSql, 10, "name5"); String selectSql = "select * from test where id=? and name=?"; List<Map<String, Object>> result = simpleJdbcTemplate.queryForList(selectSql, 10, "name5"); Assert.assertEquals(1, result.size()); RowMapper<UserModel> mapper = new UserRowMapper(); List<UserModel> result2 = simpleJdbcTemplate.query(selectSql, mapper, 10, "name5"); Assert.assertEquals(1, result2.size()); }
1)SimpleJdbcTemplate初始化:可以使用DataSource、JdbcTemplate或NamedParameterJdbcTemplate对象作为构造器参数初始化;
2)update(insertSql, 10, "name5"):采用Java5+可变参数列表从而代替new Object[]{10, "name5"}方式;
3)query(selectSql, mapper, 10, "name5"):使用Java5+可变参数列表及RowMapper回调并利用泛型特性来指定返回值类型(List<UserModel>)。
SimpleJdbcTemplate类还支持命名参数特性,如queryForList(String sql, SqlParameterSource args)和queryForList(String sql, Map<String, ?> args) ,类似于NamedParameterJdbcTemplate中使用,在此就不介绍了。
注:SimpleJdbcTemplate还提供类似于ParameterizedRowMapper 用于泛型特性的支持,ParameterizedRowMapper是RowMapper的子类,但从Spring 3.0由于允许环境需要Java5+,因此不再需要ParameterizedRowMapper ,而可以直接使用RowMapper;
query(String sql, ParameterizedRowMapper<T> rm, SqlParameterSource args)
query(String sql, RowMapper<T> rm, Object... args) //直接使用该语句
SimpleJdbcTemplate还提供如下方法用于获取JdbcTemplate和NamedParameterJdbcTemplate:
1)获取JdbcTemplate对象方法:JdbcOperations是JdbcTemplate的接口
JdbcOperations getJdbcOperations()
2)获取NamedParameterJdbcTemplate对象方法:NamedParameterJdbcOperations是NamedParameterJdbcTemplate的接口
NamedParameterJdbcOperations getNamedParameterJdbcOperations()
对JDBC的支持 之 7.3 关系数据库操作对象化
7.3.1 概述
所谓关系数据库对象化其实就是用面向对象方式表示关系数据库操作,从而可以复用。
Spring JDBC框架将数据库操作封装为一个RdbmsOperation,该对象是线程安全的、可复用的对象,是所有数据库对象的父类。而SqlOperation继承了RdbmsOperation,代表了数据库SQL操作,如select、update、call等,如图7-4所示。
图7-4 关系数据库操作对象化支持类
数据库操作对象化只要有以下几种类型,所以类型是线程安全及可复用的:
- 查询:将数据库操作select封装为对象,查询操作的基类是SqlQuery,所有查询都可以使用该类表示,Spring JDBC还提供了一些更容易使用的MappingSqlQueryWithParameters和MappingSqlQuery用于将结果集映射为Java对象,查询对象类还提供了两个扩展UpdatableSqlQuery和SqlFunction;
- 更新:即增删改操作,将数据库操作insert 、update、delete封装为对象,增删改基类是SqlUpdate,当然还提供了BatchSqlUpdate用于批处理;
- 存储过程及函数:将存储过程及函数调用封装为对象,基类是SqlCall类,提供了StoredProcedure实现。
7.3.2 查询
1)SqlQuery:需要覆盖如下方法来定义一个RowMapper,其中parameters参数表示命名参数或占位符参数值列表,而context是由用户传入的上下文数据。
java代码:
RowMapper<T> newRowMapper(Object[] parameters, Map context)
SqlQuery提供两类方法:
- execute及executeByNamedParam方法:用于查询多行数据,其中executeByNamedParam用于支持命名参数绑定参数;
- findObject及findObjectByNamedParam方法:用于查询单行数据,其中findObjectByNamedParam用于支持命名参数绑定。
演示一下SqlQuery如何使用:
java代码:
@Test public void testSqlQuery() { SqlQuery query = new UserModelSqlQuery(jdbcTemplate); List<UserModel> result = query.execute("name5"); Assert.assertEquals(0, result.size()); }
从测试代码可以SqlQuery使用非常简单,创建SqlQuery实现对象,然后调用相应的方法即可,接下来看一下SqlQuery实现:
java代码:
package cn.javass.spring.chapter7; //省略import public class UserModelSqlQuery extends SqlQuery<UserModel> { public UserModelSqlQuery(JdbcTemplate jdbcTemplate) { //super.setDataSource(jdbcTemplate.getDataSource()); super.setJdbcTemplate(jdbcTemplate); super.setSql("select * from test where name=?"); super.declareParameter(new SqlParameter(Types.VARCHAR)); compile(); } @Override protected RowMapper<UserModel> newRowMapper(Object[] parameters, Map context) { return new UserRowMapper(); } }
从测试代码可以看出,具体步骤如下:
一、setJdbcTemplate/ setDataSource:首先设置数据源或JdbcTemplate;
二、setSql("select * from test where name=?"):定义sql语句,所以定义的sql语句都将被编译为PreparedStatement;
三、declareParameter(new SqlParameter(Types.VARCHAR)):对PreparedStatement参数描述,使用SqlParameter来描述参数类型,支持命名参数、占位符描述;
对于命名参数可以使用如new SqlParameter("name", Types.VARCHAR)描述;注意占位符参数描述必须按占位符参数列表的顺序进行描述;
四、编译:可选,当执行相应查询方法时会自动编译,用于将sql编译为PreparedStatement,对于编译的SqlQuery不能再对参数进行描述了。
五、以上步骤是不可变的,必须按顺序执行。
2)MappingSqlQuery:用于简化SqlQuery中RowMapper创建,可以直接在实现mapRow(ResultSet rs, int rowNum)来将行数据映射为需要的形式;
MappingSqlQuery所有查询方法完全继承于SqlQuery。
演示一下MappingSqlQuery如何使用:
java代码:
@Test public void testMappingSqlQuery() { jdbcTemplate.update("insert into test(name) values(‘name5‘)"); SqlQuery<UserModel> query = new UserModelMappingSqlQuery(jdbcTemplate); Map<String, Object> paramMap = new HashMap<String, Object>(); paramMap.put("name", "name5"); UserModel result = query.findObjectByNamedParam(paramMap); Assert.assertNotNull(result); }
MappingSqlQuery使用和SqlQuery完全一样,创建MappingSqlQuery实现对象,然后调用相应的方法即可,接下来看一下MappingSqlQuery实现,findObjectByNamedParam方法用于执行命名参数查询:
java代码:
package cn.javass.spring.chapter7; //省略import public class UserModelMappingSqlQuery extends MappingSqlQuery<UserModel> { public UserModelMappingSqlQuery(JdbcTemplate jdbcTemplate) { super.setDataSource(jdbcTemplate.getDataSource()); super.setSql("select * from test where name=:name"); super.declareParameter(new SqlParameter("name", Types.VARCHAR)); compile(); } @Override protected UserModel mapRow(ResultSet rs, int rowNum) throws SQLException { UserModel model = new UserModel(); model.setId(rs.getInt("id")); model.setMyName(rs.getString("name")); return model; } }
和SqlQuery唯一不同的是使用mapRow来讲每行数据转换为需要的形式,其他地方完全一样。
1) UpdatableSqlQuery:提供可更新结果集查询支持,子类实现updateRow(ResultSet rs, int rowNum, Map context)对结果集进行更新。
2) GenericSqlQuery:提供setRowMapperClass(Class rowMapperClass)方法用于指定RowMapper实现,在此就不演示了。具体请参考testGenericSqlQuery()方法。
3) SqlFunction:SQL“函数”包装器,用于支持那些返回单行结果集的查询。该类主要用于返回单行单列结果集。
java代码:
@Test public void testSqlFunction() { jdbcTemplate.update("insert into test(name) values(‘name5‘)"); String countSql = "select count(*) from test"; SqlFunction<Integer> sqlFunction1 = new SqlFunction<Integer>(jdbcTemplate.getDataSource(), countSql); Assert.assertEquals(1, sqlFunction1.run()); String selectSql = "select name from test where name=?"; SqlFunction<String> sqlFunction2 = new SqlFunction<String>(jdbcTemplate.getDataSource(), selectSql); sqlFunction2.declareParameter(new SqlParameter(Types.VARCHAR)); String name = (String) sqlFunction2.runGeneric(new Object[] {"name5"}); Assert.assertEquals("name5", name); }
如代码所示,SqlFunction初始化时需要DataSource和相应的sql语句,如果有参数需要使用declareParameter对参数类型进行描述;run方法默认返回int型,当然也可以使用runGeneric返回其他类型,如String等。
7.3.3 更新
SqlUpdate类用于支持数据库更新操作,即增删改(insert、delete、update)操作,该方法类似于SqlQuery,只是职责不一样。
SqlUpdate提供了update及updateByNamedParam方法用于数据库更新操作,其中updateByNamedParam用于命名参数类型更新。
演示一下SqlUpdate如何使用:
java代码:
package cn.javass.spring.chapter7; //省略import public class InsertUserModel extends SqlUpdate { public InsertUserModel(JdbcTemplate jdbcTemplate) { super.setJdbcTemplate(jdbcTemplate); super.setSql("insert into test(name) values(?)"); super.declareParameter(new SqlParameter(Types.VARCHAR)); compile(); } }
java代码:
@Test public void testSqlUpdate() { SqlUpdate insert = new InsertUserModel(jdbcTemplate); insert.update("name5"); String updateSql = "update test set name=? where name=?"; SqlUpdate update = new SqlUpdate(jdbcTemplate.getDataSource(), updateSql, new int[]{Types.VARCHAR, Types.VARCHAR}); update.update("name6", "name5"); String deleteSql = "delete from test where name=:name"; SqlUpdate delete = new SqlUpdate(jdbcTemplate.getDataSource(), deleteSql, new int[]{Types.VARCHAR}); Map<String, Object> paramMap = new HashMap<String, Object>(); paramMap.put("name", "name5"); delete.updateByNamedParam(paramMap); }
InsertUserModel类实现类似于SqlQuery实现,用于执行数据库插入操作,SqlUpdate还提供一种更简洁的构造器SqlUpdate(DataSource ds, String sql, int[] types),其中types用于指定占位符或命名参数类型;SqlUpdate还支持命名参数,使用updateByNamedParam方法来进行命名参数操作。
7.3.4 存储过程及函数
StoredProcedure用于支持存储过程及函数,该类的使用同样类似于SqlQuery。
StoredProcedure提供execute方法用于执行存储过程及函数。
一、StoredProcedure如何调用自定义函数:
java代码:
@Test public void testStoredProcedure1() { StoredProcedure lengthFunction = new HsqldbLengthFunction(jdbcTemplate); Map<String,Object> outValues = lengthFunction.execute("test"); Assert.assertEquals(4, outValues.get("result")); }
StoredProcedure使用非常简单,定义StoredProcedure实现HsqldbLengthFunction,并调用execute方法执行即可,接下来看一下HsqldbLengthFunction实现:
java代码:
package cn.javass.spring.chapter7; //省略import public class HsqldbLengthFunction extends StoredProcedure { public HsqldbLengthFunction(JdbcTemplate jdbcTemplate) { super.setJdbcTemplate(jdbcTemplate); super.setSql("FUNCTION_TEST"); super.declareParameter( new SqlReturnResultSet("result", new ResultSetExtractor<Integer>() { @Override public Integer extractData(ResultSet rs) throws SQLException, DataAccessException { while(rs.next()) { return rs.getInt(1); } return 0; } })); super.declareParameter(new SqlParameter("str", Types.VARCHAR)); compile(); } }
StoredProcedure自定义函数使用类似于SqlQuery,首先设置数据源或JdbcTemplate对象,其次定义自定义函数,然后使用declareParameter进行参数描述,最后调用compile(可选)编译自定义函数。
接下来看一下mysql自定义函数如何使用:
java代码:
@Test public void testStoredProcedure2() { JdbcTemplate mysqlJdbcTemplate = new JdbcTemplate(getMysqlDataSource()); String createFunctionSql = "CREATE FUNCTION FUNCTION_TEST(str VARCHAR(100)) " + "returns INT return LENGTH(str)"; String dropFunctionSql = "DROP FUNCTION IF EXISTS FUNCTION_TEST"; mysqlJdbcTemplate.update(dropFunctionSql); mysqlJdbcTemplate.update(createFunctionSql); StoredProcedure lengthFunction = new MysqlLengthFunction(mysqlJdbcTemplate); Map<String,Object> outValues = lengthFunction.execute("test"); Assert.assertEquals(4, outValues.get("result")); }
MysqlLengthFunction自定义函数使用与HsqldbLengthFunction使用完全一样,只是内部实现稍有差别:
java代码:
package cn.javass.spring.chapter7; //省略import public class MysqlLengthFunction extends StoredProcedure { public MysqlLengthFunction(JdbcTemplate jdbcTemplate) { super.setJdbcTemplate(jdbcTemplate); super.setSql("FUNCTION_TEST"); super.setFunction(true); super.declareParameter(new SqlOutParameter("result", Types.INTEGER)); super.declareParameter(new SqlParameter("str", Types.VARCHAR)); compile(); } }
MysqlLengthFunction与HsqldbLengthFunction实现不同的地方有两点:
- setFunction(true):表示是自定义函数调用,即编译后的sql为{?= call …}形式;如果使用hsqldb不能设置为true,因为在hsqldb中{?= call …}和{call …}含义一样;
- declareParameter(new SqlOutParameter("result", Types.INTEGER)):将自定义函数返回值类型直接描述为Types.INTEGER;SqlOutParameter必须指定name,而不用使用SqlReturnResultSet首先获取结果集,然后再从结果集获取返回值,这是mysql与hsqldb的区别;
一、StoredProcedure如何调用存储过程:
java代码:
@Test public void testStoredProcedure3() { StoredProcedure procedure = new HsqldbTestProcedure(jdbcTemplate); Map<String,Object> outValues = procedure.execute("test"); Assert.assertEquals(0, outValues.get("outId")); Assert.assertEquals("Hello,test", outValues.get("inOutName")); }
StoredProcedure存储过程实现HsqldbTestProcedure调用与HsqldbLengthFunction调用完全一样,不同的是在实现时,参数描述稍有不同:
java代码:
package cn.javass.spring.chapter7; //省略import public class HsqldbTestProcedure extends StoredProcedure { public HsqldbTestProcedure(JdbcTemplate jdbcTemplate) { super.setJdbcTemplate(jdbcTemplate); super.setSql("PROCEDURE_TEST"); super.declareParameter(new SqlInOutParameter("inOutName", Types.VARCHAR)); super.declareParameter(new SqlOutParameter("outId", Types.INTEGER)); compile(); } }
- declareParameter:使用SqlInOutParameter描述INOUT类型参数,使用SqlOutParameter描述OUT类型参数,必须按顺序定义,不能颠倒。
7.4 Spring提供的其它帮助
7.4.1 SimpleJdbc方式
Spring JDBC抽象框架提供SimpleJdbcInsert和SimpleJdbcCall类,这两个类通过利用JDBC驱动提供的数据库元数据来简化JDBC操作。
1、SimpleJdbcInsert: 用于插入数据,根据数据库元数据进行插入数据,本类用于简化插入操作,提供三种类型方法:execute方法用于普通插入、executeAndReturnKey及executeAndReturnKeyHolder方法用于插入时获取主键值、executeBatch方法用于批处理。
java代码:
@Test public void testSimpleJdbcInsert() { SimpleJdbcInsert insert = new SimpleJdbcInsert(jdbcTemplate); insert.withTableName("test"); Map<String, Object> args = new HashMap<String, Object>(); args.put("name", "name5"); insert.compile(); //1.普通插入 insert.execute(args); Assert.assertEquals(1, jdbcTemplate.queryForInt("select count(*) from test")); //2.插入时获取主键值 insert = new SimpleJdbcInsert(jdbcTemplate); insert.withTableName("test"); insert.setGeneratedKeyName("id"); Number id = insert.executeAndReturnKey(args); Assert.assertEquals(1, id); //3.批处理 insert = new SimpleJdbcInsert(jdbcTemplate); insert.withTableName("test"); insert.setGeneratedKeyName("id"); int[] updateCount = insert.executeBatch(new Map[] {args, args, args}); Assert.assertEquals(1, updateCount[0]); Assert.assertEquals(5, jdbcTemplate.queryForInt("select count(*) from test")); }
- new SimpleJdbcInsert(jdbcTemplate) : 首次通过DataSource对象或JdbcTemplate对象初始化SimpleJdbcInsert;
- insert.withTableName("test") : 用于设置数据库表名;
- args : 用于指定插入时列名及值,如本例中只有name列名,即编译后的sql类似于“insert into test(name) values(?)”;
- insert.compile() : 可选的编译步骤,在调用执行方法时自动编译,编译后不能再对insert对象修改;
- 执行: execute方法用于执行普通插入;executeAndReturnKey用于执行并获取自动生成主键(注意是Number类型),必须首先通过setGeneratedKeyName设置主键然后才能获取,如果想获取复合主键请使用setGeneratedKeyNames描述主键然后通过executeReturningKeyHolder获取复合主键KeyHolder对象;executeBatch用于批处理;
2、SimpleJdbcCall: 用于调用存储过程及自定义函数,本类用于简化存储过程及自定义函数调用。
java代码:
@Test public void testSimpleJdbcCall1() { //此处用mysql,因为hsqldb调用自定义函数和存储过程一样 SimpleJdbcCall call = new SimpleJdbcCall(getMysqlDataSource()); call.withFunctionName("FUNCTION_TEST"); call.declareParameters(new SqlOutParameter("result", Types.INTEGER)); call.declareParameters(new SqlParameter("str", Types.VARCHAR)); Map<String, Object> outVlaues = call.execute("test"); Assert.assertEquals(4, outVlaues.get("result")); }
- new SimpleJdbcCall(getMysqlDataSource()) :通过DataSource对象或JdbcTemplate对象初始化SimpleJdbcCall;
- withFunctionName("FUNCTION_TEST") : 定义自定义函数名;自定义函数sql语句将被编译为类似于{?= call …}形式;
- declareParameters : 描述参数类型,使用方式与StoredProcedure对象一样;
- 执行: 调用execute方法执行自定义函数;
java代码:
@Test public void testSimpleJdbcCall2() { //调用hsqldb自定义函数得使用如下方式 SimpleJdbcCall call = new SimpleJdbcCall(jdbcTemplate); call.withProcedureName("FUNCTION_TEST"); call.declareParameters(new SqlReturnResultSet("result", new ResultSetExtractor<Integer>() { @Override public Integer extractData(ResultSet rs) throws SQLException, DataAccessException { while(rs.next()) { return rs.getInt(1); } return 0; }})); call.declareParameters(new SqlParameter("str", Types.VARCHAR)); Map<String, Object> outVlaues = call.execute("test"); Assert.assertEquals(4, outVlaues.get("result")); }
调用hsqldb数据库自定义函数与调用mysql自定义函数完全不同,详见StoredProcedure中的解释。
java代码:
@Test public void testSimpleJdbcCall3() { SimpleJdbcCall call = new SimpleJdbcCall(jdbcTemplate); call.withProcedureName("PROCEDURE_TEST"); call.declareParameters(new SqlInOutParameter("inOutName", Types.VARCHAR)); call.declareParameters(new SqlOutParameter("outId", Types.INTEGER)); SqlParameterSource params = new MapSqlParameterSource().addValue("inOutName", "test"); Map<String, Object> outVlaues = call.execute(params); Assert.assertEquals("Hello,test", outVlaues.get("inOutName")); Assert.assertEquals(0, outVlaues.get("outId")); }
与自定义函数调用不同的是使用withProcedureName来指定存储过程名字;其他参数描述等完全一样。
7.4.2 控制数据库连接
Spring JDBC通过DataSource控制数据库连接,即通过DataSource实现获取数据库连接。
Spring JDBC提供了一下DataSource实现:
- DriverManagerDataSource :简单封装了DriverManager获取数据库连接;通过DriverManager的getConnection方法获取数据库连接;
- SingleConnectionDataSource :内部封装了一个连接,该连接使用后不会关闭,且不能在多线程环境中使用,一般用于测试;
- LazyConnectionDataSourceProxy :包装一个DataSource,用于延迟获取数据库连接,只有在真正创建Statement等时才获取连接,因此再说实际项目中最后使用该代理包装原始DataSource从而使得只有在真正需要连接时才去获取。
第三方提供的DataSource实现主要有C3P0、Proxool、DBCP等,这些实现都具有数据库连接池能力。
DataSourceUtils: Spring JDBC抽象框架内部都是通过它的getConnection(DataSource dataSource)方法获取数据库连接,releaseConnection(Connection con, DataSource dataSource) 用于释放数据库连接,DataSourceUtils用于支持Spring管理事务,只有使用DataSourceUtils获取的连接才具有Spring管理事务。
7.4.3 获取自动生成的主键
有许多数据库提供自动生成主键的能力,因此我们可能需要获取这些自动生成的主键,JDBC 3.0标准支持获取自动生成的主键,且必须数据库支持自动生成键获取。
1 )JdbcTemplate 获取自动生成主键方式:
java代码:
@Test public void testFetchKey1() throws SQLException { final String insertSql = "insert into test(name) values(‘name5‘)"; KeyHolder generatedKeyHolder = new GeneratedKeyHolder(); jdbcTemplate.update(new PreparedStatementCreator() { @Override public PreparedStatement createPreparedStatement(Connection conn) throws SQLException { return conn.prepareStatement(insertSql, new String[]{"ID"}); }}, generatedKeyHolder); Assert.assertEquals(0, generatedKeyHolder.getKey()); }
使用JdbcTemplate的update(final PreparedStatementCreator psc, final KeyHolder generatedKeyHolder)方法执行需要返回自动生成主键的插入语句,其中psc用于创建PreparedStatement并指定自动生成键,如“prepareStatement(insertSql, new String[]{"ID"})”;generatedKeyHolder是KeyHolder类型,用于获取自动生成的主键或复合主键;如使用getKey方法获取自动生成的主键。
2 )SqlUpdate 获取自动生成主键方式:
java代码:
@Test public void testFetchKey2() { final String insertSql = "insert into test(name) values(‘name5‘)"; KeyHolder generatedKeyHolder = new GeneratedKeyHolder(); SqlUpdate update = new SqlUpdate(); update.setJdbcTemplate(jdbcTemplate); update.setReturnGeneratedKeys(true); //update.setGeneratedKeysColumnNames(new String[]{"ID"}); update.setSql(insertSql); update.update(null, generatedKeyHolder); Assert.assertEquals(0, generatedKeyHolder.getKey()); }
SqlUpdate获取自动生成主键方式和JdbcTemplate完全一样,可以使用setReturnGeneratedKeys(true)表示要获取自动生成键;也可以使用setGeneratedKeysColumnNames指定自动生成键列名。
3 )SimpleJdbcInsert : 前边示例已介绍,此处就不演示了。
7.4.4 JDBC批量操作
JDBC批处理用于减少与数据库交互的次数来提升性能,Spring JDBC抽象框架通过封装批处理操作来简化批处理操作
1 )JdbcTemplate 批处理: 支持普通的批处理及占位符批处理;
java代码:
@Test public void testBatchUpdate1() { String insertSql = "insert into test(name) values(‘name5‘)"; String[] batchSql = new String[] {insertSql, insertSql}; jdbcTemplate.batchUpdate(batchSql); Assert.assertEquals(2, jdbcTemplate.queryForInt("select count(*) from test")); }
直接调用batchUpdate方法执行需要批处理的语句即可。
java代码:
@Test public void testBatchUpdate2() { String insertSql = "insert into test(name) values(?)"; final String[] batchValues = new String[] {"name5", "name6"}; jdbcTemplate.batchUpdate(insertSql, new BatchPreparedStatementSetter() { @Override public void setValues(PreparedStatement ps, int i) throws SQLException { ps.setString(1, batchValues[i]); } @Override public int getBatchSize() { return batchValues.length; } }); Assert.assertEquals(2, jdbcTemplate.queryForInt("select count(*) from test")); }
JdbcTemplate还可以通过batchUpdate(String sql, final BatchPreparedStatementSetter pss)方法进行批处理,该方式使用预编译语句,然后通过BatchPreparedStatementSetter实现进行设值(setValues)及指定批处理大小(getBatchSize)。
2 )NamedParameterJdbcTemplate 批处理: 支持命名参数批处理;
java代码:
@Test public void testBatchUpdate3() { NamedParameterJdbcTemplate namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(jdbcTemplate); String insertSql = "insert into test(name) values(:myName)"; UserModel model = new UserModel(); model.setMyName("name5"); SqlParameterSource[] params = SqlParameterSourceUtils.createBatch(new Object[] {model, model}); namedParameterJdbcTemplate.batchUpdate(insertSql, params); Assert.assertEquals(2, jdbcTemplate.queryForInt("select count(*) from test")); }
通过batchUpdate(String sql, SqlParameterSource[] batchArgs)方法进行命名参数批处理,batchArgs指定批处理数据集。SqlParameterSourceUtils.createBatch用于根据JavaBean对象或者Map创建相应的BeanPropertySqlParameterSource或MapSqlParameterSource。
3) SimpleJdbcTemplate 批处理: 已更简单的方式进行批处理;
java代码:
@Test public void testBatchUpdate4() { SimpleJdbcTemplate simpleJdbcTemplate = new SimpleJdbcTemplate(jdbcTemplate); String insertSql = "insert into test(name) values(?)"; List<Object[]> params = new ArrayList<Object[]>(); params.add(new Object[]{"name5"}); params.add(new Object[]{"name5"}); simpleJdbcTemplate.batchUpdate(insertSql, params); Assert.assertEquals(2, jdbcTemplate.queryForInt("select count(*) from test")); }
本示例使用batchUpdate(String sql, List<Object[]> batchArgs)方法完成占位符批处理,当然也支持命名参数批处理等。
4 )SimpleJdbcInsert 批处理:
java代码:
@Test public void testBatchUpdate5() { SimpleJdbcInsert insert = new SimpleJdbcInsert(jdbcTemplate); insert.withTableName("test"); Map<String, Object> valueMap = new HashMap<String, Object>(); valueMap.put("name", "name5"); insert.executeBatch(new Map[] {valueMap, valueMap}); Assert.assertEquals(2, jdbcTemplate.queryForInt("select count(*) from test")); }
如代码所示,使用executeBatch(Map<String, Object>[] batch)方法执行批处理。
7.5 集成Spring JDBC及最佳实践
大多数情况下Spring JDBC都是与IOC容器一起使用。通过配置方式使用Spring JDBC。
而且大部分时间都是使用JdbcTemplate类(或SimpleJdbcTemplate和NamedParameterJdbcTemplate)进行开发,即可能80%时间使用JdbcTemplate类,而只有20%时间使用其他类开发,符合80/20法则。
Spring JDBC通过实现DaoSupport来支持一致的数据库访问。
Spring JDBC提供如下DaoSupport实现:
- JdbcDaoSupport:用于支持一致的JdbcTemplate访问;
- NamedParameterJdbcDaoSupport:继承JdbcDaoSupport,同时提供NamedParameterJdbcTemplate访问;
- SimpleJdbcDaoSupport:继承JdbcDaoSupport,同时提供SimpleJdbcTemplate访问。
由于JdbcTemplate、NamedParameterJdbcTemplate、SimpleJdbcTemplate类使用DataSourceUtils获取及释放连接,而且连接是与线程绑定的,因此这些JDBC模板类是线程安全的,即JdbcTemplate对象可以在多线程中重用。
接下来看一下Spring JDBC框架的最佳实践:
1)首先定义Dao接口
java代码:
package cn.javass.spring.chapter7.dao; import cn.javass.spring.chapter7.UserModel; public interface IUserDao { public void save(UserModel model); public int countAll(); }
2)定义Dao实现,此处是使用Spring JDBC实现:
java代码:
package cn.javass.spring.chapter7.dao.jdbc; import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource; import org.springframework.jdbc.core.simple.SimpleJdbcDaoSupport; import cn.javass.spring.chapter7.UserModel; import cn.javass.spring.chapter7.dao.IUserDao; public class UserJdbcDaoImpl extends SimpleJdbcDaoSupport implements IUserDao { private static final String INSERT_SQL = "insert into test(name) values(:myName)"; private static final String COUNT_ALL_SQL = "select count(*) from test"; @Override public void save(UserModel model) { getSimpleJdbcTemplate().update(INSERT_SQL, new BeanPropertySqlParameterSource(model)); } @Override public int countAll() { return getJdbcTemplate().queryForInt(COUNT_ALL_SQL); } }
此处注意首先Spring JDBC实现放在dao.jdbc包里,如果有hibernate实现就放在dao.hibernate包里;其次实现类命名如UserJdbcDaoImpl,即×××JdbcDaoImpl,当然如果自己有更好的命名规范可以遵循自己的,此处只是提个建议。
3)进行资源配置(resources/chapter7/applicationContext-resources.xml):
java代码:
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> <property name="locations"> <list> <value>classpath:chapter7/resources.properties</value> </list> </property> </bean>
PropertyPlaceholderConfigurer用于替换配置元数据,如本示例中将对bean定义中的${…}占位符资源用“classpath:chapter7/resources.properties”中相应的元素替换。
java代码:
<bean id="dataSource" class="org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy"> <property name="targetDataSource"> <bean class="org.logicalcobwebs.proxool.ProxoolDataSource"> <property name="driver" value="${db.driver.class}" /> <property name="driverUrl" value="${db.url}" /> <property name="user" value="${db.username}" /> <property name="password" value="${db.password}" /> <property name="maximumConnectionCount" value="${proxool.maxConnCount}" /> <property name="minimumConnectionCount" value="${proxool.minConnCount}" /> <property name="statistics" value="${proxool.statistics}" /> <property name="simultaneousBuildThrottle" value="${proxool.simultaneousBuildThrottle}" /> <property name="trace" value="${proxool.trace}" /> </bean> </property> </bean>
dataSource定义数据源,本示例使用proxool数据库连接池,并使用LazyConnectionDataSourceProxy包装它,从而延迟获取数据库连接;${db.driver.class}将被“classpath:chapter7/resources.properties”中的“db.driver.class”元素属性值替换。
proxool数据库连接池:本示例使用proxool-0.9.1版本,请到proxool官网下载并添加proxool-0.9.1.jar和proxool-cglib.jar到类路径。
ProxoolDataSource属性含义如下:
- driver:指定数据库驱动;
- driverUrl:数据库连接;
- username:用户名;
- password:密码;
- maximumConnectionCount:连接池最大连接数量;
- minimumConnectionCount:连接池最小连接数量;
- statistics:连接池使用样本状况统计;如1m,15m,1h,1d表示没1分钟、15分钟、1小时及1天进行一次样本统计;
- simultaneousBuildThrottle:一次可以创建连接的最大数量;
- trace:true表示被执行的每个sql都将被记录(DEBUG级别时被打印到相应的日志文件);
4)定义资源文件(classpath:chapter7/resources.properties):
java代码:
proxool.maxConnCount=10 proxool.minConnCount=5 proxool.statistics=1m,15m,1h,1d proxool.simultaneousBuildThrottle=30 proxool.trace=false db.driver.class=org.hsqldb.jdbcDriver db.url=jdbc:hsqldb:mem:test db.username=sa db.password=
用于替换配置元数据中相应的占位符数据,如${db.driver.class}将被替换为“org.hsqldb.jdbcDriver”。
5)dao定义配置(chapter7/applicationContext-jdbc.xml):
java代码:
<bean id="abstractDao" abstract="true"> <property name="dataSource" ref="dataSource"/> </bean> <bean id="userDao" class="cn.javass.spring.chapter7.dao.jdbc.UserJdbcDaoImpl" parent="abstractDao"/>
首先定义抽象的abstractDao,其有一个dataSource属性,从而可以让继承的子类自动继承dataSource属性注入;然后定义userDao,且继承abstractDao,从而继承dataSource注入;我们在此给配置文件命名为applicationContext-jdbc.xml表示Spring JDBC DAO实现;如果使用hibernate实现可以给配置文件命名为applicationContext-hibernate.xml。
6) 最后测试一下吧(cn.javass.spring.chapter7. JdbcTemplateTest):
java代码:
@Test public void testBestPractice() { String[] configLocations = new String[] { "classpath:chapter7/applicationContext-resources.xml", "classpath:chapter7/applicationContext-jdbc.xml"}; ApplicationContext ctx = new ClassPathXmlApplicationContext(configLocations); IUserDao userDao = ctx.getBean(IUserDao.class); UserModel model = new UserModel(); model.setMyName("test"); userDao.save(model); Assert.assertEquals(1, userDao.countAll()); }
首先读取配置文件,获取IUserDao接口实现,然后再调用IUserDao接口方法,进行数据库操作,这样对于开发人员使用来说,只面向接口,不关心实现,因此很容易更换实现,比如像更换为hibernate实现非常简单。
spring--JDBC的支持--7