SpringBoot避坑指南(二)——构建数据访问层

一、JDBC规范

1.1 JDBC简介

JDBC 是 Java Database Connectivity 的全称,它的设计初衷是提供一套能够应用于各种数据库的统一标准,这套标准需要不同数据库厂家之间共同遵守,并提供各自的实现方案供 JDBC 应用程序调用。

作为一套统一标准,JDBC 规范具备完整的架构体系,如下图所示:
在这里插入图片描述
从上图中可以看到,Java 应用程序通过 JDBC 所提供的 API 进行数据访问,而这些 API 中包含了开发人员所需要掌握的各个核心编程对象,下面我们一起来看下。

1.2 JDBC 规范中有哪些核心编程对象?

对于日常开发而言,JDBC 规范中的核心编程对象包括 DriverManger、DataSource、Connection、Statement,及 ResultSet。
正如前面的 JDBC 规范整体架构图中所示,JDBC 中的 DriverManager 主要负责加载各种不同的驱动程序(Driver),并根据不同的请求向应用程序返回相应的数据库连接(Connection),应用程序再通过调用 JDBC API 实现对数据库的操作。

JDBC 中的 Driver 定义如下,其中最重要的是第一个获取 Connection 的 connect 方法:

public interface Driver {
    
    
    //获取数据库连接
    Connection connect(String url, java.util.Properties info)
        throws SQLException;
    boolean acceptsURL(String url) throws SQLException;
    DriverPropertyInfo[] getPropertyInfo(String url, java.util.Properties info)
                         throws SQLException;
    int getMajorVersion();
    int getMinorVersion();
    boolean jdbcCompliant();
    public Logger getParentLogger() throws SQLFeatureNotSupportedException;
}

针对 Driver 接口,不同的数据库供应商分别提供了自身的实现方案。例如,MySQL 中的 Driver 实现类如下代码所示:

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    
    
    // 通过 DriverManager 注册 Driver
    static {
    
    
        try {
    
    
            java.sql.DriverManager.registerDriver(new Driver());
        } catch (SQLException E) {
    
    
            throw new RuntimeException("Can't register driver!");
        }
	}
	....
}

这里就使用了 DriverManager,而 DriverManager 除提供了上述用于注册 Driver 的 registerDriver 方法之外,还提供了 getConnection 方法用于针对具体的 Driver 获取 Connection 对象。

1.3、DataSource

通过前面的介绍,我们知道在 JDBC 规范中可直接通过 DriverManager 获取 Connection,我们也知道获取 Connection 的过程需要建立与数据库之间的连接,而这个过程会产生较大的系统开销。

为了提高性能,通常我们首先会建立一个中间层将 DriverManager 生成的 Connection 存放到连接池中,再从池中获取 Connection。

而我们可以认为 DataSource 就是这样一个中间层,它作为 DriverManager 的替代品而推出,是获取数据库连接的首选方法。

DataSource 在 JDBC 规范中代表的是一种数据源,核心作用是获取数据库连接对象 Connection。在日常开发过程中,我们通常会基于 DataSource 获取 Connection。DataSource 接口的定义如下代码所示:

public interface DataSource  extends CommonDataSource, Wrapper {
    
    
 
  Connection getConnection() throws SQLException;
 
  Connection getConnection(String username, String password)
    throws SQLException;
}

从上面我们可以看到,DataSource 接口提供了两个获取 Connection 的重载方法,并继承了 CommonDataSource 接口。CommonDataSource 是 JDBC 中关于数据源定义的根接口,除了 DataSource 接口之外,它还有另外两个子接口,如下图所示:
在这里插入图片描述

其中,DataSource 是官方定义的获取 Connection 的基础接口,XADataSource 用来在分布式事务环境下实现 Connection 的获取,而 ConnectionPoolDataSource 是从连接池 ConnectionPool 中获取 Connection 的接口。

所谓的 ConnectionPool 相当于预先生成一批 Connection 并存放在池中,从而提升 Connection 获取的效率。

在 JDBC 规范中,除了 DataSource 之外,Connection、Statement、ResultSet 等核心对象也都继承了这个 Wrapper 接口。

作为一种基础组件,它同样不需要开发人员自己实现 DataSource,因为业界已经存在了很多优秀的实现方案,如 DBCP、C3P0 和 Druid 等。

DataSource 的目的是获取 Connection 对象。我们可以把 Connection 理解为一种会话(Session)机制,Connection 代表一个数据库连接,负责完成与数据库之间的通信。

所有 SQL 的执行都是在某个特定 Connection 环境中进行的,同时它还提供了一组重载方法分别用于创建 Statement 和 PreparedStatement。另一方面,Connection 也涉及事务相关的操作。

Connection 接口中定义的方法很丰富,其中最核心的几个方法如下代码所示:

public interface Connection  extends Wrapper, AutoCloseable {
    
    
	//创建 Statement
	Statement createStatement() throws SQLException;
	//创建 PreparedStatement
	PreparedStatement prepareStatement(String sql) throws SQLException;
	//提交
	void commit() throws SQLException;
	//回滚
	void rollback() throws SQLException;
	//关闭连接
	void close() throws SQLException;
}

Statement/PreparedStatement

JDBC 规范中的 Statement 存在两种类型,一种是普通的 Statement,一种是支持预编译的 PreparedStatement。

所谓预编译,是指数据库的编译器会对 SQL 语句提前编译,然后将预编译的结果缓存到数据库中,下次执行时就可以通过替换参数并直接使用编译过的语句,从而大大提高 SQL 的执行效率。

当然,这种预编译也需要一定成本,因此在日常开发中,如果对数据库只执行一次性读写操作时,用 Statement 对象进行处理会比较合适;而涉及 SQL 语句的多次执行时,我们可以使用 PreparedStatement。

如果需要查询数据库中的数据,我们只需要调用 Statement 或 PreparedStatement 对象的 executeQuery 方法即可。

这个方法以 SQL 语句作为参数,执行完后返回一个 JDBC 的 ResultSet 对象。当然,Statement 或 PreparedStatement 还提供了一大批执行 SQL 更新和查询的重载方法,我们无意一一展开。

以 Statement 为例,它的核心方法如下代码所示:

public interface Statement extends Wrapper, AutoCloseable {
    
    
	//执行查询语句
	ResultSet executeQuery(String sql) throws SQLException; 
	//执行更新语句
	int executeUpdate(String sql) throws SQLException; 
	//执行 SQL 语句
	boolean execute(String sql) throws SQLException; 
	//执行批处理
    int[] executeBatch() throws SQLException;
}

ResultSet

一旦我们通过 Statement 或 PreparedStatement 执行了 SQL 语句并获得了 ResultSet 对象,就可以使用该对象中定义的一大批用于获取 SQL 执行结果值的工具方法,如下代码所示:

public interface ResultSet extends Wrapper, AutoCloseable {
    
    
	//获取下一个结果
	boolean next() throws SQLException;
	//获取某一个类型的结果值
	Value getXXX(int columnIndex) throws SQLException;}

ResultSet 提供了 next() 方法便于开发人员实现对整个结果集的遍历。如果 next() 方法返回为 true,意味着结果集中存在数据,可以调用 ResultSet 对象的一系列 getXXX() 方法来取得对应的结果值。
如何使用 JDBC 规范访问数据库?
对于开发人员而言,JDBC API 是我们访问数据库的主要途径,如果我们使用 JDBC 开发一个访问数据库的执行流程,常见的代码风格如下所示(省略了异常处理):

// 创建池化的数据源
PooledDataSource dataSource = new PooledDataSource ();
// 设置 MySQL Driver
dataSource.setDriver ("com.mysql.jdbc.Driver");
// 设置数据库 URL、用户名和密码
dataSource.setUrl ("jdbc:mysql://localhost:3306/test");
dataSource.setUsername("root");
dataSource.setPassword("root");
// 获取连接
Connection connection = dataSource.getConnection();
 
// 执行查询
PreparedStatement statement = connection.prepareStatement ("select * from user");
// 获取查询结果进行处理
ResultSet resultSet = statement.executeQuery();
while (resultSet.next()) {
    
    }
 
// 关闭资源
statement.close();
resultSet.close();
connection.close();

这段代码中完成了对基于前面介绍的 JDBC API 中的各个核心编程对象的数据访问。上述代码主要面向查询场景,而针对用于插入数据的处理场景,我们只需要在上述代码中替换几行代码,即将“执行查询”和“获取查询结果进行处理”部分的查询操作代码替换为插入操作代码就行。
针对前面所介绍的代码示例,我们明确地将基于 JDBC 规范访问关系型数据库的操作分成两大部分:一部分是准备和释放资源以及执行 SQL 语句,另一部分则是处理 SQL 执行结果。

而对于任何数据访问而言,前者实际上都是重复的。在上图所示的整个开发流程中,事实上只有“处理 ResultSet ”部分的代码需要开发人员根据具体的业务对象进行定制化处理。这种抽象为整个执行过程提供了优化空间。诸如 Spring 框架中 JdbcTemplate 这样的模板工具类就应运而生了.

二、JdbcTemplate

2.1 使用

要想在应用程序中使用 JdbcTemplate,首先我们需要引入对它的依赖,如下代码所示:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

JdbcTemplate 提供了一系列的 query、update、execute 重载方法应对数据的 CRUD 操作。

@Service
public class UserServiceImpl implements UserService {
    
    

    final
    JdbcTemplate jdbcTemplate;

    final
    SimpleJdbcInsert simpleJdbcInsert;

    public UserServiceImpl(JdbcTemplate jdbcTemplate) {
    
    
        this.jdbcTemplate = jdbcTemplate;
        // 此处指定user表
        this.simpleJdbcInsert = new SimpleJdbcInsert(jdbcTemplate).withTableName("user");
    }

    @Override
    public List<User> listUser() {
    
    
        String sql = "select id,name,age from user";
        RowMapper<User> rowMapper =
                new BeanPropertyRowMapper<>(User.class);
        return jdbcTemplate.query(sql,rowMapper);
    }

    @Override
    public int insertUser(User user) {
    
    
        Map map = JSON.parseObject(JSON.toJSONString(user), Map.class);
        return simpleJdbcInsert.execute(map);
    }

    @Override
    public int updateUser(User user) {
    
    
        String sql = "update user set name= ?, age = ? where id = ?";
         return jdbcTemplate.update(sql,new Object[]{
    
    user.getName(),user.getAge(),user.getId()});
    }

    @Override
    public int deleteUser(String id) {
    
    
        String sql = "delete from user where id = ?";
        return jdbcTemplate.update(sql,new Object[]{
    
    id});
    }
}

2.2 原理解析

使用 JdbcTemplate 模板工具类完成关系型数据库访问的详细实现过程,通过 JdbcTemplate 不仅简化了数据库操作,还避免了使用原生 JDBC 带来的代码复杂度和冗余性问题。

那么,JdbcTemplate 在 JDBC 基础上如何实现封装的呢?今天,我将带领大家从设计思想出发,讨论 JDBC API 到 JdbcTemplate 的演进过程,并剖析 JdbcTemplate 的部分核心源码。
我们直接看 JdbcTemplate 的 execute(StatementCallback action) 方法,如下代码所示:

public <T> T execute(StatementCallback<T> action) throws DataAccessException {
    
    
        Assert.notNull(action, "Callback object must not be null");
 
        Connection con = DataSourceUtils.getConnection(obtainDataSource());
        Statement stmt = null;
        try {
    
    
            stmt = con.createStatement();
            applyStatementSettings(stmt);
            T result = action.doInStatement(stmt);
            handleWarnings(stmt);
            return result;
        }
        catch (SQLException ex) {
    
    
            String sql = getSql(action);
            JdbcUtils.closeStatement(stmt);
            stmt = null;
            DataSourceUtils.releaseConnection(con, getDataSource());
            con = null;
            throw translateException("StatementCallback", sql, ex);
        }
        finally {
    
    
            JdbcUtils.closeStatement(stmt);
            DataSourceUtils.releaseConnection(con, getDataSource());
        }
}

从以上代码中可以看出,execute 方法中接收了一个 StatementCallback 回调接口,然后通过传入 Statement 对象完成 SQL 语句的执行

StatementCallback 回调接口定义代码如下:

public interface StatementCallback<T> {
    
    
 
    T doInStatement(Statement stmt) throws SQLException, DataAccessException;
}

public void execute(final String sql) throws DataAccessException {
    
    
        if (logger.isDebugEnabled()) {
    
    
            logger.debug("Executing SQL statement [" + sql + "]");
        }
 
        class ExecuteStatementCallback implements StatementCallback<Object>, SqlProvider {
    
    
            @Override
            @Nullable
            public Object doInStatement(Statement stmt) throws SQLException {
    
    
                stmt.execute(sql);
                return null;
            }
            @Override
            public String getSql() {
    
    
                return sql;
            }
        }
 
        execute(new ExecuteStatementCallback());
}

猜你喜欢

转载自blog.csdn.net/luomo0203/article/details/119976578