JDBC核心教程
一、JDBC核心概念
JDBC(Java Database Connectivity)是Java平台提供的一套标准API,用于Java程序连接和操作关系型数据库(如MySQL、Oracle),它定义了统一的接口规范,屏蔽了不同数据库的底层实现差异,让开发者无需关心具体数据库的细节,只需面向接口编程,就能实现"一次编码,多库兼容"的效果(理论上更换数据库只需替换驱动和连接信息,无需修改核心业务代码)。
JDBC的本质是"接口+实现"的设计模式:Java官方定义接口(位于java.sql和javax.sql包下),各数据库厂商(如MySQL、Oracle)提供接口的实现类,即JDBC驱动,Java程序通过调用接口方法,间接调用驱动中的实现类,完成与数据库的交互。
1.1 JDBC核心作用
-
建立Java程序与数据库之间的连接;
-
向数据库发送SQL语句(增删改查、事务控制等);
-
接收数据库返回的执行结果(查询结果集、影响行数等);
-
处理数据库交互过程中的异常,释放资源。
1.2 JDBC与ORM框架的关系
很多开发者会混淆JDBC与MyBatis、Hibernate等ORM框架,核心关系如下:
-
JDBC:底层基础API,是操作数据库的"手动工具",需要手动编写SQL、处理结果集和资源,代码繁琐但灵活性极高;
-
ORM框架:基于JDBC封装的高级工具,通过注解或XML映射数据,简化SQL编写和结果集映射,提升开发效率,但底层仍依赖JDBC。
类比:JDBC是"手动挡汽车",需手动控制细节;ORM框架是"自动挡汽车",自动处理底层逻辑,更易操作但灵活度稍低。
1.3 JDBC核心组件(5个核心接口/类)
JDBC的所有操作,都围绕以下5个核心组件展开,协同完成数据库交互:
-
DriverManager(驱动管理器):管理JDBC驱动,负责加载驱动、创建并获取数据库连接(Connection),是连接数据库的入口。
-
Connection(数据库连接):代表Java程序与数据库之间的物理连接,是所有数据库操作的基础,还负责事务控制。
-
Statement(SQL执行器):用于执行静态SQL语句,但存在SQL注入风险,生产环境不推荐使用。
-
PreparedStatement(预编译SQL执行器):继承自Statement,支持预编译SQL和参数占位符,可防止SQL注入,性能更优,是生产环境首选。
-
ResultSet(结果集):存储查询语句(SELECT)返回的结果,提供多种方法遍历和提取数据。
二、JDBC开发环境搭建
使用JDBC操作数据库,需准备3个核心组件:JDK(推荐JDK8,兼容性最好)、MySQL数据库(8.0版本)、MySQL JDBC驱动(MySQL Connector/J),搭建步骤如下。
2.1 下载并导入JDBC驱动
驱动是Java程序与MySQL交互的"桥梁",有两种导入方式,推荐使用Maven依赖(自动管理版本,无需手动下载)。
方式1:Maven依赖
在Maven项目的pom.xml文件中,添加以下依赖(版本需与MySQL版本适配:MySQL 8.0用8.0.xx版本,MySQL 5.7用5.1.xx版本):
xml
<!-- MySQL JDBC驱动依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version> <!-- 版本适配MySQL 8.0 -->
</dependency>
提示:国内开发者可配置阿里云Maven镜像,加速依赖下载,在Maven安装目录的conf/settings.xml中添加镜像配置即可。
方式2:手动下载导入
-
从MySQL官网下载对应版本的驱动JAR包:dev.mysql.com/downloads/c...
-
在IDEA中创建lib目录,将下载的JAR包放入;
-
右键JAR包,选择"Add as Library",完成导入。
2.2 准备测试环境
提前创建测试数据库和表,后续所有示例均基于此结构:
sql
-- 1. 创建测试数据库
CREATE DATABASE IF NOT EXISTS jdbc_demo
CHARACTER SET utf8mb4
COLLATE utf8mb4_general_ci;
-- 2. 切换数据库
USE jdbc_demo;
-- 3. 创建学生表
CREATE TABLE IF NOT EXISTS student (
id INT PRIMARY KEY AUTO_INCREMENT, -- 自增主键
name VARCHAR(50) NOT NULL, -- 姓名(非空)
age INT CHECK (age > 0 AND age < 100), -- 年龄范围约束
gender ENUM('男', '女', '其他') DEFAULT '男', -- 性别默认值
create_time DATETIME DEFAULT CURRENT_TIMESTAMP -- 创建时间默认当前时间
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 4. 插入测试数据
INSERT INTO student (name, age, gender)
VALUES ('张三', 20, '男'), ('李四', 19, '女'), ('王五', 21, '男');
2.3 定义连接常量
将数据库连接信息(URL、用户名、密码)定义为常量,方便后续维护和修改,避免硬编码导致的维护成本:
java
public class JDBCConstants {
// 数据库连接URL(MySQL 8.0+ 需指定时区,避免时区异常)
public static final String URL = "jdbc:mysql://localhost:3306/jdbc_demo?serverTimezone=Asia/Shanghai&useSSL=false&characterEncoding=utf8";
// 数据库用户名(替换为自己的MySQL用户名)
public static final String USERNAME = "root";
// 数据库密码(替换为自己的MySQL密码)
public static final String PASSWORD = "123456";
}
URL参数说明:serverTimezone=Asia/Shanghai指定时区为上海,避免时区偏差;useSSL=false关闭SSL连接(开发环境可关闭);characterEncoding=utf8确保中文编码正常,杜绝乱码。
三、JDBC核心操作
JDBC操作数据库的核心流程固定:加载驱动 → 获取连接 → 创建SQL执行器 → 执行SQL → 处理结果 → 关闭资源,以下是增删改查(CRUD)的完整实战示例,均使用PreparedStatement(防SQL注入,生产标准)。
3.1 新增数据(INSERT)
向student表插入一条新数据,使用参数占位符(?)传递参数,避免SQL注入,同时确保资源正常关闭。
java
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class JDBCInsertDemo {
public static void main(String[] args) {
// 声明核心组件,放在try外部,方便finally关闭
Connection conn = null;
PreparedStatement pstmt = null;
try {
// 1. 加载JDBC驱动(MySQL 8.0+ 支持SPI机制,可省略此步,兼容老版本建议保留)
Class.forName("com.mysql.cj.jdbc.Driver");
// 2. 获取数据库连接
conn = DriverManager.getConnection(JDBCConstants.URL, JDBCConstants.USERNAME, JDBCConstants.PASSWORD);
// 3. 定义SQL(使用?作为参数占位符)
String sql = "INSERT INTO student (name, age, gender) VALUES (?, ?, ?)";
// 4. 创建PreparedStatement对象,预编译SQL
pstmt = conn.prepareStatement(sql);
// 5. 设置参数(索引从1开始,与SQL中?的顺序一致)
pstmt.setString(1, "赵六"); // 第一个?对应name字段
pstmt.setInt(2, 22); // 第二个?对应age字段
pstmt.setString(3, "男"); // 第三个?对应gender字段
// 6. 执行SQL(INSERT/UPDATE/DELETE用executeUpdate(),返回受影响的行数)
int rows = pstmt.executeUpdate();
// 7. 处理执行结果
System.out.println("新增成功,受影响行数:" + rows);
} catch (ClassNotFoundException e) {
// 驱动加载失败异常(如依赖未导入、类名写错)
e.printStackTrace();
} catch (SQLException e) {
// 数据库操作异常(如SQL语法错误、连接失败)
e.printStackTrace();
} finally {
// 8. 关闭资源(顺序:ResultSet → PreparedStatement → Connection,逆序关闭,避免内存泄漏)
try {
if (pstmt != null) pstmt.close();
if (conn != null) conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
3.2 查询数据(SELECT)
查询student表的所有数据,使用ResultSet遍历结果集,提取每条数据的字段值,注意结果集的遍历方式。
java
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class JDBCSelectDemo {
public static void main(String[] args) {
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null; // 查询操作需用到ResultSet存储结果集
try {
// 1. 加载驱动(可省略)
Class.forName("com.mysql.cj.jdbc.Driver");
// 2. 获取连接
conn = DriverManager.getConnection(JDBCConstants.URL, JDBCConstants.USERNAME, JDBCConstants.PASSWORD);
// 3. 定义查询SQL
String sql = "SELECT id, name, age, gender, create_time FROM student";
// 4. 创建PreparedStatement,预编译SQL
pstmt = conn.prepareStatement(sql);
// 5. 执行查询(SELECT用executeQuery(),返回ResultSet结果集)
rs = pstmt.executeQuery();
// 6. 遍历结果集(rs.next():移动到下一条数据,返回true表示有数据)
while (rs.next()) {
// 提取字段值(推荐用字段名,避免列索引变化导致错误)
int id = rs.getInt("id");
String name = rs.getString("name");
int age = rs.getInt("age");
String gender = rs.getString("gender");
String createTime = rs.getString("create_time");
// 打印结果
System.out.printf("id:%d,姓名:%s,年龄:%d,性别:%s,创建时间:%s%n",
id, name, age, gender, createTime);
}
} catch (ClassNotFoundException | SQLException e) {
e.printStackTrace();
} finally {
// 关闭资源(顺序:ResultSet → PreparedStatement → Connection)
try {
if (rs != null) rs.close();
if (pstmt != null) pstmt.close();
if (conn != null) conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
3.3 修改数据(UPDATE)
修改指定id的学生信息,核心逻辑与新增一致,重点是通过参数占位符传递修改后的值和条件。
java
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class JDBCUpdateDemo {
public static void main(String[] args) {
Connection conn = null;
PreparedStatement pstmt = null;
try {
Class.forName("com.mysql.cj.jdbc.Driver");
conn = DriverManager.getConnection(JDBCConstants.URL, JDBCConstants.USERNAME, JDBCConstants.PASSWORD);
// 定义修改SQL:修改id=4的学生年龄为23
String sql = "UPDATE student SET age = ? WHERE id = ?";
pstmt = conn.prepareStatement(sql);
// 设置参数(先设置修改后的值,再设置条件参数)
pstmt.setInt(1, 23); // 第一个?:修改后的年龄
pstmt.setInt(2, 4); // 第二个?:条件id
// 执行修改,获取受影响行数
int rows = pstmt.executeUpdate();
System.out.println("修改成功,受影响行数:" + rows);
} catch (ClassNotFoundException | SQLException e) {
e.printStackTrace();
} finally {
try {
if (pstmt != null) pstmt.close();
if (conn != null) conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
3.4 删除数据(DELETE)
删除指定条件的学生数据,注意添加WHERE条件,避免误删全表数据,这是生产环境的重要规范。
java
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class JDBCDeleteDemo {
public static void main(String[] args) {
Connection conn = null;
PreparedStatement pstmt = null;
try {
Class.forName("com.mysql.cj.jdbc.Driver");
conn = DriverManager.getConnection(JDBCConstants.URL, JDBCConstants.USERNAME, JDBCConstants.PASSWORD);
// 定义删除SQL:删除id=4的学生(必须加WHERE条件)
String sql = "DELETE FROM student WHERE id = ?";
pstmt = conn.prepareStatement(sql);
// 设置条件参数
pstmt.setInt(1, 4);
// 执行删除
int rows = pstmt.executeUpdate();
System.out.println("删除成功,受影响行数:" + rows);
} catch (ClassNotFoundException | SQLException e) {
e.printStackTrace();
} finally {
try {
if (pstmt != null) pstmt.close();
if (conn != null) conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
四、JDBC进阶特性
4.1 事务控制
JDBC的事务管理通过Connection对象实现,核心是控制事务的提交(commit)和回滚(rollback),确保一组SQL操作要么全部成功,要么全部失败,避免数据错乱(如转账场景)。
JDBC默认开启自动提交(每执行一条SQL自动提交),手动控制事务需关闭自动提交,执行完所有SQL后手动提交,出现异常则回滚。
java
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class JDBCTransactionDemo {
public static void main(String[] args) {
Connection conn = null;
PreparedStatement pstmt1 = null;
PreparedStatement pstmt2 = null;
try {
Class.forName("com.mysql.cj.jdbc.Driver");
conn = DriverManager.getConnection(JDBCConstants.URL, JDBCConstants.USERNAME, JDBCConstants.PASSWORD);
// 1. 关闭自动提交,开启手动事务
conn.setAutoCommit(false);
// 2. 执行一组SQL操作(模拟转账:张三减100,李四加100)
// 操作1:张三年龄减1(模拟扣款)
String sql1 = "UPDATE student SET age = age - 1 WHERE name = ?";
pstmt1 = conn.prepareStatement(sql1);
pstmt1.setString(1, "张三");
pstmt1.executeUpdate();
// 操作2:李四年龄加1(模拟收款)
String sql2 = "UPDATE student SET age = age + 1 WHERE name = ?";
pstmt2 = conn.prepareStatement(sql2);
pstmt2.setString(1, "李四");
pstmt2.executeUpdate();
// 3. 所有操作执行成功,手动提交事务
conn.commit();
System.out.println("事务提交成功,操作完成!");
} catch (Exception e) {
e.printStackTrace();
try {
// 4. 出现异常,回滚事务(撤销所有操作)
if (conn != null) conn.rollback();
System.out.println("事务回滚,操作失败!");
} catch (SQLException ex) {
ex.printStackTrace();
}
} finally {
// 关闭资源
try {
if (pstmt1 != null) pstmt1.close();
if (pstmt2 != null) pstmt2.close();
if (conn != null) conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
进阶:JDBC支持设置保存点(Savepoint),可实现事务的部分回滚,适用于复杂业务场景,即回滚到指定保存点,而不是回滚整个事务。
4.2 连接池
传统JDBC每次操作数据库都需要创建和关闭连接,连接的创建和销毁成本极高,频繁操作会导致系统性能下降,甚至耗尽数据库连接。连接池的核心作用是"复用连接",提前创建一批连接存入池中,使用时从池中获取,用完后归还,无需重复创建和销毁。
生产环境常用的连接池:Druid(阿里出品,性能最优、功能最全)、C3P0(老牌连接池,稳定性好)、HikariCP(Spring Boot默认连接池,轻量高效),以下是Druid连接池的简单使用示例。
Druid连接池使用步骤
- 添加Druid Maven依赖:
xml
<!-- Druid连接池依赖 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.16</version>
</dependency>
- 使用Druid获取连接,替代DriverManager:
java
import com.alibaba.druid.pool.DruidDataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class DruidDemo {
public static void main(String[] args) {
// 1. 创建Druid数据源(连接池)
DruidDataSource dataSource = new DruidDataSource();
// 2. 配置连接池参数(与JDBC连接信息一致)
dataSource.setUrl(JDBCConstants.URL);
dataSource.setUsername(JDBCConstants.USERNAME);
dataSource.setPassword(JDBCConstants.PASSWORD);
// 3. 配置连接池核心参数(优化性能)
dataSource.setInitialSize(5); // 初始连接数
dataSource.setMaxActive(20); // 最大连接数
dataSource.setMinIdle(3); // 最小空闲连接数
dataSource.setMaxWait(1000); // 获取连接的最大等待时间(毫秒)
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
// 4. 从连接池获取连接(无需手动创建,用完归还)
conn = dataSource.getConnection();
// 后续操作与普通JDBC一致
String sql = "SELECT * FROM student";
pstmt = conn.prepareStatement(sql);
rs = pstmt.executeQuery();
while (rs.next()) {
System.out.println("姓名:" + rs.getString("name") + ",年龄:" + rs.getInt("age"));
}
} 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();
}
}
}
}
4.3 批处理
当需要批量执行大量SQL语句(如批量插入、批量修改)时,使用JDBC的批处理功能,可大幅提升执行效率,减少与数据库的交互次数。
java
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class JdbcBatchDemo {
public static void main(String[] args) {
Connection conn = null;
PreparedStatement pstmt = null;
try {
Class.forName("com.mysql.cj.jdbc.Driver");
conn = DriverManager.getConnection(JDBCConstants.URL, JDBCConstants.USERNAME, JDBCConstants.PASSWORD);
conn.setAutoCommit(false); // 关闭自动提交,提升批处理效率
// 批量插入SQL
String sql = "INSERT INTO student (name, age, gender) VALUES (?, ?, ?)";
pstmt = conn.prepareStatement(sql);
// 批量添加参数(模拟批量插入3条数据)
for (int i = 0; i < 3; i++) {
pstmt.setString(1, "批量用户" + (i+1));
pstmt.setInt(2, 20 + i);
pstmt.setString(3, "男");
pstmt.addBatch(); // 将SQL添加到批处理队列
}
// 执行批处理,返回每条SQL的受影响行数
int[] rows = pstmt.executeBatch();
conn.commit(); // 提交事务
System.out.println("批量插入成功,总受影响行数:" + rows.length);
} catch (ClassNotFoundException | SQLException e) {
e.printStackTrace();
try {
if (conn != null) conn.rollback();
} catch (SQLException ex) {
ex.printStackTrace();
}
} finally {
try {
if (pstmt != null) pstmt.close();
if (conn != null) conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
五、JDBC常见问题与避坑指南
-
驱动加载失败(ClassNotFoundException):检查JDBC驱动依赖是否导入、驱动类名是否正确(MySQL 8.0是com.mysql.cj.jdbc.Driver,5.7是com.mysql.jdbc.Driver)。
-
数据库连接失败(SQLException):检查URL参数(时区、编码)是否正确、MySQL服务是否启动、用户名密码是否正确、3306端口是否被占用。
-
SQL注入风险:禁止使用Statement执行动态SQL,必须使用PreparedStatement的参数占位符(?),避免直接拼接SQL字符串。
-
资源泄漏:必须在finally中关闭ResultSet、PreparedStatement、Connection,顺序为逆序关闭;使用连接池时,确保连接正常归还。
-
时区异常:MySQL 8.0+ 必须在URL中指定serverTimezone(如Asia/Shanghai),否则会出现时区偏差导致连接失败。
-
中文乱码:URL中添加characterEncoding=utf8,同时确保数据库、表的编码为utf8mb4,驱动版本与MySQL版本适配。
六、JDBC核心总结
-
JDBC是Java操作关系型数据库的基础API,核心是"接口+驱动",屏蔽数据库底层差异,实现统一操作。
-
核心流程:加载驱动 → 获取连接 → 创建SQL执行器 → 执行SQL → 处理结果 → 关闭资源,固定且通用。
-
生产环境首选PreparedStatement(防SQL注入)、连接池(复用连接)、事务控制(保证数据安全),这三者是JDBC进阶的核心。
-
避坑重点:驱动版本与MySQL版本适配、资源必须关闭、避免SQL注入、正确配置URL参数。
-
JDBC是MyBatis等ORM框架的底层基础,掌握JDBC原理,能更好地理解ORM框架的工作机制,排查底层问题。