文章目录
JdbcTemplate浅学
前面我们学习了有关JDBC的封装,如何让JDBC使用更加灵活。用到的技术手段简单应用有很多。现在小结如下:
- 利用反射,得到结果集的参数,从而实现查询更加灵活。
- 模板设计模式,只需要sql语句和占位符参数,可以实现不只一个ORM对象的增删改操作
- 策略模式,大大提高了增删改的功能差异修改
- DAO、ORM
- 代理模式,实现了数据库连接池等操作,静态代理、动态代理(结合反射)
- 工厂模式
- Apache的开源项目DBCP数据库连接池的使用
那么,上面的这些对JDBC的封装优化等,spring-Jdbc已经做好了。更加的强大!
1、什么是spring-jdbc
1.1、简介
Spring对数据库的操作在jdbc上面做了深层次的封装,使用spring的注入功能,可以把DataSource注册到JdbcTemplate之中。JdbcTemplate其全限定命名:org.springframework.jdbc.core.JdbcTemplate。要使用JdbcTemlate还需一个这个包,其包含了一下事务和异常控制
Spring JDBC – 框架和应用开发者各自分工
也就是说Spring-JDBC帮你屏蔽了很多JDBC底层繁琐的API操作、让你更方便的开发
1.2、spring-jdbc层级包
org.springframework.jdbc.core
包含JdbcTemplate 类和它各种回调接口、外加一些相关的类。它的一个子包
org.springframework.jdbc.core.simple包含SimpleJdbcInsert和SimpleJdbcCall等类。另一个叫org.springframework.jdbc.core.namedparam的子包包含NamedParameterJdbcTemplate及它的一些工具类。org.springframework.jdbc.datasource
包含DataSource数据源访问的工具类,以及一些简单的DataSource实现用于测试和脱离JavaEE容器运行的JDBC代码。子包org.springfamework.jdbc.datasource.embedded提供Java内置数据库例如HSQL, H2, 和Derby的支持。org.springframework.jdbc.object
包含用于在RDBMS查询、更新和存储过程中创建线程安全及可重用的对象类。这种方式类似于JDO的查询方式,不过查询返回的对象是与数据库脱离的。此包针对JDBC做了很多上层封装、而底层依赖于org.springframework.jdbc.core包。
org.springframework.jdbc.support
- 包含SQLException的转换类和一些工具类。JDBC处理过程中抛出的异常会被转换成org.springframework.dao里面定义的异常类。也就是说,凡是使用Spring的JDBC封装层的代码无需实现任何JDBC或者RDBMS相关的异常处理。所有的这些被转化的异常都是unchecked异常,因而也给了你一种额外的选择,你可以抓住这些异常,从而转化成其他类型的异常被允许调用者传播。
1.3、Jdbc-Template
JdbcTemplate
JdbcTemplate是JDBC core包里面的核心类
。它封装了对资源的创建和释放,可以帮你避免忘记关闭连接等常见错误。它也包含了核心JDBC工作流的一些基础工作、例如执行和声明语句,而把SQL语句的生成以及查询结果的提取工作留给应用代码。JdbcTemplate执行查询、更新SQL语句和调用存储过程,运行结果集迭代和抽取返回参数值。它也可以捕获JDBC异常并把它们转换成更加通用、解释性更强的异常层次结构、这些异常都定义在org.springframework.dao包
里面。
当你在代码中使用了JdbcTemplate类,你只需要实现回调接口。PreparedStatementCreator回调接口通过传入的Connection类(该类包含SQL和任何必要的参数)创建已声明的语句。CallableStatementCreator也提供类似的方式、该接口用于创建回调语句。RowCallbackHandler用于获取结果集每一行的值。
可以在DAO实现类中通过传入DataSource引用来完成JdbcTemplate的初始化;也可以在Spring IOC容器里面配置、作为DAO bean的依赖Bean配置。
DataSource最好在Spring IOC容器里作为Bean配置起来。在上面第一种情况下,DataSource bean直接传给相关的服务;第二种情况下DataSource bean传递给JdbcTemplate bean。
JdbcTemplate中使用的所有SQL以“DEBUG”级别记入日志(一般情况下日志的归类是JdbcTemplate对应的全限定类名,不过如果需要对JdbcTemplate进行定制的话,可能是它的子类名)
1.4、JdbcTemplate提供的方法
JdbcTemplate主要提供以下五类方法:
- execute方法:可以用于执行任何SQL语句,一般用于执行DDL语句;
- update方法及batchUpdate方法:update方法用于执行新增、修改、删除等语句;batchUpdate方法用于执行批处理相关语句;
- query方法及queryForXXX方法:用于执行查询相关语句;
- call方法:用于执行存储过程、函数相关语句。
JdbcTemplate实例一旦配置之后是线程安全的。这点很重要因为这样你就能够配置JdbcTemplate的单例,然后安全的将其注入到多个DAO中(或者repositories)。JdbcTemplate是有状态的,内部存在对DataSource的引用,但是这种状态不是会话状态。
使用JdbcTemplate类的常用做法是在你的Spring配置文件里配置好一个DataSource,然后将其依赖注入进你的DAO类中
1.5、 JdbcTemplate与其他访问JDBC方案
-
JdbcTemplate -
这是经典的也是最常用的Spring对于JDBC访问的方案。这也是最低级别的封装
, 其他的工作模式事实上在底层使用了JdbcTemplate作为其底层的实现基础。JdbcTemplate在JDK 1.4以上的环境上工作得很好。 -
NamedParameterJdbcTemplate -
对JdbcTemplate做了封装,提供了更加便捷的基于命名参数的使用方式而不是传统的JDBC所使用的“?”作为参数的占位符
。这种方式在你需要为某个SQL指定许多个参数时,显得更加直观而易用。该特性必须工作在JDK 1.4以上。 -
SimpleJdbcTemplate -
这个类结合了JdbcTemplate和NamedParameterJdbcTemplate的最常用的功能,同时它也利用了一些Java 5的特性所带来的优势
,例如泛型、varargs和autoboxing等,从而提供了更加简便的API访问方式。需要工作在Java 5以上的环境中。 -
SimpleJdbcInsert 和 SimpleJdbcCall - 这两个类可以充分利用数据库元数据的特性来简化配置。通过使用这两个类进行编程,你可以仅仅提供数据库表名或者存储过程的名称以及一个Map作为参数。其中Map的key需要与数据库表中的字段保持一致。
这两个类通常和SimpleJdbcTemplate配合使用。这两个类需要工作在JDK 5以上,同时数据库需要提供足够的元数据信息。
-
RDBMS 对象包括MappingSqlQuery, SqlUpdate and StoredProcedure -
这种方式允许你在初始化你的数据访问层时创建可重用并且线程安全的对象。该对象在你定义了你的查询语句,声明查询参数并编译相应的Query之后被模型化。
一旦模型化完成,任何执行函数就可以传入不同的参数对之进行多次调用。这种方式需要工作在JDK 1.4以上。
2、使用JdbcTemplate实现CRUD操作
1、数据源
datasource = BasicDataSourceFactory.createDataSource(properties);
2、实现CRUD
/**
* 使用JdbcTemplate实现对数据的增删改查。
* 1、什么是JdbcTemplate?
* + JdbcTemplate是Spring提供的访问数据库的方式之一,是Spring中最基本、最底层的访问数据库的实现方式。
* + 通过使用JdbcTemplate,开发者无需关心数据库连接的创建和关闭细节,只需要专注于实现业务逻辑即可。
* + 在使用JdbcTemplate的时候,只需要声明即可,无需自己初始化,因为Spring在初始化数据源datasource的时候会
* 自己创建JdbcTemplate的实例。
*/
public class JdbcTemplateTest implements Dao {
//使用数据源连接池来注册JDBC
private JdbcTemplate jdbc = new JdbcTemplate(JDBCUtils.getDatasource());
public static void main(String[] args) {
JdbcTemplateTest jdbcTest = new JdbcTemplateTest();
User user = new User();
user.setName("李一平");
user.setMoney(500f);
jdbcTest.addUser(user);
System.out.println("插入user: " + user);
}
@Override
public void addUser(User user) {
final String sql = "insert into bank(name,money) values(?,?)";
final Object[] args = new Object[]{
user.getName(), user.getMoney()};
//获取刚插入的主键并返回
KeyHolder holder = new GeneratedKeyHolder();
jdbc.update(new PreparedStatementCreator() {
@Override
public PreparedStatement createPreparedStatement(Connection connection) throws SQLException {
PreparedStatement ps = connection.prepareStatement(sql, new String[]{
"id"});
for (int i = 0; i < args.length; i++) {
ps.setObject(i + 1, args[i]);
}
return ps;
}
}, holder);
int id = Objects.requireNonNull(holder.getKey()).intValue();
user.setId(id);
}
@Override
public void deleteUser(String userName) {
//删除操作
String sql = "delete from bank where name=?";
Object[] args = new Object[]{
userName};
jdbc.update(sql, args);
}
@Override
public void updateUser(User user) {
//更新操作
String sql = "update bank set name=? set money=? where id=?";
Object[] args = new Object[]{
user.getName(), user.getMoney(), user.getId()};
jdbc.update(sql, args);//返回影响的记录条数
}
@Override
public User findUser(String userName) {
//查询
String sql = "select * from bank where name=?";
Object[] args = new Object[]{
userName};
//我们原本的实现是创建一个结果集处理接口,实现处理方法。JdbcTemplate类似,只不过是利用了结果集参数反射获取属性值。
User user = (User) jdbc.queryForObject(sql, args, new BeanPropertyRowMapper<User>(User.class));
if (user != null)
return user;
else throw new NullPointerException("空异常!");
}
public String findUserById(int id) {
String sql = "select name from bank where id=?";
Object[] args = new Object[]{
id};
return jdbc.queryForObject(sql, args, String.class);
}
@Override
public boolean toUser(User user, User toUser, float money) {
if (money > 0.0001) {
user.setMoney(user.getMoney() - money);
toUser.setMoney(toUser.getMoney() + money);
updateUser(user);
updateUser(toUser);
return true;
} else
throw new RuntimeException("转账金额不能为负数!");
}
}
3、JdbcTemplate常用方法和其他一些类
我们在IDEA里面点击该类,然后点击View-》ToolWindows-》Structure
3.1、查询
-
queryForObject
spring 3.2.2 版本之后jdbcTemplate.queryForInt()和jdbcTemplate.queryForLong() 就取消了,全部用queryForObject代替了
-
queryForObject(sql, requiredType)
本质上和queryForInt相同,只是可以返回不同的对象,例如返回一个String对象
2个参数——sql语句 、期待返回来的对象类型 -
queryForObject(sql, requiredType,args…)
第三个参数是个可变参数 -
queryForObject(sql, args[],requiredType)
第二个参数是个参数数组 -
queryForObject(sql, rowMapper)
注意,这里查询必须保证只能查询一条数据,否则会报错。 -
queryForList
返回一个装有map的list,每一个map是一条记录
,map里面的key是字段名
List<Map<String, Object>> rows= jdbcTemplate.queryForList(“SELECT * FROM user”); -
queryForMap
这个查询只能是查询一条记录的查询,返回一个map,key的值是column的值 -
queryForRowSet
返回一个结果集然后调用.getString或者getInt等去取值 -
query()方法
3.2、更新
-
execute
执行sql语句,无返回执,用于更新操作(增、删、改) -
update
更新操作,返回受影响的行数 -
batchUpdate
执行批量更新,参数为string数组
3.3、get方法
3.4、RowMapper
sping中的RowMapper可以将数据中的每一行数据封装成用户定义的类。
我们在数据库查询中,如果返回的类型是用户自定义的类型(其实我们在数据库查询中大部分返回的都是自定义的类)则需要包装,如果是Java自定义的类型,如:String则不需要。
如果sping与hibernate 相结合了,基本上是用不到,大多数都是在spring单独使用时用到,常见的情况就是与JdbcTemplate一起使用。
可以通过建立内部类实现RowMapper接口,RowMapper中有一个mapRow方法,所以实现RowMapper接口一定要实现mapRow方法,而对自定义类的包装就在mapRow方法中实现。
3.5、BeanPropertyRowMapper
BeanPropertyRowMapper是RowMapper将表行转换为指定映射目标类的新实例的实现
。映射的目标类必须是顶级类,并且必须具有默认或无参数构造函数
。
使用BeanPropertyRowMapper自动绑定,需要确保数据库表列名称与Java实体类属性名称相同
,否则使用别名,则对应的属性值返回的是null或者默认值,而不是报错。
如:
@Override
public User findUser(String userName) {
//查询
String sql = "select id as i,name as n,money as m from bank where name=?";
Object[] args = new Object[]{
userName};
//我们原本的实现是创建一个结果集处理接口,实现处理方法。JdbcTemplate类似,只不过是利用了结果集参数反射获取属性值。
User user = (User) jdbc.queryForObject(sql, args, new BeanPropertyRowMapper<User>(User.class));
if (user != null)
return user;
else throw new NullPointerException("空异常!");
}
测试代码:
JdbcTemplateTest jdbcTest = new JdbcTemplateTest();
User user = new User();
jdbcTest.findUser("王二小");
System.out.println(user);
将sql改为:
String sql = "select id,name,money as m from bank where name=?";
再测试:
3.6、KeyHolder and GeneratedKeyHolder
KeyHolder用于获取约束键,如主键。是一个接口。
public interface KeyHolder {
Number getKey() throws InvalidDataAccessApiUsageException;
Map<String, Object> getKeys() throws InvalidDataAccessApiUsageException;
List<Map<String, Object>> getKeyList();
}
而spring有对KeyHolder的默认实现GeneratedKeyHolder。
Spring利用GeneratedKeyHolder,提供了一个可以返回新增记录对应主键值的方法:
int update(PreparedStatementCreator psc, KeyHolder generatedKeyHolder)
通用的实现类GeneratedKeyHolder,该类返回新增记录时的自增长主键值。对应的方法是getKey(),注意返回的类型是number。使用时需要转化。
如获取插入User的主键id:
@Override
public void addUser(User user) {
final String sql = "insert into bank(name,money) values(?,?)";
final Object[] args = new Object[]{
user.getName(), user.getMoney()};
//获取刚插入的主键并返回
KeyHolder holder = new GeneratedKeyHolder();
jdbc.update(new PreparedStatementCreator() {
@Override
public PreparedStatement createPreparedStatement(Connection connection) throws SQLException {
PreparedStatement ps = connection.prepareStatement(sql, new String[]{
"id"});
for (int i = 0; i < args.length; i++) {
ps.setObject(i + 1, args[i]);
}
return ps;
}
}, holder);
int id = Objects.requireNonNull(holder.getKey()).intValue();
user.setId(id);
}
解析:
-
keyHolder 是数据库自增主键值的持有者,它监听PreparedStatement的返回的值
-
Statement.RETURN_GENERATED_KEYS获取主键值,并存放在自己的池中(实际上是一个list);也可以传入具体类型参数。但是声明获取主键值更加规范。
-
一般来说,一个keyHolder实例只绑定一个PreparedStatement的执行,当然最好也只是插入一条数据库记录,这样才能保证池中只有一个主键值。
-
当keyHolder获得主键值后,您可以在任何时候通过访问keyHolder对象得到这个主键值,也就是说只要它的生命期存在,这个主键的值就一直不会丢失。
总结:
-
在执行PreparedStatement之前创建自增主键的持有者对象keyHolder
-
在创建PreparedStatement对象时一定要声明返回主键值,如:
-
只要keyHolder的生命期存在,那么主键的值在任何时候与位置你都可以取得到