从源码到实战之Spring中的JdbcTemplate及策略模式自定义JdbcTemplate实现

2020-11-03 10:59:57 浏览数 (1)

目 录:

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

  1. 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。其源码分析如下:

代码语言:javascript复制
/**
 * 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);
    }
}
  1. 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);
    }
}
  1. 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 对象 实现代码如下:
代码语言:javascript复制
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 对象集合 实现代码如下:
代码语言:javascript复制
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 实现中,可以了解到策略模式的用法,策略模式是面向接口编程思想的具体体现,通常情况下,作为设计者会暴露出来一个接口,同时可以提供一些接口实现,也可以不提供,而让使用者根据具体情况去编写具体实现代码。以达到灵活的扩展目的。

加油

0 人点赞