Spring-jdbcTempalate研究
很多时候,需要使用jdbcTemplate,既有出于性能考虑的因素,也有出于个人偏好。
关于jdbcTemplate的几个关键性的问题:
一、简介
JdbcTemplate位于org.springframework包,组件标识为spring-jdbc。
处于spring家族的核心区域。spring专注于应用开发,应用开发据大部分和数据库有关,数据库的操作主要由jdbc负责。
用spring.io自己的话说,spring-jdbc就是默默地干了大家不愿意干,但又不得不干的事情。
具体哪些是我们不愿意干的,看spring自己提供的图:
x表示需要做的。
本文不讨论jdbcTemplate是如何做了大家不想做的事情,而是讨论能用jdbcTemplate做什么。
要研究透JdbcTemplate,其实光JdbcTemplate自身是不够,还需要了解jdbc的其它一些内容,如果要彻底研究,请阅读spring.io有关的内容。
限于篇幅,本文只讨论jdbcTempalte等几个template。
关键字列表:
- DataSource
- DataSourceUtils
- Connection
- RowMapper
- SqlParameterSource
- ListMap
- InitializingBean
二、传递SQL参数
从jdbc底层来说,只有一种传递参数的方式,下面来看参考代码:Lesson: JDBC Basics (The Java™ Tutorials > JDBC Database Access) (oracle.com)
Processing SQL Statements with JDBC (The Java™ Tutorials > JDBC Database Access > JDBC Basics) (oracle.com)
public void updateCoffeeSales(HashMap<String, Integer> salesForWeek) throws SQLException { String updateString = "update COFFEES set SALES = ? where COF_NAME = ?"; String updateStatement = "update COFFEES set TOTAL = TOTAL + ? where COF_NAME = ?"; try (PreparedStatement updateSales = con.prepareStatement(updateString); PreparedStatement updateTotal = con.prepareStatement(updateStatement)) { con.setAutoCommit(false); for (Map.Entry<String, Integer> e : salesForWeek.entrySet()) { updateSales.setInt(1, e.getValue().intValue()); updateSales.setString(2, e.getKey()); updateSales.executeUpdate(); updateTotal.setInt(1, e.getValue().intValue()); updateTotal.setString(2, e.getKey()); updateTotal.executeUpdate(); con.commit(); } } catch (SQLException e) { JDBCTutorialUtilities.printSQLException(e); if (con != null) { try { System.err.print("Transaction is being rolled back"); con.rollback(); } catch (SQLException excep) { JDBCTutorialUtilities.printSQLException(excep); } } } }
在原生jdbc中,使用?表示一个参数,?起到占位的作用。
Spring jdbcTemplate为了传递参数方便,支持多种表示参数和设置参数的方式。
表示参数的方式:
a.占位,使用?表示
b.命名,使用":参数名“表示
传递参数的几种方式:
a.不定大小的数组,集合。通常对应占位传参
b.Map,Bean。通常对应命名参数
来看看Spring JdbcTemplate的一些源码:
JdbcTemplate org.springframework.jdbc.core.JdbcTemplate.batchUpdate(String, Collection<T>, int, ParameterizedPreparedStatementSetter<T>) org.springframework.jdbc.core.JdbcTemplate.batchUpdate(String, List<Object[]>) org.springframework.jdbc.core.JdbcTemplate.query(String, Object[], int[], ResultSetExtractor<T>) org.springframework.jdbc.core.JdbcTemplate.update(String, Object...) NamedParamterJdbcTempalte org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate.update(String, Map<String, ?>) org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate.queryForObject(String, SqlParameterSource, Class<T>)
大部分传参都容易理解。命名参数传递总体比较优雅,比较好维护,除了写sql的时候会有那么一点点麻烦。
但我们感兴趣的是SqlParameterSource
我们来看下SqlParameterSource
* @author Thomas Risberg * @author Juergen Hoeller * @since 2.0 * @see NamedParameterJdbcOperations * @see NamedParameterJdbcTemplate * @see MapSqlParameterSource * @see BeanPropertySqlParameterSource */ public interface SqlParameterSource SqlParameterSource 接口有三个真正的实现: org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource org.springframework.jdbc.core.namedparam.MapSqlParameterSource org.springframework.jdbc.core.namedparam.EmptySqlParameterSource 其中BeanPropertySqlParameterSource特别受一些人喜欢(有些人喜欢把任何东西包装成bean) BeanPropertySqlParameterSource的源码注释: SqlParameterSource implementation that obtains parameter valuesfrom bean properties of a given JavaBean object. The names of the beanproperties have to match the parameter names. Uses a Spring BeanWrapper for bean property access underneath.
下面来看看一个例子:
@Override @Transactional(propagation=Propagation.REQUIRED,isolation=Isolation.DEFAULT,rollbackFor=Exception.class) public int addFamilyWithNJT2(String name) { String sql="insert into family(name) values(:name)"; //使用bean/pojo传递参数 Family family=new Family(name); KeyHolder keyHolder=new GeneratedKeyHolder(); SqlParameterSource paramSource=new BeanPropertySqlParameterSource(family); int qty=njdbcTp.update(sql, paramSource, keyHolder); JSONObject.toJSONString(paramSource, true); return keyHolder.getKey().intValue(); }
三、批处理执行
批量执行,多用于数据导入,采集的业务场景。
当然,如果是对付高速大量的数据导入,不建议使用目前这种方式,建议直接使用原生的jdbc或者是数据库产生的api来操作。
只不过,只要咱的数据量不是太大,一般也够用。
下面来个例子:
@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class) @Override public String batchExecute() { /** * 几种基本的batch操作 */ String sql = "insert into family(name,batch_no) values(?,?)"; //1.0 ParameterizedPreparedStatementSetter List<Object[]> argList = new ArrayList<>(); String batchNo = UUID.randomUUID().toString(); for (int i = 0; i < 2; i++) { Object[] a = new Object[2]; a[0] = UUID.randomUUID().toString(); a[1] = batchNo; argList.add(a); } jdbcTp.batchUpdate(sql, argList, 4, (PreparedStatement ps, Object[] argument) -> { ps.setObject(1, argument[0]); ps.setObject(2, argument[1]); }); //2.0 BatchPreparedStatementSetter BatchPreparedStatementSetter btss = new BatchPreparedStatementSetter() { @Override public void setValues(PreparedStatement ps, int i) throws SQLException { ps.setObject(1, argList.get(i)[0]); ps.setObject(2, argList.get(i)[1]); } @Override public int getBatchSize() { //这个大小不能超过参数集合大小,否则会报错。 return argList.size(); } }; int[] qtys = jdbcTp.batchUpdate(sql, btss); int ttlQty = 0; for (int i = 0, len = qtys.length; i < len; i++) { ttlQty += qtys[i]; } System.out.println(ttlQty); return batchNo; }
四、插入并返回自增主键值
@Override public int addFamilyWithJT(String name) { KeyHolder keyHolder = new GeneratedKeyHolder(); jdbcTp.update((Connection con) -> { String sql = "insert into family(name) values(?)"; PreparedStatement ps =con.prepareStatement(sql, new String[]{"custom_id"}); ps.setInt(1, Integer.valueOf(name)); return ps; }, keyHolder); return keyHolder.getKey().intValue(); }
五、查询并返回bean/pojo
@Override public HcsThirdsrv getByName(String serviceName) { String sql="SELECT\r\n" + " service_name,\r\n" + " service_name_cn,\r\n" + " instance_service_name,\r\n" + " service_desc,\r\n" + " status_flag,\r\n" + " add_time,\r\n" + " last_optime\r\n" + "FROM\r\n" + " hcs_thirdsrv \n" + "where service_name=? or service_name_cn=? \n" + "limit 1"; RowMapper<HcsThirdsrv> rowMapper =new BeanPropertyRowMapper<HcsThirdsrv>(HcsThirdsrv.class); HcsThirdsrv srv=this.jdbcTp.queryForObject(sql, rowMapper,serviceName); return srv; }
spring对于返回bean的支持并不友好,希望以后的版本,能够直接出一个不用rowMapper的(现在我们自己都是对jdbcTemplate再封装一遍)。
六、性能
1.比较-mybatis、原生jdbc
主要是和mybatis比较,网上有专门的测试,例如:
https://blog.csdn.net/liulk20170518/article/details/119358143
但不是很多,也比较老旧。此外考虑到mybatis的不断进化。
但毫无疑问,mybatis总会比jdbcTemplate慢一些,因为它花了额外的一些时间做七七八八的处理。
执行速度上,原生jdbc>jdbcTemplate>mybatis,这是没有异议的。
我们很多项目还大量使用mybatis,主要是出于工程考虑:用cpu的速度来弥补工程师的思维能力欠缺和手动速度,以提高工程效率。
灵活优雅,有时候就是慢的代名词。
简单粗暴,有时候能够更快解决问题。
2.如何优化
java的主要性能消耗在于数据转换、反射和解析,后者是先天不可调整,所以只能尽量减少反射操作和数据转换。
所以,如果可能的话,执行sql的时候,尽量使用ListMap或者Map来返回结果。如果您看不习惯没有关系,只要快就可以了。
七、异常
spring提供了几个常见的异常:
- BadSqlGrammarException --语法错误
- CannotGetJdbcConnectionException -- 获取连接异常
- IncorrectResultSetColumnCountException -- 错误结果集合列数异常,例如本来只要一列的,现在有2列
- InvalidResultSetAccessException -- 不可用的结果集合读取异常,通常发生在列位置或者名称设置错误的情况
- JdbcUpdateAffectedIncorrectNumberOfRowsException -- 实际影响行数超出预计的异常。例如本来应该只影响1行,但现在2行
- LobRetrievalFailureException -- 读取大字段数据失败
- SQLWarningException -- sql警告异常。没有特别说明
- UncategorizedSQLException -- 无分类sql异常
实际执行的时候,更可能抛出的是org.springframework.dao下异常,这个包路径属于spring事务模块。
在这个包里面,有更多更在明确的异常说明,例如下图:
八、和spring其它组件关系
影响比较多,其中主要是事务。其余略。
如何保证事务?
从原生jdbc可以看出,要完成一个事务,它的代码大概是这样的:
https://www.cnblogs.com/azhqiang/p/4044127.html ------------------------------------------------------------ private Connection conn = null; private PreparedStatement ps = null; try { conn.setAutoCommit(false); //将自动提交设置为false ps.executeUpdate("修改SQL"); //执行修改操作 ps.executeQuery("查询SQL"); //执行查询操作 conn.commit(); //当两个操作成功后手动提交 } catch (Exception e) { conn.rollback(); //一旦其中一个操作出错都将回滚,使两个操作都不成功 e.printStackTrace(); }
保持事务的关键在于:使用同一个连接。
以oracle为例子,不同的连接就是不同的会话,它们之间的事务是无关的。这个原则在绝大部分rdbms上是一样的成立的,这也即使rdbms存在的主要理由之一。
如果spirng要完成事务的关键就是保证在事务传递的情况下,能够使用同样的一个连接(大部分情况下)。
这里就涉及到spring的事务组件spring-tx和spring-jdbc中的DataSourceUtils。
spring-tx如何如何保证连接的一致性,是一个有点小小复杂的事情,本文略,总之道路就是这个道理。
九、有关工具类
- DataSourceUtils
- JdbcUtils
在只有一个数据源的环境中获取当前数据源(仅限于特定环境):
package study.spring; import javax.sql.DataSource; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.stereotype.Component; @Component public class SpringbootAwareHelper implements BeanFactoryAware, ApplicationContextAware { private static BeanFactory beanFactory; private static ApplicationContext appContext; @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { SpringbootAwareHelper.beanFactory = beanFactory; } public static <T> T getBean(String id, Class<T> type) { return beanFactory.getBean(id, type); } public static <T> T getBean(Class<T> type) { return beanFactory.getBean(type); } @SuppressWarnings("unchecked") public static <T> T getBean(String beanName) { return (T) beanFactory.getBean(beanName); } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { appContext = applicationContext; Object ds = applicationContext.getBean("dataSource"); if (ds != null) { System.out.println("当前的连接池是:" + ds.getClass().getName()); } else { System.out.println("没有连接池!"); } // 打印加载的bean信息 /*String[] beanDefinitionNames = applicationContext.getBeanDefinitionNames(); for (String beanDefinitionName : beanDefinitionNames) { System.out.println(beanDefinitionName); }*/ } public static DataSource getCurrentDatasource() { return appContext.getBean(DataSource.class); } }
十、适用场景
- 对性能要求较高的。
- 个性化要求高的。有些sql在mapper中写有点麻烦,尤其一些复杂的sql。
- 不想多一个依赖的,例如一些核心的东西。能够少依赖就尽量少依赖。这种情况下,可能直接上原生jdbc了,连jdbcTemplate都不用了
因为这个缘故,所以spring提供了spring-jdbc组件。
例如公用组件,核心工具,应该尽量少依赖外部的框架。当然实际做的时候,更可能取决于公司的规模和实例,项目和产品的性质。
如果我们想的再长远一些,那么java是否真有存在的必要性?除了强大的生态,java并没有什么傲人的优势,而计算机最大优势就是人对于速度的最求。所以java要存活下去,则必须
修改jvm和编译器,虽然现在已经不断在优化,但是还不够。最好的办法是提供一个编译器直接在运行主机上进行编译打包,不要耗费时间每次去询问。
一处编译处处执行,并没有那么大的必要性,尤其是做项目。