最近为了阅读 MyBatis 源码,大概看了下 JDBC 的代码,了解在不使用 ORM 框架的情况下 Java 实现 SQL 查询的原理。
JDBC 定义如下:
JDBC(Java DataBase Connectivity,java数据库连接)是一种用于执行 SQL 语句的 Java API,可以为多种关系数据库提供统一访问,它由一组用 Java 语言编写的类和接口组成。JDBC 提供了一种基准,据此可以构建更高级的工具和接口,使数据库开发人员能够编写数据库应用程序。
也就是说 JDBC 是 SUN 公司提出来的一系列规范,但它只定义了接口规范,具体的实现则交给各个数据库的厂商去做。
1. 使用流程
JDBC 流程:
- 通过 SPI 加载 Driver 驱动类。
- 建立数据库连接,获取 Connection 连接对象。
- 通过连接创建 Statement 对象,执行 SQL 语句,获取返回结果。
- 释放资源。
在 Maven 中添加对 mysql 驱动的依赖:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.23</version>
</dependency>
数据库表:
1.1 普通查询
@Test
public void query() throws Exception {
// 打印至控制台
DriverManager.setLogWriter(new PrintWriter(System.out));
// 建立连接
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/testdb", "test", "test");
// 执行 SQL 查询,获取返回结果
Statement statement = conn.createStatement();
statement.setQueryTimeout(60);
ResultSet resultSet = statement.executeQuery("select * from t_student");
while (resultSet.next()) {
System.out.println("id:" + resultSet.getInt(1) + " address:" + resultSet.getString(2) + " name:" + resultSet.getString(4));
}
}
执行结果:
DriverManager.getConnection("jdbc:mysql://localhost:3306/testdb")
trying com.mysql.cj.jdbc.Driver
getConnection returning com.mysql.cj.jdbc.Driver
id:1 address:hunan name:zhangsan
id:2 address:guangzhou name:lisi
id:3 address:四川 name:大大
1.2 参数查询
@Test
public void queryByParam() throws SQLException {
DriverManager.setLogWriter(new PrintWriter(System.out));
// 建立连接
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/testdb", "test", "test");
// 执行 SQL 查询,获取返回结果
PreparedStatement preparedStatement = conn.prepareStatement("select * from t_student where id = ? and age = ?");
preparedStatement.setString(1, "1");
preparedStatement.setInt(2, 11);
ResultSet resultSet = preparedStatement.executeQuery();
while (resultSet.next()) {
System.out.println("id:" + resultSet.getInt(1) + " address:" + resultSet.getString(2) + " name:" + resultSet.getString(4));
}
}
1.3 批量更新
@Test
public void updateBatch() throws SQLException {
DriverManager.setLogWriter(new PrintWriter(System.out));
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/testdb", "test", "test");
PreparedStatement preparedStatement = conn.prepareStatement("update t_student set age = ? where id = ?");
preparedStatement.setInt(1, 10);
preparedStatement.setString(2, "1");
preparedStatement.addBatch();
preparedStatement.setInt(1, 10);
preparedStatement.setString(2, "2");
preparedStatement.addBatch();
int[] result = preparedStatement.executeBatch();
System.out.println("result = " + result.length);
}
2. 源码解析
2.1 加载驱动
主要流程:
- 通过 SPI 加载 MySQL 驱动包中的 Driver 类。
- 将 Driver 类注册到 JDBC 的 DriverManager 之中。
2.1.1 SPI 加载驱动类
DriverManager 中定义了静态代码块,会通过 SPI 来加载数据库驱动类。
java.sql.DriverManager
/**
* Load the initial JDBC drivers by checking the System property
* jdbc.properties and then use the {@code ServiceLoader} mechanism
*/
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
java.sql.DriverManager#loadInitialDrivers
private static void loadInitialDrivers() {
// ...
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
// 采用 SPI 机制加载数据库驱动
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
/* Load these drivers, so that they can be instantiated.
* It may be the case that the driver class may not be there
* i.e. there may be a packaged driver with the service class
* as implementation of java.sql.Driver but the actual class
* may be missing. In that case a java.util.ServiceConfigurationError
* will be thrown at runtime by the VM trying to locate
* and load the service.
*
* Adding a try catch block to catch those runtime errors
* if driver not available in classpath but it's
* packaged as service and that service is there in classpath.
*/
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
// ...
}
通过 SPI,加载配置文件 META-INF/services/java.sql.Driver,由于项目中具有 MySql 驱动的 jar 包,这里是读取到配置文件:
mysql-connector-java-8.0.23.jar!\META-INF\services\java.sql.Driver
文件中的内容为 com.mysql.cj.jdbc.Driver
,反射获取得到该类,并进行实例化。
2.1.2 注册驱动类
实例化 MySQL 驱动的时候,会通过 DriverManager#registerDriver 方法来注册驱动。
com.mysql.cj.jdbc.Driver
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
//
// Register ourselves with the DriverManager
//
static {
try {
java.sql.DriverManager.registerDriver(new Driver()); // 注册驱动
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
/**
* Construct a new driver and register it with DriverManager
*
* @throws SQLException
* if a database error occurs.
*/
public Driver() throws SQLException {
// Required for Class.forName().newInstance()
}
}
其底层实现是,将 MySQL 驱动 Driver 对象包装为 DriverInfo,存储在 DriverManager#registeredDrivers 集合中。
java.sql.DriverManager#registerDriver
// List of registered JDBC drivers
private final