目 录:
1. JdbcTemplate 简单概述
1.1 源码解析
1.2 相关方法说明
1.3 实战演练入门
1.3.1 基本类型测试
1.3.2 大文本对象测试
2. NamedParameterJdbcTemplate 简单概述
2.1 源码分析
2.2 实战演练入门
3. JdbcTemplate 的实现原理解密
3.1 自定义实现 JdbcTemplate
3.2 MyResultSetHandler 接口及实现(策略模式)
3.3 测试MyJdbcTemplate
- JdbcTemplate 简单概述
Spring 对数据库的操作在 JDBC 上面做了基本的封装,让开发者在操作数据库时只需关注SQL语句和查询结果处理器,即可完成对数据库表相应的 CURD 功能(当然,只使用 JdbcTemplate,还不能摆脱持久层 DAO 实现类的编写)。
在配合 Spring 的 IoC 功能,可以把 DataSource 注册到 JdbcTemplate 之中。同时利用 Spring 基于 AOP 的事务即可完成简单的数据库 CRUD 操作。存在多数据源时,可以将不同的 DataSource 注册到 各自的 JdbcTemplate 中,Spring 实现不同对 JdbcTemplate 的 Bean 进行管理,从而实现多数据源操作数据库。
1.1 JdbcTemplate 的全限定名为 org.springframework.jdbc.core.JdbcTemplate
。对于 SpringBoot 项目要使用 JDBC 模板,只需引入 spring-boot-starter-jdbc 坐标,然后通过 @Autowired 注解完成自动注入 JdbcTemplate。其源码分析如下:
/**
* JdbcTemplate实现了JdbcOperations接口,操作方法都定义在此接口中
*/
public class JdbcTemplate extends JdbcAccessor implements JdbcOperations {
/**
* 使用默认构造函数构建JdbcTemplate
*/
public JdbcTemplate() {
}
/**
* 通过数据源构建JdbcTemplate
* @param dataSource
*/
public JdbcTemplate(DataSource dataSource) {
setDataSource(dataSource);
afterPropertiesSet();
}
/**
* 当使用默认构造函数构建时,提供了设置数据源的方法
* @param dataSource
*/
public void setDataSource(@Nullable DataSource dataSource) {
this.dataSource = dataSource;
}
}
1.2 JdbcTemplate 相关方法的说明如下:
代码语言:javascript复制execute 方法:
可以用于执行任何 SQL 语句,一般用于执行 DDL 语句;
update 方法及batchUpdate方法:
update 方法用于执行新增、修改、删除等语句;batchUpdate 方法用于执行批处理相关语句;
query 方法及 queryForXXX 方法:
用于执行查询相关语句;
call 方法:
用于执行存储过程、函数相关语句。
1.3 实战演练入门(分两部分)
以 SpringBoot 应用为例,参考《Spring Boot 快速入门系列(III)—— 数据操作篇之 JdbcTemplate》进行测试:
比如存在一个用户实体如下:
代码语言:javascript复制public class User implements Serializable {
// 主键
private String id;
// 姓名
private String name;
// 年龄
private String age;
// 性别
private String sex;
// 头像
private byte[] image;
// 个人简介
private String description;
/** 省略getter/setter */
}
1.3.1 (基本类型)测试类代码编写如下:
代码语言:javascript复制@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfiguration.class)
public class SpringJdbcTemplateTest {
@Autowired
private JdbcTemplate jdbcTemplate;
@Test
public void testInsert() {
jdbcTemplate.update("insert into user(name, age, sex) values (?,?,?)", "giserway", 18, "1");
}
@Test
public void testUpdate() {
jdbcTemplate.update("update user set name = ?, age = ? where id = ?", "tom", 20, 1);
}
@Test
public void testDelete() {
jdbcTemplate.update("delete from user where id = ?", 1);
}
@Test
public void testFindOne(){
User user = null;
try {
user = jdbcTemplate.queryForObject("select name, age, sex from user where id = ?", new BeanPropertyRowMapper<>(User.class),1);
} catch (Exception e) {
e.printStackTrace();
}
}
@Test
public void testFindAll() {
List<User> users = jdbcTemplate.query("select id,name,age,sex from user", new BeanPropertyRowMapper<>(User.class));
}
@Test
public void testFindCount() {
Long count = jdbcTemplate.queryForObject("select count(*) from user where age > ?", Long.class, 18);
}
@Test
public void testQueryForList() {
List<Map<String, Object>> list = jdbcTemplate.queryForList("select id,name,age,sex from user where age > ?", 18);
for (Map<String, Object> map : list) {
for (Map.Entry<String, Object> me : map.entrySet()) {
System.out.println(me.getKey() "," me.getValue());
}
}
}
@Test
public void testQueryForList2() {
List<String> list = jdbcTemplate.queryForList("select name from user where age > ?", String.class, 18);
for (String name : list) {
System.out.println(name);
}
}
@Test
public void testQueryForMap() {
Map<String, Object> map = jdbcTemplate.queryForMap("select id,name,age,sex from user where id = ?", 1);
for (Map.Entry me : map.entrySet()) {
System.out.println(me.getKey() "," me.getValue());
}
}
}
1.3.2 大文本对象测试类代码编写如下:
- 在数据库中,CLOB 和 BLOB 都是大字段类型。
- CLOB 是可以直接存储文字的,而 BLOB 是按二进制来存储的。
- 其实这两个字段类型是可以互换的的,或者可以直接用 LOB 字段代替这两个。
- 对于 ORACLE 数据库,通常像图片、文件、音乐等信息就用 BLOB 字段来存储,先将文件转为二进制再存储进去。而像文章或者是较长的文字,就用 CLOB 存储,这样对以后的查询更新存储等操作都提供很大的方便。
下面是通过 JdbcTemplate 实现大字段类型的存储,编码测试示例如下:
代码语言:javascript复制@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfiguration.class)
public class SpringJdbcTemplateCLOBAndBLOBTest {
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private LobHandler lobHandler;
@Test
public void testClobBlobWrite() {
try {
FileSystemResource res = new FileSystemResource("D:\test\1.jpg");
byte[] personImg = FileCopyUtils.copyToByteArray(res.getFile());
User user = new User();
user.setId(1);
user.setImage(personImg);
user.setDescription("本人性格热情开朗,待人友好,为人诚实谦虚。工作勤奋,认真负责,能吃苦耐劳,尽职尽责,有耐心。具有亲和力,平易近人,善于与人沟通,也许这天的我没什么值得推荐的荣誉,但是我有一颗简单的心,做好了应对困难的准备,期望您的机会和慧眼,相信我下次在做自我介绍时,会给您一个惊喜。");
jdbcTemplate.execute("update user set image = ?, description =? where id = ?", new AbstractLobCreatingPreparedStatementCallback(lobHandler) {
@Override
protected void setValues(PreparedStatement ps, LobCreator lobCreator) throws SQLException, DataAccessException {
lobCreator.setBlobAsBytes(ps, 1, user.getImage());
lobCreator.setClobAsString(ps, 2, user.getDescription());
ps.setInt(3, user.getId());
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
@Test
public void testClobBlobRead() {
User user = jdbcTemplate.query("select id, name, image, description from user where id = ?", new ResultSetExtractor<User>() {
@Override
public User extractData(ResultSet rs) throws SQLException, DataAccessException {
User user = null;
if (rs.next()) {
user = new User();
user.setId(rs.getInt(1));
user.setName(rs.getString(2));
user.setImage(lobHandler.getBlobAsBytes(rs, 3));
user.setDescription(lobHandler.getClobAsString(rs, 4));
}
return user;
}
}, 3);
System.out.println(user);
}
}
- NamedParameterJdbcTemplate 简单概述
- 在 JDBC 标准用法中, SQL 参数是用占位符 ? 替换
表示,参数绑定受到位置的限制。定位参数的问题在于,一旦参数的顺序发生变化,就必须改变参数绑定。在 Spring JDBC 框架中,绑定 SQL 参数的另一种选择是使用具名参数(named parameter)。
- 那么什么是具名参数?
具名参数:SQL 按名称(以冒号开头)而不是按位置进行指定。具名参数更易于维护,也提升了可读性。具名参数由框架类在运行时用占位符取代,具名参数只在 NamedParameterJdbcTemplate 中得到支持。NamedParameterJdbcTemplate 可以使用全部 jdbcTemplate 对象方法。
2.1 源码分析如下:
代码语言:javascript复制/**
* 通过观察源码我们发现,NamedParameterJdbcTemplate 里面封装了一个JdbcTemplate对象
* 只不过把它看成了接口类型JdbcOperations。
*/
public class NamedParameterJdbcTemplate implements NamedParameterJdbcOperations {
/**
* The JdbcTemplate we are wrapping.
*/
private final JdbcOperations classicJdbcTemplate;
/**
* 通过数据源构建 JdbcOperations
* @param dataSource
*/
public NamedParameterJdbcTemplate(DataSource dataSource) {
Assert.notNull(dataSource, "DataSource must not be null");
this.classicJdbcTemplate = new JdbcTemplate(dataSource);
}
/**
* 使用JdbcOperations 构建一个NamedParameterJdbcTemplate
* @param classicJdbcTemplate
*/
public NamedParameterJdbcTemplate(JdbcOperations classicJdbcTemplate) {
Assert.notNull(classicJdbcTemplate, "JdbcTemplate must not be null");
this.classicJdbcTemplate = classicJdbcTemplate;
}
//...
}
2.2 实战演练入门
代码语言:javascript复制@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfiguration.class)
public class SpringJdbcTemplateUseTest {
@Autowired
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
@Autowired
private JdbcTemplate jdbcTemplate;
@Test
public void testFindMore() {
// 不知道有多少个id需要查询时,使用占位符?极其不方便
// List<User> users = jdbcTemplate.query("select name, age, sex from user where id in (?,?)",new Object[]{1,2},new BeanPropertyRowMapper<> (User.class));
Map<String, List<Integer>> map = new HashMap<>();
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
map.put("ids", list);
List<User> users = namedParameterJdbcTemplate.query("select name, age, sex from user where id in(:ids)", map, new BeanPropertyRowMapper<>(User.class));
System.out.println(users);
}
@Test
public void testNamedParameter() {
User user = new User();
user.setName("jack");
user.setAge(18);
user.setSex("1");
BeanMap beanMap = BeanMap.create(user);
namedParameterJdbcTemplate.update("insert into user(name, age, sex) values (:name, :age, :sex)", beanMap);
}
}
- JdbcTemplate 的实现原理解密
3.1 自定义实现 MyJdbcTemplate
3.1.1 首先导入 pom 文件相关依赖
坐标如下:
代码语言:javascript复制<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.9</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.45</version>
</dependency>
</dependencies>
3.1.2 自定义 MyJdbcTemplate
代码实现编码如下:
代码语言:javascript复制public class MyJdbcTemplate {
// 定义数据源
private DataSource dataSource;
// 通过构造函数给数据源赋值
public MyJdbcTemplate(DataSource dataSource) {
this.dataSource = dataSource;
}
//通过set方法给数据源赋值
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
/**
* 查询方法
* @param sql sql语句
* @param rsh 结果集处理器
* @param params sql语句的参数
* @return
*/
public Object query(String sql, MyResultSetHandler rsh, Object... params) {
//1.判断是否有数据源,没有数据源就直接抛异常
if (dataSource == null) {
throw new NullPointerException("DataSource can not empty!");
}
//2.定义连接和处理对象
Connection connection = null;
PreparedStatement pstm = null;
ResultSet rs = null;
try {
//2.获取连接
connection = dataSource.getConnection();
//3.获取预处理对象
pstm = connection.prepareStatement(sql);
//4.获取参数元信息
ParameterMetaData pmd = pstm.getParameterMetaData();
//5.获取参数个数
int parameterCount = pmd.getParameterCount();
//6.验证参数
if (parameterCount > 0) {
if (params == null) {
throw new NullPointerException("Parameter can not be null !");
}
if (parameterCount != params.length) {
throw new IllegalArgumentException("Incorrect parameter count: expected " String.valueOf(parameterCount) ", actual " String.valueOf(params.length));
}
//7.给参数赋值
for (int i = 0; i < parameterCount; i ) {
pstm.setObject((i 1), params[i]);
}
}
//8.验证通过,执行SQL语句
s = pstm.executeQuery();
//9.处理结果集:策略模式
return rsh.handle(rs);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
release(connection, pstm, rs);
}
}
/**
* 增删改操作
* @param sql
* @param params
* @return
*/
public int update(String sql, Object... params) {
//1.判断是否有数据源,没有数据源就直接抛异常
if (dataSource == null) {
throw new NullPointerException("DataSource can not empty!");
}
//2.定义连接和处理对象
Connection connection = null;
PreparedStatement pstm = null;
try {
//2.获取连接
connection = dataSource.getConnection();
//3.获取预处理对象
pstm = connection.prepareStatement(sql);
//4.获取参数元信息
ParameterMetaData pmd = pstm.getParameterMetaData();
//5.获取参数个数
int parameterCount = pmd.getParameterCount();
//6.验证参数
if (parameterCount > 0) {
if (params == null) {
throw new NullPointerException("Parameter can not be null !");
}
if (parameterCount != params.length) {
throw new IllegalArgumentException("Incorrect parameter count: expected " String.valueOf(parameterCount) ", actual " String.valueOf(params.length));
}
//7.给参数赋值
for (int i = 0; i < parameterCount; i ) {
pstm.setObject((i 1), params[i]);
}
}
//8.验证通过,执行SQL语句
return pstm.executeUpdate();
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
release(connection, pstm, null);
}
}
/**
* 释放数据库连接
* @param conn
* @param pstm
* @param rs
*/
private void release(Connection conn, PreparedStatement pstm, ResultSet rs) {
if (rs != null) {
try {
rs.close();
} catch (Exception e) {
e.printStackTrace();
}
}
if (pstm != null) {
try {
pstm.close();
} catch (Exception e) {
}
if (conn != null) {
try {
conn.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
3.1.3 定义结果集处理接口 MyResultSetHandler
代码语言:javascript复制public interface MyResultSetHandler<T> {
/**
* 处理结果集
* @param rs
* @return
* @throws Exception
*/
Object handle(ResultSet rs) throws Exception;
}
接口两种实现方式如下(使用策略模式):
- 获取 JavaBean 对象 实现代码如下:
public class BeanHandler<T> implements ResultSetHandler {
private Class<T> requiredType;
private BeanListHandler<T> beanListHandler;
/**
* 覆盖默认无参构造
* @param requriedType
*/
public BeanHandler(Class requriedType) {
this.requiredType = requriedType;
}
public BeanHandler(BeanListHandler beanListHandler) {
this.beanListHandler = beanListHandler;
}
@Override
public T handle(ResultSet rs) throws Exception {
if (beanListHandler != null) {
return beanListHandler.handle(rs).get(0);
}
//1.定义返回值
T bean = null;
//2.由于是查询一个,所以只需判断rs能往下走,不用while循环即可
if (rs.next()) {
//3.实例化bean对象
bean = requiredType.newInstance();
// 4.获取参数元信息
ResultSetMetaData rsmd = rs.getMetaData();
//5.取出参数个数
int columnCount = rsmd.getColumnCount();
//6.遍历参数个数
for (int i = 0; i < columnCount; i ) {
//7.取出列名称
String columnLabel = rsmd.getColumnLabel(i 1);
//8.取出列的值
Object value = rs.getObject(columnLabel);
//9.创建实体类的属性描述器,使用内省填充对象数据
PropertyDescriptor pd = new PropertyDescriptor(columnLabel, requiredType);
// 10. 获取属性的写方法
Method method = pd.getWriteMethod();
//11.填充数据
method.invoke(bean, value);
}
}
//返回
return bean;
}
}
- 获取 JavaBean 对象集合 实现代码如下:
public class BeanListHandler<T> implements ResultSetHandler {
private Class<T> requiredType;
/**
* 覆盖默认无参构造
* @param requriedType
*/
public BeanListHandler(Class requriedType) {
this.requiredType = requriedType;
}
@Override
public List<T> handle(ResultSet rs) throws Exception {
//1.定义返回值
List<T> list = new ArrayList();
T bean = null;
//2.由于是查询一个,所以只需判断rs能往下走,不用while循环即可
if (rs.next()) {
//3.实例化bean对象
bean = requiredType.newInstance();
//4.获取参数元信息
ResultSetMetaData rsmd = rs.getMetaData();
//5.取出参数个数
int columnCount = rsmd.getColumnCount();
//6.遍历参数个数
for (int i = 0; i < columnCount; i ) {
//7.取出列名称
String columnLabel = rsmd.getColumnLabel(i 1);
//8.取出列的值
Object value = rs.getObject(columnLabel);
//9.创建实体类的属性描述器,使用内省填充对象数据
PropertyDescriptor pd = new PropertyDescriptor(columnLabel, requiredType);
//10.获取属性的写方法
Method method = pd.getWriteMethod();
//11.填充数据
method.invoke(bean, value);
}
//12.给list填充数据
list.add(bean);
}
//返回
return list;
}
}
3.2 测试自定义的 MyJdbcTemplate
3.2.1 新建配置类及自定义 Bean 对象的注入
代码语言:javascript复制/**
* 配置类
*/
@Configuration
@Import(JdbcConfig.class)
@PropertySource("classpath:jdbc.properties")
public class SpringConfiguration {
}
代码语言:javascript复制public class JdbcConfig {
@Value("${jdbc.driver}")
private String driver;
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String username;
@Value("${jdbc.password}")
private String password;
@Bean
public MyJdbcTemplate createJdbcTemplate(DataSource dataSource) {
return new MyJdbcTemplate(dataSource);
}
@Bean
public DataSource createDataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(driver);
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
}
}
3.2.2 在 resources/ 下新建配置文件
jdbc.properties,内容如下:
代码语言:javascript复制jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/db_test
jdbc.username=root
jdbc.password=123456
3.2.3 新建测试类
测试代码如下:
代码语言:javascript复制@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfiguration.class)
public class SpringJdbcTemplateUseTest {
@Autowired
private MyJdbcTemplate jdbcTemplate;
@Test
public void testInsert() {
jdbcTemplate.update("insert into user(name, age, sex) values (?,?,?)", "giserway", 18, "1");
}
@Test
public void testUpdate() {
jdbcTemplate.update("update user set name = ?, age = ? where id = ?", "tom", 20, 1);
}
@Test
public void testDelete() {
jdbcTemplate.update("delete from user where id = ?", 1);
}
@Test
public void testFindOne() {
User user = (User) jdbcTemplate.query("select name, age, sex from user where id = ?", new BeanHandler<>(User.class), 1);
System.out.println(user);
}
@Test
public void testFindList() {
List<User> users = (List<User>) jdbcTemplate.query("select name, age, sex from user where age > 18", new BeanListHandler<>(User.class));
for (User user : users) {
System.out.println(user);
}
}
}
4. 小结
本文从 JdbcTemplate 实现持久层入门到自定义实现,了解并掌握 JdbcTemplate 的基本使用及其实现原理;从自定义 JdbcTemplate 实现中,可以了解到策略模式的用法,策略模式是面向接口编程思想的具体体现,通常情况下,作为设计者会暴露出来一个接口,同时可以提供一些接口实现,也可以不提供,而让使用者根据具体情况去编写具体实现代码。以达到灵活的扩展目的。
加油