【重写SpringFramework】JDBC基本实现(chapter 4-1)

171 阅读8分钟

1. 前言

本章介绍数据库事务的相关内容。我们知道,数据库事务与 JDBC 密切相关,在 Spring 框架中,tx 模块和 jdbc 模块是互相依赖的。这一情况是极为罕见的,因为 Spring 为了减少模块之间的耦合程度,尽量遵循单向依赖的原则。为了简化代码,我们将原来 jdbc 模块的内容整合到 tx 模块中,并用一节的篇幅来介绍 jdbc 模块中与事务有关的内容。

2. 数据库连接

2.1 ConnectionHolder

ConnectionHolder 封装了一个 JDBC 连接,事务管理器将该实例绑定到指定的数据源上。transactionActive 属性表示当前连接对象上是否存在事务。

public class ConnectionHolder {
    //数据库连接
    private Connection connection;
    //是否事务存在的标记
    private boolean transactionActive = false;

    public ConnectionHolder(Connection connection) {
        this.connection = connection;
    }
}

2.2 DataSourceUtils

DataSourceUtils 工具类负责管理数据库连接,定义了获取连接和释放连接的方法。

  • getConnection 方法:先尝试获取线程绑定的数据库连接,这一步骤与事务有关,暂未实现。然后从数据源中获取数据库连接。
  • releaseConnection 方法:先尝试关闭线程绑定的数据库连接,如果不存在,则关闭传入的数据库连接。
public class DataSourceUtils {
    public static Connection getConnection(DataSource dataSource) throws SQLException {
        //TODO 尝试获取线程绑定的连接

        //从数据源中获取连接
        return dataSource.getConnection();
    }

    public static void releaseConnection(Connection con, DataSource dataSource) {
        if (con == null) {
            return;
        }

        //TODO 尝试获取线程绑定的连接,并关闭

        //关闭连接
        con.close();
    }
}

需要注意的是,数据库连接有两个来源,一是数据源 DataSource,二是缓存在事务中。由于我们尚未实现事务相关的功能,就目前而言,获取连接和释放连接都直接与数据源打交道。

3. JdbcTemplate

3.1 概述

Spring 对常用的数据库操作进行了封装,JdbcOperations 接口定义了大量方法。出于简化代码的考虑,我们只选取了三个比较常用的方法。

  • query 方法:查询一组数据
  • queryForObject 方法:查询单条数据
  • update 方法:执行增删改操作
public interface JdbcOperations {
    <T> List<T> query(String sql, RowMapper<T> rowMapper, Object... args);
    <T> T queryForObject(String sql, RowMapper<T> rowMapper, Object... args);
    int update(String sql, Object... args);
}

RowMapper 接口的作用是将查询结果转换成一个实体类,row 代表数据表中的一行数据,也称做一条记录,可以映射成一个 Java Bean。

public interface RowMapper<T> {
    T mapRow(ResultSet rs, int rowNum) throws SQLException;
}

JdbcTemplate 实现了 JdbcOperations 接口,由于涉及到原生的 JDBC 操作,持有一个 DataSource 实例以获取数据库连接。

public class JdbcTemplate implements JdbcOperations{
    //数据源
    private DataSource dataSource;

    public JdbcTemplate(DataSource dataSource) {
        this.dataSource = dataSource;
    }
}

3.2 查询操作

query 方法的作用是查询一组数据,我们对查询流程进行了简化处理,分为以下四步:

  1. 获取数据库连接 Connection 和预处理语句 PreparedStatement
  2. 填充查询参数
  3. 调用 executeQuery 方法,执行查询操作
  4. 对返回的数据进行处理,这里使用 RowMapper 接口来处理结果集中的数据
@Override
public <T> List<T> query(String sql, RowMapper<T> rowMapper, Object... args) {
    List<T> list = new ArrayList<>();
    Connection connection;
    PreparedStatement statement = null;
    ResultSet rs = null;

    try {
        //1. 获取数据库连接和预处理语句
        connection = DataSourceUtils.getConnection(this.dataSource);
        statement = connection.prepareStatement(sql);

        //2. 填充查询参数
        for (int i = 0; i < args.length; i++) {
            statement.setObject(i +1, args[i]);
        }
        //3. 执行查询
        rs = statement.executeQuery();
        //4. 对查询结果进行处理
        int n = 1;
        while(rs.next()){
            list.add(rowMapper.mapRow(rs, n++));
        }
    } finally {
        release(rs, statement, connection);
    }
    return list;
}

queryForObject 方法用于查询单条数据,首先调用 query 方法获取查询结果,然后检查返回结果,如果记录数量不是 1 则抛出异常。

@Override
public <T> T queryForObject(String sql, RowMapper<T> rowMapper, Object... args) {
    List<T> results = query(sql, rowMapper, args);

    if (CollectionUtils.isEmpty(results)) {
        throw new DataAccessException("查询单条数据,结果集为空");
    }

    if (results.size() > 1) {
        throw new DataAccessException("期望的查询结果数量为1,实际为" + results.size());
    }
    return results.iterator().next();
}

3.3 修改操作

update 方法用于执行新增、修改、删除操作,主要由三步组成。

  1. 获取数据库连接和预处理语句
  2. 填充参数
  3. 调用 executeUpdate 方法,执行增删改操作,返回受影响的行数
public int update(String sql, Object... args) {
    Connection connection;
    PreparedStatement statement = null;
    try {
        //获取数据库连接和预处理语句
        connection = DataSourceUtils.getConnection(this.dataSource);
        statement = connection.prepareStatement(sql);

        //填充参数
        for (int i = 0; i < args.length; i++) {
            statement.setObject(i + 1, args[i]);
        }
        //执行修改操作
        return statement.executeUpdate();
    } finally {
        release(null, statement);
    }
}

4. 测试

4.1 准备工作

第一步,引入 mysql 数据库驱动和 Druid 数据源,pom.xml 文件的修改如下:

<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <version>9.2.0</version>
    <scope>provided</scope>
</dependency>

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.2.24</version>
    <scope>provided</scope>
</dependency>

需要注意的是,测试代码使用的数据库平台是 mysql 9.0 ,如果读者使用的是 mysql 5.x,请按如下方式修改依赖项。

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.8</version>
    <scope>provided</scope>
</dependency>

第二步,在 resources 目录中创建配置文件 jdbc.properties,内容如下:

#数据库地址
spring.datasource.url=jdbc:mysql:///springwheel?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8
#用户名
spring.datasource.username=root
#密码
spring.datasource.password=root
#数据库驱动
spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver
#数据源类型
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource

如果读者使用的是 mysql 5.x,请修改 driverClassName 这项属性。

spring.datasource.driverClassName=com.mysql.jdbc.Driver

第三步,创建数据库和数据表 t_user,其中 id 为自增主键,phone 为唯一索引。建表语句如下:

CREATE TABLE `t_user`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `phone` varchar(11) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `idx_unique_phone`(`phone`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

第四步,创建配置类,首先通过 @PropertySource 注解加载配置文件,将数据库相关的配置项注入到相应字段上。然后通过单例方法创建 DataSourceJdbcTemplate 实例,最终得到一个可用的 JdbcTemplate 实例。此外,@PropertySource 注解负责扫描 Service 和 DAO 类,这些组件统一存放在 tx.test.common 目录下,详见下文。

//测试类,配置jdbc组件
@Configuration
@PropertySource("jdbc.properties")
@ComponentScan("tx.test.common")
public class DataSourceConfig {
    @Value("${spring.datasource.url}")
    private String url;
    @Value("${spring.datasource.username}")
    private String username;
    @Value("${spring.datasource.password}")
    private String password;
    @Value("${spring.datasource.driverClassName}")
    private String driverClassName;
    @Value("${spring.datasource.type}")
    private Class<? extends DataSource> type;

    //创建数据源
    @Bean
    public DataSource dataSource(){
        DataSource dataSource = this.type.newInstance();
        DataBinder dataBinder = new DataBinder(dataSource);
        MutablePropertyValues pvs = new MutablePropertyValues();
        pvs.addPropertyValue("url", url);
        pvs.addPropertyValue("username", username);
        pvs.addPropertyValue("password", password);
        pvs.addPropertyValue("driverClassName", driverClassName);
        dataBinder.bind(pvs);
        return dataSource;
    }

    //创建模板类
    @Bean
    public JdbcTemplate jdbcTemplate(DataSource dataSource){
        return new JdbcTemplate(dataSource);
    }
}

4.2 新增操作

准备一个实体类和 DAO 类,实体类比较简单,定义了三个属性。

//测试类,实体对象
public class User {
    private int id;
    private String name;
    private String phone;
}

接下来是 DAO 类的实现。首先声明 @Repository 注解,确保可以通过组件扫描的方式加载。然后注入 JdbcTemplate 实例,接下来定义 save 方法来保存数据。(修改和删除操作只是 SQL 语句不同,此处不赘述,读者请自行测试)

//测试类
@Repository
public class UserDao {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    public void save(String name, String phone){
        String sql = "INSERT INTO t_user SET name = ?, phone = ?";
        jdbcTemplate.update(sql, name, phone);
    }
}

测试方法比较简单,首先创建 AnnotationConfigApplicationContext 实例,完成 DataSourceJdbcTemplateUserDao 等主要组件的加载,然后调用 UserDaosave 方法插入数据。

//测试方法
@Test
public void testInsert(){
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DataSourceConfig.class);
    UserDao userDao = context.getBean("userDao", UserDao.class);
    userDao.save("Tom", "12305");
    System.out.println("新增测试");
}

打开数据表 t_user,可以看到数据已经添加进去了。

mysql> select * from t_user;
+----+-------+-------+
| id | name  | phone |
+----+-------+-------+
|  1 | Tom   | 12305 |
+----+-------+-------+
1 rows in set (0.00 sec)

4.3 查询操作

先来看测试类的修改。在 UserDao 类中定义一个 RowMapper 接口的匿名类,用于将查询得到的结果转换成 User 实体类。然后定义两个查询方法,其中 getAll 方法查询所有的用户,getByPhone 方法查询指定手机号的用户。

//测试类
@Repository
public class UserDao {

    private static final RowMapper<User> ROW_MAPPER = new RowMapper<User>() {
        @Override
        public User mapRow(ResultSet rs, int rowNum) throws SQLException {
            User user = new User();
            user.setId(rs.getInt(1));
            user.setName(rs.getString(2));
            user.setPhone(rs.getString(3));
            return user;
        }
    };


    public List<User> getAll(){
        String sql = "SELECT id, phone, name FROM t_user";
        return this.jdbcTemplate.query(sql, ROW_MAPPER);
    }

    public User getByPhone(String phone){
        String sql = "SELECT id, phone, name FROM t_user WHERE phone = ?";
        return this.jdbcTemplate.queryForObject(sql, ROW_MAPPER, phone);
    }
}

查询的测试方法与新增操作的差不多,只是最终调用的是 getAll 方法,获取数据表 t_user 中的所有数据。

//测试方法
@Test
public void testQuery(){
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DataSourceConfig.class);
    UserDao userDao = context.getBean("userDao", UserDao.class);
    List<User> list = userDao.getAll();
    System.out.println("查询测试: " + list);
}

从测试结果可以看到,用户表中所有的数据都查询出来了。

查询测试: [User{id=1, name='Tom', phone='12305'}, User{id=2, name='Jerry', phone='12306'}]

5. 总结

由于数据库事务与 JDBC 密切相关,在深入研究事务的运行机制之前,我们有必要了解一下 Spring 对于 JDBC 是如何封装的。jdbc 原本是独立的模块,为了方便起见,我们将 jdbc 与 tx 模块合并到一起。由于 jdbc 模块不是本章重点,因此只实现一部分功能:

  • 数据库连接的管理,包括获取和释放 Connection 对象
  • JdbcTemplate 模板类,包括增删改查的操作。其中 queryXxx 方法负责查询,update 方法负责增删改。
  • 事务管理器与事务对象(本节不涉及)

6. 项目信息

新增修改一览,新增(12),修改(0)。

tx
├─ src
│  ├─ main
│  │  └─ java
│  │     └─ cn.stimd.spring
│  │        ├─ dao
│  │        │  └─ DataAccessException.java (+)
│  │        └─ jdbc
│  │           ├─ core
│  │           │  ├─ JdbcOperations.java (+)
│  │           │  ├─ JdbcTemplate.java (+)
│  │           │  └─ RowMapper.java (+)
│  │           └─ datasource
│  │              ├─ ConnectionHolder.java (+)
│  │              └─ DataSourceUtils.java (+)
│  └─ test
│     ├─ java
│     │  └─ tx
│     │     ├─ common
│     │     │  ├─ User.java (+)
│     │     │  └─ UserDao.java (+)
│     │     └─ jdbc
│     │        ├─ DataSourceConfig.java (+)
│     │        └─ JdbcTest.java (+)
│	  └─ resources
│	     └─ jdbc.properties (+)
└─ pom.xml (+)

注:+号表示新增、*表示修改

注:项目的 master 分支会跟随教程的进度不断更新,如果想查看某一节的代码,请选择对应小节的分支代码。


欢迎关注公众号【Java编程探微】,加群一起讨论。

原创不易,觉得内容不错请关注、点赞、收藏。