1. 概述

本文将带你了解 JDBC(Java Database Connectivity),这是用于连接数据库并执行查询的 API。JDBC 的优势在于其通用性——只要提供合适的驱动程序,它就能与任何数据库协同工作。

2. JDBC 驱动

JDBC 驱动是 JDBC API 的具体实现,用于连接特定类型的数据库。驱动主要分为四类:

  • Type 1:映射到其他数据访问 API(如 JDBC-ODBC 桥接驱动)
  • Type 2:使用目标数据库的客户端库(原生 API 驱动)
  • Type 3:通过中间件转换 JDBC 调用(网络协议驱动)
  • Type 4:直接将 JDBC 调用转换为数据库协议(瘦驱动)

⚠️ 最常用的是 Type 4 驱动,优势在于:

  • 跨平台性:不依赖本地库
  • 性能更优:直接连接数据库服务器
  • 数据库特定:每种数据库需要专用驱动

3. 连接数据库

连接数据库只需两步:初始化驱动和打开连接。

3.1 注册驱动

以 Type 4 驱动为例(MySQL 数据库)。首先添加依赖:

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.32</version>
</dependency>

通过 Class.forName() 动态加载驱动类:

Class.forName("com.mysql.cj.jdbc.Driver");

踩坑提示:在 JDBC 4.0+ 版本中,classpath 中的驱动会自动加载,无需手动注册!现代开发环境可省略此步骤。

3.2 创建连接

使用 DriverManager.getConnection() 打开连接:

try (Connection con = DriverManager
  .getConnection("jdbc:mysql://localhost:3306/myDb", "user1", "pass")) {
    // 使用连接执行操作
}

资源管理Connection 实现了 AutoCloseable,务必放在 try-with-resources 块中!

不同数据库的 URL 格式示例:

  • MySQL: jdbc:mysql://localhost:3306/myDb?user=user1&password=pass
  • PostgreSQL: jdbc:postgresql://localhost/myDb
  • HSQLDB: jdbc:hsqldb:mem:myDb

创建数据库和用户的 SQL:

CREATE DATABASE myDb;
CREATE USER 'user1' IDENTIFIED BY 'pass';
GRANT ALL on myDb.* TO 'user1';

4. 执行 SQL 语句

通过 Connection 创建 StatementPreparedStatementCallableStatement 执行 SQL。

4.1 Statement

Statement 提供基础 SQL 执行功能。创建方式:

try (Statement stmt = con.createStatement()) {
    // 执行 SQL 操作
}

执行方法:

  • executeQuery():用于 SELECT 查询
  • executeUpdate():用于更新数据或结构
  • execute():通用方法(返回结果未知时)

创建表示例:

String tableSql = "CREATE TABLE IF NOT EXISTS employees" 
  + "(emp_id int PRIMARY KEY AUTO_INCREMENT, name varchar(30),"
  + "position varchar(30), salary double)";
stmt.execute(tableSql);

结果判断

  • stmt.getUpdateCount() 返回受影响行数(0 表示结构更新,-1 表示查询)
  • 查询结果通过 stmt.getResultSet() 获取

插入数据示例:

String insertSql = "INSERT INTO employees(name, position, salary)"
  + " VALUES('john', 'developer', 2000)";
stmt.executeUpdate(insertSql);

查询数据示例:

String selectSql = "SELECT * FROM employees"; 
try (ResultSet resultSet = stmt.executeQuery(selectSql)) {
    // 处理结果集
}

资源管理ResultSet 必须关闭!推荐使用 try-with-resources。

4.2 PreparedStatement

预编译 SQL 语句,用 ? 表示参数。创建方式:

String updatePositionSql = "UPDATE employees SET position=? WHERE emp_id=?";
try (PreparedStatement pstmt = con.prepareStatement(updatePositionSql)) {
    // 设置参数并执行
}

参数设置示例:

pstmt.setString(1, "lead developer"); // 第一个参数
pstmt.setInt(2, 1); // 第二个参数
int rowsAffected = pstmt.executeUpdate();

4.3 CallableStatement

用于调用存储过程。创建方式:

String preparedSql = "{call insertEmployee(?,?,?,?)}";
try (CallableStatement cstmt = con.prepareCall(preparedSql)) {
    // 设置参数并执行
}

参数处理:

  • 输入参数:setX() 方法(同 PreparedStatement
  • 输出参数:需先注册类型
// 注册输出参数
cstmt.registerOutParameter(1, Types.INTEGER);

// 设置输入参数
cstmt.setString(2, "ana");
cstmt.setString(3, "tester");
cstmt.setDouble(4, 2000);

// 执行并获取结果
cstmt.execute();
int new_id = cstmt.getInt(1);

存储过程示例(MySQL):

delimiter //
CREATE PROCEDURE insertEmployee(OUT emp_id int, 
  IN emp_name varchar(30), IN position varchar(30), IN salary double) 
BEGIN
INSERT INTO employees(name, position,salary) VALUES (emp_name,position,salary);
SET emp_id = LAST_INSERT_ID();
END //
delimiter ;

权限问题:执行存储过程需授权:

GRANT ALL ON mysql.proc TO 'user1';

或连接时添加参数:

con = DriverManager.getConnection(
  "jdbc:mysql://localhost:3306/myDb?noAccessToProcedureBodies=true", 
  "user1", "pass");

5. 解析查询结果

ResultSet 表示查询结果,具有类似表格的行列结构。

5.1 ResultSet 接口

先定义数据模型:

public class Employee {
    private int id;
    private String name;
    private String position;
    private double salary;
 
    // 标准构造器、getter/setter
}

遍历结果集示例:

String selectSql = "SELECT * FROM employees"; 
try (ResultSet resultSet = stmt.executeQuery(selectSql)) {
    List<Employee> employees = new ArrayList<>(); 
    while (resultSet.next()) { 
        Employee emp = new Employee(); 
        emp.setId(resultSet.getInt("emp_id")); 
        emp.setName(resultSet.getString("name")); 
        emp.setPosition(resultSet.getString("position")); 
        emp.setSalary(resultSet.getDouble("salary")); 
        employees.add(emp); 
    }
}

取值技巧

  • 使用列名("emp_id")而非列序号,避免查询结构调整导致错误
  • 通过 getX() 方法获取数据(X 为数据类型)

5.2 可更新的 ResultSet

默认 ResultSet 只能前向遍历且不可修改。创建可更新版本:

stmt = con.createStatement(
  ResultSet.TYPE_SCROLL_INSENSITIVE, 
  ResultSet.CONCUR_UPDATABLE
);

导航方法:

  • first()/last():移动到首行/末行
  • next()/previous():前进/后退
  • absolute(int row):跳转到指定行
  • relative(int nrRows):相对移动

更新方法:

  • updateX():更新当前行数据(仅内存)
  • updateRow():将修改持久化到数据库
  • insertRow():插入新行
  • deleteRow():删除当前行

插入数据示例:

try (Statement updatableStmt = con.createStatement(
  ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE)) {
    try (ResultSet updatableResultSet = updatableStmt.executeQuery(selectSql)) {
        updatableResultSet.moveToInsertRow();
        updatableResultSet.updateString("name", "mark");
        updatableResultSet.updateString("position", "analyst");
        updatableResultSet.updateDouble("salary", 2000);
        updatableResultSet.insertRow();
    }
}

6. 解析元数据

JDBC 提供两种元数据接口:数据库元数据和结果集元数据。

6.1 DatabaseMetadata

获取数据库全局信息(表、存储过程等):

DatabaseMetaData dbmd = con.getMetaData();
ResultSet tablesResultSet = dbmd.getTables(null, null, "%", null);
while (tablesResultSet.next()) {
    LOG.info(tablesResultSet.getString("TABLE_NAME"));
}

6.2 ResultSetMetadata

获取结果集结构信息(列名、数量等):

ResultSetMetaData rsmd = rs.getMetaData();
int nrColumns = rsmd.getColumnCount();

IntStream.range(1, nrColumns).forEach(i -> {
    try {
        LOG.info(rsmd.getColumnName(i));
    } catch (SQLException e) {
        e.printStackTrace();
    }
});

7. 处理事务

默认情况下,每条 SQL 语句自动提交。可通过编程控制事务:

String updatePositionSql = "UPDATE employees SET position=? WHERE emp_id=?";
PreparedStatement pstmt = con.prepareStatement(updatePositionSql);
pstmt.setString(1, "lead developer");
pstmt.setInt(2, 1);

String updateSalarySql = "UPDATE employees SET salary=? WHERE emp_id=?";
PreparedStatement pstmt2 = con.prepareStatement(updateSalarySql);
pstmt.setDouble(1, 3000);
pstmt.setInt(2, 1);

boolean autoCommit = con.getAutoCommit();
try {
    con.setAutoCommit(false); // 关闭自动提交
    pstmt.executeUpdate();
    pstmt2.executeUpdate();
    con.commit(); // 手动提交
} catch (SQLException exc) {
    con.rollback(); // 异常回滚
} finally {
    con.setAutoCommit(autoCommit); // 恢复原设置
}

事务场景:当多个操作需保持原子性时(如转账操作),必须使用事务!

8. 关闭资源

使用完毕后必须关闭连接释放资源:

con.close();

最佳实践:所有 JDBC 资源(Connection/Statement/ResultSet)都应放在 try-with-resources 块中,避免资源泄漏!

9. 总结

本文系统介绍了 JDBC 的核心功能:

  • 驱动类型与选择
  • 数据库连接与配置
  • SQL 语句的三种执行方式
  • 结果集处理与元数据查询
  • 事务控制机制
  • 资源管理最佳实践

简单粗暴总结:JDBC 是 Java 数据库操作的基石,掌握它才能玩转 MyBatis、JPA 等高级框架。记住三点:用 try-with-resources 管理资源、优先使用 PreparedStatement 防注入、复杂操作用事务保一致性!

完整示例代码见 GitHub 仓库


原始标题:Introduction to JDBC