MyBatis3源码深度解析(二)JDBC API简介

前言

MyBatis框架对JDBC做了轻量级的封装,因此在开始学习MyBatis源码之前,有必要全面地了解JDBC规范。本章使用的JDBC版本是4.2。

第2章 JDBC规范详解

2.1 JDBC API简介

JDBC(Java Database Connectivity)是Java语言中提供的访问关系型数据库的接口

使用JDBC API可以执行SQL语句、检索SQL执行结果以及将数据更改写回底层数据源。

JDBC API基于X/Open SQL CLI,是ODBC的基础,为Java程序提供了访问一个或多个数据源的方法。

使用JDBC操作数据源大致需要以下几个步骤:

(1)与数据源建立连接。
(2)执行SQL语句。
(3)检索SQL执行结果。
(4)关闭连接。

2.1.1 建立数据源连接

JDBC API中定义了Connection接口,用来表示与底层数据源的连接。

JDBC应用程序可以使用以下两种方式获取Connection对象:

2.1.1.1 DriverManager

这是一个在JDBC 1.0规范中就已经存在、完全由JDBC API实现的驱动管理类。当应用程序第一次尝试通过URL连接数据源时,DriverManager会自动加载classpath下所有的JDBC驱动。

DriverManager类提供了一系列重载的getConnection()方法,用来获取Connection对象,例如:

// 获取Connection对象
Connection connection = DriverManager.getConnection("jdbc:hsqldb:mem:mybatis", "sa", "");
2.1.1.2 DataSource

这是在DBC 2.0规范中引入的接口。相比DriverManager,它提供了更多底层数据源的细节,而且对应用来说,不需要关注JDBC驱动的实现。

一个DataSource对象的属性被设置后,它就代表一个特定的数据源。当DataSource实例的getConnection()方法被调用后,DataSource实例就会返回一个与数据源建立连接的Connection对象。

在应用程序中修改DataSource对象的属性后,它就代表一个新的数据源,通过getConnection()方法可以获取指向这个新的数据源的Connection对象。同样,数据源的具体实现修改后,不需要修改应用程序代码。

需要注意的是,JDBC API中只提供了DataSource接口,没有提供DataSource的具体实现,DataSource具体的实现由JDBC驱动程序提供。另外,目前一些主流的数据库连接池(例如DBCP、C3P0、Druid等)也提供了DataSource接口的具体实现

MyBatis框架提供了DataSource接口的实现,其实现方法如下:

// 创建DataSource实例
DataSource dataSource = new UnpooledDataSource("org.hsqldb.jdbcDriver",
    "jdbc:hsqldb:mem:mybatis", "sa", "");
// 获取Connection对象
Connection connection = dataSource.getConnection();

MyBatis框架还提供了DataSource的工厂,即DataSourceFactory,支持使用工厂模式创建DataSource实例。代码如下:

// 创建DataSource实例
DataSourceFactory dsf = new UnpooledDataSourceFactory();
Properties properties = new Properties();
InputStream configStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("database.properties");
properties.load(configStream);
dsf.setProperties(properties);
DataSource dataSource = dsf.getDataSource();
// 获取Connection对象
Connection connection = dataSource.getConnection();

另外,JDBC API中定义了两个DataSource接口比较重要的扩展,用于支撑企业级应用。这两个接口分别为:

  • ConnectionPoolDataSource:支持缓存和复用Connection对象,这样能够在很大程度上提升应用性能和伸缩性。
  • XADataSource:该实例返回的Connection对象能够支持分布式事务。

2.1.2 执行SQL语句

JDBC与数据源建立连接后,接下来是要对数据源进行查询、新增、更新或删除等操作,即执行SQL语句。

执行SQL语句使用的是Statement接口,该接口是JDBC API中提供的SQL语句执行器,调用Statement接口中定义的executeQuery()方法执行查询操作,调用executeUpdate()方法执行更新操作,另外还可以调用executeBatch()方法执行批量处理操作。

甚至,当不确定SQL语句的类型时,可以通过调用execute()方法进行统一的操作,根据返回值来判断SQL语句类型,通过Statement接口提供的getResultSet()方法来获取查询结果集,或者通过getUpdateCount()方法来获取更新操作影响的行数.

下面是一个通过Statement执行查询操作的案例:

Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery("select * from user");

2.1.3 处理SQL执行结果

SQL语句执行完毕后,解析来要处理SQL执行结果。例如执行一条SELECT语句后,需要获取查询的结果,执行UPDATE或者INSERT语句后,需要通过影响的记录数来确定是否更新成功。

DBC API中提供了ResultSet接口,该接口的实现类封装SQL查询的结果,对ResultSet对象进行遍历,通过其提供的一系列getXXX()方法(例如getString)获取查询结果集。

下面是一个遍历ResultSet的案例:

ResultSet resultSet = statement.executeQuery("select * from user");
// 遍历ResultSet
ResultSetMetaData metaData = resultSet.getMetaData();
int columCount = metaData.getColumnCount();
while (resultSet.next()) {
    for (int i = 1; i <= columCount; i++) {
        String columName = metaData.getColumnName(i);
        String columVal = resultSet.getString(columName);
        System.out.println(columName + ":" + columVal);
    }
    System.out.println("--------------------------------------");
}
2.1.4 使用JDBC操作数据库

将以上关键步骤整合起来,得到以下单元测试:

@Test
public void testJdbc() {
    // 初始化数据
    DbUtils.initData();
    try {
        // 加载驱动
        Class.forName("org.hsqldb.jdbcDriver");
        // 获取Connection对象
        Connection connection = DriverManager.getConnection("jdbc:hsqldb:mem:mybatis", "sa", "");
        // 获取Statement对象
        Statement statement = connection.createStatement();
        // 执行SQL语句,得到ResultSet
        ResultSet resultSet = statement.executeQuery("select * from user");
        // 遍历ResultSet
        ResultSetMetaData metaData = resultSet.getMetaData();
        int columCount = metaData.getColumnCount();
        while (resultSet.next()) {
            for (int i = 1; i <= columCount; i++) {
                String columName = metaData.getColumnName(i);
                String columVal = resultSet.getString(columName);
                System.out.println(columName + ":" + columVal);
            }
            System.out.println("--------------------------------------");
        }
        // 关闭连接
        IOUtils.closeQuietly(statement);
        IOUtils.closeQuietly(connection);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

代码中包含两个自定义的工具类DbUtils和IOUtils,方便后续测试使用。

public abstract class DbUtils {

    public static Connection initData() {
        Connection conn = null;
        // 加载HSQLDB驱动
        try {
            Class.forName("org.hsqldb.jdbcDriver");
            // 获取Connection对象
            conn = DriverManager.getConnection("jdbc:hsqldb:mem:mybatis", "sa", "");
            // 使用Mybatis的ScriptRunner工具类执行数据库脚本
            ScriptRunner scriptRunner = new ScriptRunner(conn);
            // 不输出sql日志
            scriptRunner.setLogWriter(null);
            scriptRunner.runScript(Resources.getResourceAsReader("create-table.sql"));
            scriptRunner.runScript(Resources.getResourceAsReader("init-data.sql"));
        } catch (Exception e) {
            e.printStackTrace();
        }
        return conn;
    }
}

public abstract class IOUtils {

    public static void closeQuietly(Closeable closeable) {
        if(closeable != null) {
            try {
                closeable.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static void closeQuietly(AutoCloseable closeable) {
        if(closeable != null) {
            try {
                closeable.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

执行该单元测试,会查询出user表中的所有记录并输出到控制台,如下:

ID:0
CREATE_TIME:2024-02-21 10:24:30
NAME:User1
PASSWORD:pwd
PHONE:18705464523
NICK_NAME:User1
---------------------------------------
ID:1
CREATE_TIME:2024-02-21 10:24:30
NAME:User2
PASSWORD:pwd
PHONE:18705464523
NICK_NAME:User2
---------------------------------------
ID:2
......

2.2 JDBC API中的类与接口

查阅官方文档可知,JDBC API由java.sql和javax.sql两个包构成。

2.2.1 java.sql包

java.sql包中涵盖JDBC最核心的API,包括数据类型、枚举、API相关、驱动相关、异常等:

#数据类型
java.sql.Array
java.sql.Blob
java.sql.Clob
java.sql.Date
java.sql.NClob
java.sql.Ref
java.sql.Struct
java.sql.Time
java.sql.RowId
java.sql.Timestamp
java.sql.SQLData
java.sql.SQLInput
java.sql.SQLOutput
java.sql.SQLXML

#枚举
java.sql.SQLType
java.sql.JDBCType
java.sql.Types
java.sql.RowIdLifetime
java.sql.PseudoColumnUsage
java.sql.ClientInfoStatus

#API相关
java.sql.Wrapper
java.sql.Connection
java.sql.Statement
java.sql.CallableStatement
java.sql.PreparedStatement
java.sql.DatabaseMetaData
java.sql.ParameterMetaData
java.sql.ResultSet
java.sql.ResultSetMetaData

#驱动相关
java.sql.Driver
java.sql.DriverAction
java.sql.DriverManager
java.sql.DriverPropertyInfo
java.sql.SQLPermission
java.sql.Savepoint

#异常
java.sql.BatchUpdateException
java.sql.DataTruncation
java.sql.SQLClientInfoException
java.sql.SQLDataException
java.sql.SQLException
java.sql.SQLFeatureNotSupportedException
java.sql.SQLIntegrityConstraintViolationException
java.sql.SQLInvalidAuthorizationSpecException
java.sql.SQLNonTransientConnectionException
java.sql.SQLNonTransientException
java.sql.SQLRecoverableException
java.sql.SQLSyntaxErrorException
java.sql.SQLTimeoutException
java.sql.SQLTransactionRollbackException
java.sql.SQLTransientConnectionException
java.sql.SQLTransientException
java.sql.SQLWarning

其中API相关的几个接口(Connection、Statement、ResultSet等)是开发中会实际操作的。这些接口都继承了java.sql.Wrapper接口。

Connection、Statement、ResultSet之间的关系如图所示:

Connection、Statement、ResultSet之间的关系
许多JDBC驱动程序会提供超越传统JDBC的扩展,驱动厂商可能会在其原始类型的基础上进行包装,使之符合JDBC API规范(如Oracle数据库驱动的OracleStatement),而Wrapper接口提供了访问这些原始类型的功能,从而可以使用JDBC驱动中一些非标准的特性。

public interface Wrapper {
    <T> T unwrap(java.lang.Class<T> iface) throws java.sql.SQLException;
    boolean isWrapperFor(java.lang.Class<?> iface) throws java.sql.SQLException;
}

由java.sql.Wrapper的源码可知,该接口提供了两个方法:

  • unwrap:返回未经过包装的JDBC驱动原始类型实例。
  • isWrapperFor:判断当前实例是否是JDBC驱动中某一类型的包装类型。

例如,Oracle数据库驱动中提供了一些非JDBC标准的方法,如果需要使用这些非标准JDBC的方法,则可用调用Wrapper接口的unwrap方法获取到Oracle驱动的原始类型(如OracleStatement),然后调用原始类型提供的非标准方法就可以访问Oracle数据库特有的一些特性了。

2.2.2 javax.sql包

javax.sql包中的类和接口最早是由JDBC 2.0版本的可选包提供的,内容不多:

#数据源
javax.sql.DataSource
javax.sql.CommonDataSource

#连接池相关
javax.sql.ConnectionPoolDataSource
javax.sql.PooledConnection
javax.sql.ConnectionEvent
javax.sql.ConnectionEventListener
javax.sql.StatementEvent
javax.sql.StatementEventListener

#ResultSet扩展
javax.sql.RowSet
javax.sql.RowSetEvent
javax.sql.RowSetInternal
javax.sql.RowSetListener
javax.sql.RowSetMetaData
javax.sql.RowSetReader
javax.sql.RowSetWriter

#分布式扩展
javax.sql.XAConnection
javax.sql.XADataSource
2.2.2.1 DataSource

在JDBC 1.0中,使用DriverManager类来产生一个与数据源连接的Connection对象;JDBC 2.0提供的DataSource接口则是一种更好的连接数据源的方式。主要理由是:

  • 避免硬编码:使用DataSource不需要像使用DriverManager一样对加载的数据库驱动程序信息进行硬编码,而可以选择使用JNDI注册这个数据源对象,然后在程序中使用一个逻辑名称来引用它,JNDI会自动根据给出的名称找到与这个名称绑定的DataSource对象,然后就可以使用这个DataSource对象来建立和具体数据库的连接。
  • 提高程序效率:DataSource接口支持连接池和分布式事务。连接池支持对连接的复用,而不是每次需要操作数据源时都新建一个物理连接,以此来显著地提高程序的效率,适用于任务繁忙、负担繁重的企业级应用。
2.2.2.2 PooledConnection

An object that provides hooks for connection pool management. A PooledConnectionobject represents a physical connection to a data source. The connection can be recycled rather than being closed when an application is finished with it, thus reducing the number of connections that need to be made.
PooledConnection是为连接池管理提供钩子的对象。一个PooledConnection对象表示到数据源的物理连接。当应用程序完成连接时,可以回收连接,而不是关闭连接,从而减少需要建立的连接数量。

An application programmer does not use the PooledConnectioninterface directly; rather, it is used by a middle tier infrastructure that manages the pooling of connections.
开发者不会直接使用PooledConnection接口;相反,它由管理连接池的中间层基础设施使用。

When an application calls the method DataSource.getConnection, it gets back a Connectionobject. If connection pooling is being done, that Connectionobject is actually a handle to a PooledConnectionobject, which is a physical connection.
当应用程序调用DataSource的getConnection方法时,它会返回一个Connection对象。如果连接池已经准备就绪,这个Connection对象实际上是PooledConnection对象的句柄,这是一个物理连接。

The connection pool manager, typically the application server, maintains a pool of PooledConnectionobjects. If there is a PooledConnectionobject available in the pool, the connection pool manager returns a Connectionobject that is a handle to that physical connection.
连接池管理器,通常是应用服务器,维护着一个PooledConnection对象池。如果连接池中有一个可用的PooledConnection对象,连接池管理器返回一个Connection对象,它是该物理连接的句柄。

If no PooledConnectionobject is available, the connection pool manager calls the ConnectionPoolDataSourcemethod getPoolConnectionto create a new physical connection. The JDBC driver implementing ConnectionPoolDataSourcecreates a new PooledConnectionobject and returns a handle to it.
如果没有可用的PooledConnection对象,连接池管理器会调用ConnectionPoolDataSource的getPoolConnection方法来创建一个新的物理连接。实现ConnectionPoolDataSource的JDBC驱动程序创建了一个新的PooledConnection对象,并返回了它的句柄。

When an application closes a connection, it calls the Connectionmethod close. When connection pooling is being done, the connection pool manager is notified because it has registered itself as a ConnectionEventListenerobject using the ConnectionPoolmethod addConnectionEventListener. The connection pool manager deactivates the handle to the PooledConnectionobject and returns the PooledConnectionobject to the pool of connections so that it can be used again. Thus, when an application closes its connection, the underlying physical connection is recycled rather than being closed.
当应用程序关闭连接时,它会调用Connection的close方法。当连接池就绪时,连接池管理器会收到通知,因为它已经使用ConnectionPool的addConnectionEventListener方法将自己注册为ConnectionEventListener对象。连接池管理器停用PooledConnection对象的句柄,并将PooledConnection对象返回到连接池,以便它可以再次使用。因此,当应用程序关闭其连接时,底层的物理连接将被回收而不是关闭。

The physical connection is not closed until the connection pool manager calls the PooledConnectionmethod close. This method is generally called to have an orderly shutdown of the server or if a fatal error has made the connection unusable.
直到连接池管理器调用PooledConnection的close方法,物理连接才会关闭。这个方法通常用于有序地关闭服务器,或者在发生致命错误导致连接不可用时调用。

A connection pool manager is often also a statement pool manager, maintaining a pool of PreparedStatementobjects. When an application closes a prepared statement, it calls the PreparedStatementmethod close. When Statementpooling is being done, the pool manager is notified because it has registered itself as a StatementEventListenerobject using the ConnectionPoolmethod addStatementEventListener. Thus, when an application closes its PreparedStatement, the underlying prepared statement is recycled rather than being closed.
连接池管理器通常也是一个Statement池管理器,维护一个PreparedStatement对象池。当一个应用程序关闭一个PreparedStatement时,它会调用PreparedStatement的close方法。当Statement池就绪时,池管理器会收到通知,因为它已经使用ConnectionPool的方法addStatementEventListener将自己注册为StatementEventListener对象。因此,当应用程序关闭PreparedStatement时,底层的PreparedStatement将被回收而不是关闭。

PooledConnection的javadoc内容很多,详细地说明了PooledConnection的用法,总结起来为以下几点:

  1. PooledConnection对象代表连接池到数据源的物理连接;
  2. 应用服务器一般充当连接池管理器,维护一个PooledConnection对象池,调用getConnection方法时,如果连接池中有可用的PooledConnection对象,则返回PooledConnection对象的句柄;如果没有,则调用ConnectionPoolDataSource的getPoolConnection方法来创建一个PooledConnection对象,并返回它的句柄。
  3. 由于连接池管理器已经被注册为ConnectionEventListener对象,因此应用程序关闭连接时会收到通知,进而停用PooledConnection对象的句柄,并回收PooledConnection对象(减少与数据源建立连接的次数,提升性能)。
  4. 连接池管理器处理维护PooledConnection对,还可以维护Statement对象。同样,当应用程序关闭PreparedStatement时,底层的PreparedStatement对象将被回收而不是关闭。
2.2.2.3 XAConnection

javax.sql包中还包含XADataSource、XAResource和XAConnection接口,这些接口提供了分布式事务的支持,具体由JDBC驱动来实现。

XAConnection接口继承了PooledConnection接口,因此它具有所有PooledConnection的特性。

public interface XAConnection extends PooledConnection
2.2.2.4 RowSet
public interface RowSet extends ResultSet

javax.sql包下的RowSet接口,继承自java.sql包下的ResultSet接口,其关系如图。

RowSet与ResultSet的关系

A RowSet object may make a connection with a data source and maintain that connection throughout its life cycle, in which case it is called a connected rowset. A rowset may also make a connection with a data source, get data from it, and then close the connection. Such a rowset is called a disconnected rowset. A disconnected rowset may make changes to its data while it is disconnected and then send the changes back to the original source of the data, but it must reestablish a connection to do so.

RowSet对象可以与数据源建立连接,并在其生命周期中维护该连接,在这种情况下该对象被称为连接RowSet
RowSet对象还可以与数据源建立连接,从数据源获取数据,然后关闭连接,这样的RowSet对象称为非连接RowSet
非连接RowSet可以在连接断开时更改其数据,然后将这些更改发送回原始数据源,不过它必须重新建立连接才能完成此操作。

换句话说,RowSet的数据操作都是在应用程序的内存中进行的,在建立连接后批量提交到数据源。RowSet的这种离线操作能够有效地利用计算机越来越充足的内存、减轻数据库服务器的负担,提升灵活性和性能。

RowSet默认是一个可滚动、可更新、可序列化的结果集,而且它作为一个JavaBean组件,可以方便地在网络间传输,用于两端的数据同步。

通俗来讲,RowSet就相当于数据库表数据在应用程序内存中的映射,所有的操作都可以直接与RowSet对象交互。RowSet与数据库之间的数据同步,作为开发人员不需要关注。

本节完,更多内容请查阅分类专栏:MyBatis3源码深度解析

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

灰色孤星A

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值