Java JDBC 完整教程
目录
- 什么是 JDBC?
- JDBC 核心组件
- 环境准备
- JDBC 编程步骤(核心流程)
- 完整代码示例
- 处理
ResultSet - 使用
PreparedStatement防止 SQL 注入 - 事务管理
- 数据库连接池
- JDBC 4.0+ 自动加载驱动
- 最佳实践与总结
- 进阶学习
什么是 JDBC?
JDBC (Java Database Connectivity) 是 Java 语言中用来规范客户端程序如何访问数据库的应用程序接口,它提供了查询和更新数据库中数据的方法。

JDBC Java 官方提供的一套规范(一套接口和类),用于让 Java 程序能够连接并操作各种不同的数据库(如 MySQL, Oracle, SQL Server, PostgreSQL 等)。
核心思想: “一次编写,到处运行”,Java 程序员只需要学习 JDBC 这一套统一的 API,就可以通过不同的 数据库驱动(Driver) 来操作不同的数据库,数据库驱动是由各个数据库厂商提供的,实现了 JDBC 接口的类库。
JDBC 核心组件
在开始编码前,需要了解几个核心的 JDBC 组件:
| 组件 | 接口/类 | 描述 |
|---|---|---|
| Driver | java.sql.Driver |
数据库驱动接口,每个数据库厂商(如 MySQL)都会提供一个实现此接口的驱动类。 |
| DriverManager | java.sql.DriverManager |
管理数据库驱动程序,用于建立和管理数据库连接。 |
| Connection | java.sql.Connection |
代表与数据库的连接会话,通过它来创建 Statement 对象。 |
| Statement | java.sql.Statement |
用于执行静态 SQL 语句并返回它所生成结果的对象。 |
| PreparedStatement | java.sql.PreparedStatement |
Statement 的子接口,用于执行预编译的 SQL 语句,能有效防止 SQL 注入,是推荐使用的 Statement。 |
| CallableStatement | java.sql.CallableStatement |
用来调用数据库存储过程。 |
| ResultSet | java.sql.ResultSet |
代表 SQL 查询的结果集,它是一个指向数据行的光标。 |
| SQLException | java.sql.SQLException |
处理与数据库访问相关的错误或警告。 |
环境准备
在开始之前,你需要:

-
JDK 安装:确保你的系统已安装 Java 开发工具包。
-
数据库:安装一个数据库,MySQL。
-
JDBC 驱动:下载对应数据库的 JDBC 驱动 JAR 包。
- MySQL 8.x 驱动:mysql-connector-j-8.x.x.jar
- MySQL 5.x 驱动:mysql-connector-java-5.x.x.jar
如何添加驱动?
(图片来源网络,侵删)- IDE (如 IntelliJ IDEA / Eclipse): 将下载的 JAR 文件添加到项目的库(Library)或依赖(Dependency)中。
- Maven 项目: 在
pom.xml文件中添加依赖。<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.28</version> <!-- 使用你下载的版本号 --> </dependency>
JDBC 编程步骤(核心流程)
使用 JDBC 操作数据库的标准流程如下:
- 加载并注册驱动:告诉 JVM 使用哪个数据库的驱动。
- 获取数据库连接:使用
DriverManager获取一个Connection对象。 - 创建 Statement 对象:通过
Connection对象创建Statement或PreparedStatement对象,用于执行 SQL。 - 执行 SQL 语句:
- 对于查询(
SELECT),使用executeQuery()方法,返回一个ResultSet。 - 对于更新(
INSERT,UPDATE,DELETE),使用executeUpdate()方法,返回受影响的行数。
- 对于查询(
- 处理结果集:如果是查询操作,遍历
ResultSet获取数据。 - 关闭资源:非常重要! 按照创建的相反顺序关闭
ResultSet,Statement,Connection,以释放数据库资源。
完整代码示例
假设我们有一个 users 表,结构如下:
CREATE DATABASE mydb;
USE mydb;
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL UNIQUE,
age INT
);
INSERT INTO users (name, email, age) VALUES
('Alice', 'alice@example.com', 28),
('Bob', 'bob@example.com', 32),
('Charlie', 'charlie@example.com', 24);
示例 1:查询数据
import java.sql.*;
public class JdbcSelectExample {
// 数据库连接信息
private static final String URL = "jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=UTC";
private static final String USER = "root"; // 你的数据库用户名
private static final String PASSWORD = "password"; // 你的数据库密码
public static void main(String[] args) {
// 1. 定义资源变量
Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
try {
// 2. 加载并注册驱动 (JDBC 4.0后可省略,但显式写出更清晰)
Class.forName("com.mysql.cj.jdbc.Driver");
// 3. 获取数据库连接
conn = DriverManager.getConnection(URL, USER, PASSWORD);
System.out.println("数据库连接成功!");
// 4. 创建 Statement 对象
stmt = conn.createStatement();
// 5. 执行 SQL 查询
String sql = "SELECT id, name, email, age FROM users";
rs = stmt.executeQuery(sql);
// 6. 处理结果集
System.out.println("ID\tName\tEmail\t\tAge");
System.out.println("----------------------------------------");
while (rs.next()) {
// 通过列名或列索引获取数据
int id = rs.getInt("id");
String name = rs.getString("name");
String email = rs.getString("email");
int age = rs.getInt("age");
System.out.printf("%d\t%s\t%s\t%d\n", id, name, email, age);
}
} catch (ClassNotFoundException e) {
System.err.println("找不到 MySQL 驱动类!");
e.printStackTrace();
} catch (SQLException e) {
System.err.println("数据库操作出错!");
e.printStackTrace();
} finally {
// 7. 关闭资源 (非常重要!)
try {
if (rs != null) rs.close();
if (stmt != null) stmt.close();
if (conn != null) conn.close();
System.out.println("资源已关闭。");
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
示例 2:插入数据
import java.sql.*;
public class JdbcInsertExample {
private static final String URL = "jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=UTC";
private static final String USER = "root";
private static final String PASSWORD = "password";
public static void main(String[] args) {
String sql = "INSERT INTO users (name, email, age) VALUES (?, ?, ?)"; // 使用问号作为占位符
Connection conn = null;
PreparedStatement pstmt = null;
try {
Class.forName("com.mysql.cj.jdbc.Driver");
conn = DriverManager.getConnection(URL, USER, PASSWORD);
// 创建 PreparedStatement 对象
pstmt = conn.prepareStatement(sql);
// 设置参数 (索引从 1 开始)
pstmt.setString(1, "David");
pstmt.setString(2, "david@example.com");
pstmt.setInt(3, 35);
// 执行更新
int affectedRows = pstmt.executeUpdate();
System.out.println("成功插入 " + affectedRows + " 行数据。");
} catch (ClassNotFoundException | SQLException e) {
e.printStackTrace();
} finally {
try {
if (pstmt != null) pstmt.close();
if (conn != null) conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
处理 ResultSet
ResultSet 是一个指向数据行的光标,默认情况下它位于第一行之前。
| 方法 | 描述 |
|---|---|
next() |
将光标移动到下一行,如果存在下一行,则返回 true,否则返回 false,通常用于 while 循环。 |
previous() |
将光标移动到上一行。 |
absolute(int row) |
将光标移动到指定的行号。 |
beforeFirst() / afterLast() |
将光标移动到第一行之前 / 最后一行之后。 |
isFirst() / isLast() |
判断光标是否在第一行 / 最后一行。 |
getXXX(int columnIndex) |
通过列的索引(从 1 开始)获取值。XXX 是数据类型,如 getInt(), getString(), getDate()。 |
getXXX(String columnName) |
通过列的名称获取值。推荐使用此方法,因为代码更具可读性,且在 SQL 查询顺序改变时不易出错。 |
使用 PreparedStatement 防止 SQL 注入
Statement 直接拼接 SQL 字符串,非常容易受到 SQL 注入 攻击。
什么是 SQL 注入? 用户输入恶意 SQL 代码,改变了原有的 SQL 逻辑,从而执行非预期的操作。
PreparedStatement 的优势:
- 防止 SQL 注入:它会对输入参数进行转义处理,确保用户输入的数据不会被当作 SQL 代码执行。
- 性能更高:对于需要多次执行的 SQL 语句(只是参数不同),数据库可以预编译 SQL 语句,提高执行效率。
对比示例:
// 危险的 Statement 用法 String userInput = "admin' OR '1'='1"; String sql = "SELECT * FROM users WHERE name = '" + userInput + "'"; // SQL 拼接 // 最终执行的 SQL 可能是: SELECT * FROM users WHERE name = 'admin' OR '1'='1' // 这会返回所有用户,导致严重安全问题! // 安全的 PreparedStatement 用法 String sql = "SELECT * FROM users WHERE name = ?"; PreparedStatement pstmt = conn.prepareStatement(sql); pstmt.setString(1, userInput); // 参数会被安全地转义 // 无论 userInput 是什么,它都只会被当作一个字符串值来处理。
只要有参数,就 必须使用 PreparedStatement。
事务管理
事务是一组操作的集合,它们要么全部成功,要么全部失败,事务具有 ACID 特性(原子性、一致性、隔离性、持久性)。
在 JDBC 中,默认情况下,每个 SQL 语句都是一个独立的事务。
如何手动管理事务?
- 开启事务:
conn.setAutoCommit(false); - 提交事务:
conn.commit();(如果所有操作都成功) - 回滚事务:
conn.rollback();(如果某个操作失败)
示例代码:
Connection conn = null;
try {
conn = DriverManager.getConnection(URL, USER, PASSWORD);
// 开启事务
conn.setAutoCommit(false);
// 操作1: 转账
pstmt1 = conn.prepareStatement("UPDATE accounts SET balance = balance - 100 WHERE user_id = 1");
pstmt1.executeUpdate();
// 模拟一个错误
// int i = 1 / 0; // 如果取消这行注释,事务将回滚
// 操作2: 收款
pstmt2 = conn.prepareStatement("UPDATE accounts SET balance = balance + 100 WHERE user_id = 2");
pstmt2.executeUpdate();
// 提交事务
conn.commit();
System.out.println("事务提交成功!");
} catch (Exception e) {
// 发生异常,回滚事务
try {
if (conn != null) {
conn.rollback();
System.out.println("事务已回滚!");
}
} catch (SQLException ex) {
ex.printStackTrace();
}
e.printStackTrace();
} finally {
// ... 关闭资源
// 记得恢复自动提交模式
if (conn != null) {
try {
conn.setAutoCommit(true);
} catch (SQLException e) {
e.printStackTrace();
}
}
}
数据库连接池
直接使用 DriverManager 获取连接的方式效率低下,因为创建和销毁连接是一个耗时耗资源的操作。
连接池 是一个管理数据库连接的缓冲池,它预先创建一定数量的连接,当应用程序需要连接时,从池中获取一个,用完后归还给池,而不是销毁,这极大地提高了性能。
常用的连接池:
- HikariCP: 目前性能最好的连接池,是 Spring Boot 2.x 的默认连接池。
- Druid: 阿里巴巴开源的连接池,功能强大,带有监控界面。
- C3P0: 一个老牌的连接池实现。
使用 HikariCP 的示例:
-
添加 Maven 依赖:
<dependency> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> <version>5.0.1</version> </dependency> -
代码示例:
import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; public class JdbcPoolExample { private static HikariDataSource dataSource; static { HikariConfig config = new HikariConfig(); config.setJdbcUrl("jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=UTC"); config.setUsername("root"); config.setPassword("password"); config.setDriverClassName("com.mysql.cj.jdbc.Driver"); // 连接池配置 config.setMaximumPoolSize(10); // 最大连接数 config.setMinimumIdle(5); // 最小空闲连接数 config.setConnectionTimeout(30000); // 连接超时时间 (毫秒) dataSource = new HikariDataSource(config); } public static void main(String[] args) { Connection conn = null; PreparedStatement pstmt = null; ResultSet rs = null; try { // 从连接池获取连接 conn = dataSource.getConnection(); System.out.println("从连接池获取连接成功!"); String sql = "SELECT name FROM users WHERE id = ?"; pstmt = conn.prepareStatement(sql); pstmt.setInt(1, 1); rs = pstmt.executeQuery(); if (rs.next()) { System.out.println("用户名: " + rs.getString("name")); } } catch (SQLException e) { e.printStackTrace(); } finally { // 关闭连接,实际上是将连接归还给连接池 try { if (rs != null) rs.close(); if (pstmt != null) pstmt.close(); if (conn != null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }
JDBC 4.0+ 自动加载驱动
从 JDBC 4.0 (Java 6) 开始,不再需要手动调用 Class.forName("com.mysql.cj.jdbc.Driver")。
只要你的 JDBC 驱动 JAR 文件在项目的类路径(Classpath)中,JVM 启动时会自动加载驱动,在现代 Java 项目中,这行代码通常是 可以省略 的。
显式写出这行代码没有坏处,它能让代码的意图更清晰,并且兼容非常老的 Java 环境。
最佳实践与总结
-
永远使用
PreparedStatement:除非你 100% 确定你的 SQL 语句是固定的、没有用户输入拼接的,否则请始终使用PreparedStatement来防止 SQL 注入。 -
总是关闭资源:使用
try-with-resources语句可以简化资源关闭的代码,避免忘记关闭导致的资源泄漏。// try-with-resources 示例 (JDBC 4.0+) try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD); PreparedStatement pstmt = conn.prepareStatement("SELECT * FROM users"); ResultSet rs = pstmt.executeQuery()) { // 在这里处理结果... // conn, pstmt, rs 会在 try 块结束时自动关闭 } catch (SQLException e) { e.printStackTrace(); } -
使用连接池:在生产环境中,绝对不要 直接使用
DriverManager获取连接,务必使用 HikariCP 等高性能连接池。 -
善用事务:对于多个关联的数据库操作,一定要使用事务来保证数据的一致性。
-
配置外部化:不要将数据库 URL、用户名、密码等硬编码在代码中,应该放在配置文件(如
application.properties)中。 -
考虑使用 ORM 框架:对于复杂的业务逻辑,直接写 SQL 会变得非常繁琐,可以考虑使用 MyBatis 或 JPA (Hibernate) 等 ORM 框架,它们能将对象和数据库表进行映射,让你用面向对象的方式操作数据库。
进阶学习
当你掌握了 JDBC 的基础后,可以继续学习以下内容:
- 批处理 (
addBatch(),executeBatch()):用于一次性执行多条相似的 SQL 语句,提高批量操作的效率。 - 可滚动和可更新的
ResultSet:通过创建Statement或Connection时设置参数,可以创建一个可以前后滚动、甚至可以直接修改数据的ResultSet。 - 数据库元数据 (
DatabaseMetaData):获取数据库本身的信息,如数据库版本、支持的 SQL 语法、表名、列名等。 - 参数化元数据 (
ParameterMetaData):获取PreparedStatement中参数的元数据。 - RowSet:
ResultSet的一个扩展,它是一个 JavaBean,可以更方便地在不同层之间传递数据,并且支持离线操作。
希望这份详细的教程能帮助你全面掌握 Java JDBC!
