【零基础学MySQL】第十六章:Java JDBC 操作 MySQL
在 Java 开发中,操作数据库是核心需求之一,而 JDBC(Java Database Connectivity)作为 Java 访问数据库的标准接口,是连接 Java 应用与关系型数据库(如 MySQL)的桥梁。本文将从 JDBC 的基本概念出发,全面讲解如何使用 JDBC 操作 MySQL 数据库,涵盖环境搭建、核心 API、CRUD 操作、事务管理、批处理、连接池等关键知识点,并提供完整的示例代码,帮助初学者快速入门,同时为进阶开发者梳理核心要点。
一、JDBC 核心概念与工作原理
在开始实战前,我们需要先理解 JDBC 的核心组成和工作流程,这是后续操作的基础。
1.1 什么是 JDBC?
JDBC 是 Java 提供的一套用于操作数据库的标准 API,它定义了 Java 程序与数据库交互的接口(如Connection、Statement、ResultSet等),而具体的实现则由数据库厂商提供(即JDBC 驱动,如 MySQL 的mysql-connector-java)。通过 JDBC,开发者可以用统一的代码操作不同的关系型数据库(MySQL、Oracle、SQL Server 等),无需关注数据库底层的差异。
1.2 JDBC 核心组件
JDBC API 主要包含以下 5 个核心接口 / 类,它们共同完成数据库的连接与操作:
-
Driver:数据库驱动接口,负责加载数据库驱动并与数据库建立连接(通常由厂商实现,如
com.mysql.cj.jdbc.Driver)。 -
DriverManager:驱动管理类,负责注册驱动、管理驱动列表,并根据 URL 获取数据库连接(
Connection)。 -
Connection:数据库连接接口,代表 Java 程序与数据库的一次连接,是所有数据库操作的基础(如创建
Statement、管理事务)。 -
Statement:SQL 执行接口,用于向数据库发送 SQL 语句并执行(分为
Statement、PreparedStatement、CallableStatement三类,推荐使用PreparedStatement避免 SQL 注入)。 -
ResultSet:结果集接口,用于存储 SQL 查询(如
SELECT)返回的数据,可通过游标遍历数据。
1.3 JDBC 工作流程
使用 JDBC 操作 MySQL 的核心流程可概括为 7 步:
-
导入 JDBC 驱动(MySQL Connector/J);
-
加载并注册数据库驱动;
-
编写 MySQL 数据库连接 URL、用户名和密码;
-
通过
DriverManager获取数据库连接(Connection); -
创建
Statement/PreparedStatement对象,编写并执行 SQL 语句; -
处理 SQL 执行结果(查询用
ResultSet遍历,增删改用返回影响行数判断); -
关闭资源(
ResultSet、Statement、Connection,需按 “后创建先关闭” 顺序,且用try-catch-finally或try-with-resources保证关闭)。
二、环境搭建:准备 JDBC 驱动与 MySQL
在编写代码前,需先完成环境准备,核心是引入 MySQL 的 JDBC 驱动并确保 MySQL 服务正常运行。
2.1 下载并引入 MySQL JDBC 驱动
MySQL 官方提供的 JDBC 驱动名为MySQL Connector/J,目前最新版本为 8.x(需注意:8.x 版本与 5.x 版本在驱动类、URL 格式上有差异,本文以 8.x 为例)。
方式 1:手动下载并添加到项目(适合非 Maven 项目)
-
访问 MySQL 官网下载驱动:MySQL Connector/J 下载页;
-
选择 “Platform Independent”,下载 ZIP 压缩包;
-
解压后获取
mysql-connector-java-8.0.xx.jar文件; -
在 IDEA/Eclipse 中,右键项目 →
Build Path→Add External Archives,选择上述 JAR 包添加。
方式 2:通过 Maven 引入(推荐,适合 Maven 项目)
在pom.xml中添加依赖,Maven 会自动下载并管理驱动:
<!-- MySQL JDBC 驱动(8.x版本) -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version> <!-- 建议使用最新稳定版 -->
<scope>runtime</scope> <!-- 仅运行时需要,编译时无需依赖 -->
</dependency>
2.2 确保 MySQL 服务正常运行
-
安装 MySQL(推荐 8.x 版本,与驱动版本匹配);
-
启动 MySQL 服务(Windows 用
net start mysql,Linux 用systemctl start mysqld); -
用 Navicat 或命令行创建测试数据库和表(后续示例将基于此表操作):
-- 1. 创建测试数据库
CREATE DATABASE jdbc_demo CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 2. 使用数据库
USE jdbc_demo;
-- 3. 创建用户表(user)
CREATE TABLE user (
id INT PRIMARY KEY AUTO_INCREMENT, -- 主键自增
username VARCHAR(50) NOT NULL UNIQUE, -- 用户名(唯一)
password VARCHAR(50) NOT NULL, -- 密码
age INT, -- 年龄
create_time DATETIME DEFAULT CURRENT_TIMESTAMP -- 创建时间(默认当前时间)
);
-- 4. 插入测试数据
INSERT INTO user (username, password, age) VALUES
('zhangsan', '123456', 20),
('lisi', '654321', 22);
三、JDBC 操作 MySQL 基础实战:CRUD
CRUD(Create - 创建、Read - 查询、Update - 更新、Delete - 删除)是数据库操作的核心,下面将通过完整示例讲解如何用 JDBC 实现这四类操作。
3.1 核心注意事项(避坑指南)
在编写代码前,先明确 8.x 版本驱动的关键差异(与 5.x 对比):
-
驱动类:8.x 为
com.mysql.cj.jdbc.Driver(5.x 为com.mysql.jdbc.Driver); -
URL 格式:需添加
serverTimezone参数(解决时区问题),格式为jdbc:mysql://``localhost:3306/``数据库名?serverTimezone=UTC&useSSL=false; -
资源关闭:必须关闭
ResultSet、Statement、Connection,推荐使用 JDK7 + 的try-with-resources(自动关闭资源,无需手动写finally)。
3.2 示例 1:查询操作(Read)
查询user表中所有用户数据,或根据用户名查询指定用户。
import java.sql.*;
/**
* JDBC 查询操作示例(SELECT)
*/
public class JdbcSelectDemo {
// 1. 数据库连接信息(建议配置在配置文件中,此处为演示直接定义)
private static final String URL = "jdbc:mysql://localhost:3306/jdbc_demo?serverTimezone=UTC&useSSL=false&allowPublicKeyRetrieval=true";
private static final String USER = "root"; // 你的MySQL用户名
private static final String PASSWORD = "123456"; // 你的MySQL密码
public static void main(String[] args) {
// 2. 使用 try-with-resources 自动关闭资源(Connection、PreparedStatement、ResultSet)
try (
// 3. 获取数据库连接
Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
// 4. 创建 PreparedStatement(预编译SQL,避免SQL注入)
PreparedStatement pstmt = conn.prepareStatement("SELECT id, username, password, age, create_time FROM user WHERE username = ?");
) {
// 5. 给SQL中的占位符(?)赋值(索引从1开始)
pstmt.setString(1, "zhangsan"); // 查询用户名是zhangsan的用户
// 6. 执行查询,获取结果集(ResultSet)
ResultSet rs = pstmt.executeQuery();
// 7. 遍历结果集(游标默认在第一行之前,需用next()移动到下一行)
while (rs.next()) {
// 通过列名或列索引获取数据(推荐用列名,避免索引变化导致错误)
int id = rs.getInt("id");
String username = rs.getString("username");
String password = rs.getString("password");
int age = rs.getInt("age");
Timestamp createTime = rs.getTimestamp("create_time");
// 打印查询结果
System.out.printf("ID: %d, 用户名: %s, 密码: %s, 年龄: %d, 创建时间: %s%n",
id, username, password, age, createTime);
}
// 注意:ResultSet也需关闭,若用try-with-resources,可将rs声明在try()中
rs.close();
} catch (SQLException e) {
// 8. 处理SQL异常(建议打印详细异常信息,便于排查)
e.printStackTrace();
}
}
}
关键说明
-
PreparedStatement:通过?占位符传递参数,避免 SQL 注入(如用户输入' OR 1=1 --时,预编译会自动转义); -
ResultSet遍历:rs.next()返回true表示存在下一行数据,可通过getXxx(列名/索引)获取对应类型的数据(如getInt()、getString()、getTimestamp()); -
时区参数:
serverTimezone=UTC(或Asia/Shanghai)必须添加,否则会报The server time zone value 'XXX' is unrecognized错误。
3.3 示例 2:插入操作(Create)
向user表中插入一条新用户数据,通过返回的 “影响行数” 判断插入是否成功。
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
/**
* JDBC 插入操作示例(INSERT)
*/
public class JdbcInsertDemo {
private static final String URL = "jdbc:mysql://localhost:3306/jdbc_demo?serverTimezone=UTC&useSSL=false";
private static final String USER = "root";
private static final String PASSWORD = "123456";
public static void main(String[] args) {
// 要插入的用户数据
String username = "wangwu";
String password = "888888";
int age = 25;
try (
Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
// 预编译插入SQL(注意:id是自增,无需手动插入)
PreparedStatement pstmt = conn.prepareStatement("INSERT INTO user (username, password, age) VALUES (?, ?, ?)");
) {
// 给占位符赋值
pstmt.setString(1, username);
pstmt.setString(2, password);
pstmt.setInt(3, age);
// 执行插入操作:executeUpdate()返回受影响的行数(int类型)
int affectedRows = pstmt.executeUpdate();
// 判断插入是否成功
if (affectedRows > 0) {
System.out.println("插入用户成功!受影响行数:" + affectedRows);
} else {
System.out.println("插入用户失败!");
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
关键说明
-
executeUpdate():用于执行INSERT、UPDATE、DELETE等 DML 语句,返回受影响的行数(若为 0 表示操作未生效); -
自增主键:若表中主键是自增(
AUTO_INCREMENT),SQL 中无需包含主键列,数据库会自动生成。
3.4 示例 3:更新操作(Update)
修改user表中指定用户的信息(如修改密码或年龄)。
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
/**
* JDBC 更新操作示例(UPDATE)
*/
public class JdbcUpdateDemo {
private static final String URL = "jdbc:mysql://localhost:3306/jdbc_demo?serverTimezone=UTC&useSSL=false";
private static final String USER = "root";
private static final String PASSWORD = "123456";
public static void main(String[] args) {
// 要更新的条件(用户名)和新数据(新密码)
String username = "wangwu";
String newPassword = "999999";
try (
Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
// 预编译更新SQL(根据用户名修改密码)
PreparedStatement pstmt = conn.prepareStatement("UPDATE user SET password = ? WHERE username = ?");
) {
// 赋值(注意:占位符顺序与SQL中?的顺序一致)
pstmt.setString(1, newPassword); // 第一个?是新密码
pstmt.setString(2, username); // 第二个?是用户名(更新条件)
// 执行更新,获取受影响行数
int affectedRows = pstmt.executeUpdate();
if (affectedRows > 0) {
System.out.println("更新用户密码成功!受影响行数:" + affectedRows);
} else {
System.out.println("更新失败:未找到用户名=" + username + "的用户");
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
3.5 示例 4:删除操作(Delete)
删除user表中指定条件的用户数据(如删除用户名是wangwu的用户)。
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
/**
* JDBC 删除操作示例(DELETE)
*/
public class JdbcDeleteDemo {
private static final String URL = "jdbc:mysql://localhost:3306/jdbc_demo?serverTimezone=UTC&useSSL=false";
private static final String USER = "root";
private static final String PASSWORD = "123456";
public static void main(String[] args) {
// 要删除的用户条件(用户名)
String username = "wangwu";
try (
Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
// 预编译删除SQL(注意:避免无条件DELETE,否则会删除全表数据)
PreparedStatement pstmt = conn.prepareStatement("DELETE FROM user WHERE username = ?");
) {
pstmt.setString(1, username);
int affectedRows = pstmt.executeUpdate();
if (affectedRows > 0) {
System.out.println("删除用户成功!受影响行数:" + affectedRows);
} else {
System.out.println("删除失败:未找到用户名=" + username + "的用户");
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
关键警告
-
执行
DELETE时必须加WHERE条件,否则会删除表中所有数据(生产环境需格外小心,建议先通过SELECT验证条件); -
重要数据删除前建议备份,或用 “逻辑删除”(如添加
is_deleted字段标记,而非物理删除)。
四、JDBC 进阶特性:事务、批处理、连接池
基础 CRUD 满足简单需求,但在实际开发中,还需掌握事务管理、批处理、连接池等进阶特性,以保证数据一致性和性能。
4.1 事务管理(ACID 特性保障)
事务是数据库操作的最小单元,具有ACID特性(原子性、一致性、隔离性、持久性)。JDBC 中默认事务是 “自动提交”(即每执行一条 SQL 就提交一次),但在多步操作(如转账:扣钱 + 加钱)中,需手动控制事务,确保所有操作要么全成功,要么全失败。
事务控制核心 API
-
conn.setAutoCommit(false):关闭自动提交(开启手动事务); -
conn.commit():所有操作执行成功后,提交事务; -
conn.rollback():若某步操作失败,回滚事务(恢复到事务开始前的状态); -
conn.setSavepoint():设置保存点(可回滚到指定保存点,而非全量回滚)。
示例:转账场景的事务控制
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
/**
* JDBC 事务管理示例(转账场景)
*/
public class JdbcTransactionDemo {
private static final String URL = "jdbc:mysql://localhost:3306/jdbc_demo?serverTimezone=UTC&useSSL=false";
private static final String USER = "root";
private static final String PASSWORD = "123456";
// 转账方法:fromUser 向 toUser 转账 money 元
public static void transferMoney(String fromUser, String toUser,
double money) {
// 注意:Connection 需在外部声明,以便在 catch 中回滚
Connection conn = null;
PreparedStatement pstmt1 = null; // 扣钱 SQL 的 PreparedStatement
PreparedStatement pstmt2 = null; // 加钱 SQL 的 PreparedStatement
try {
// 1. 获取数据库连接
conn = DriverManager.getConnection(URL, USER, PASSWORD);
// 2. 关闭自动提交,开启手动事务
conn.setAutoCommit(false);
// 3. 第一步:给 fromUser 扣钱(假设 user 表有 balance 字段,需先手动添加该字段)
String sql1 = "UPDATE user SET balance = balance - ? WHERE username = ? AND balance >= ?";
pstmt1 = conn.prepareStatement(sql1);
pstmt1.setDouble(1, money);
pstmt1.setString(2, fromUser);
pstmt1.setDouble(3, money); // 确保余额足够扣钱
int rows1 = pstmt1.executeUpdate();
// 模拟异常(测试事务回滚,实际开发中删除)
//int error = 1 / 0;
// 4. 第二步:给 toUser 加钱
String sql2 = "UPDATE user SET balance = balance + ? WHERE username = ?";
pstmt2 = conn.prepareStatement(sql2);
pstmt2.setDouble(1, money);
pstmt2.setString(2, toUser);
int rows2 = pstmt2.executeUpdate();
// 5. 验证两步操作是否都成功(rows1 和 rows2 都应为 1,否则说明用户不存在或余额不足)
if ((rows1 == 1) && (rows2 == 1)) {
// 6. 所有操作成功,提交事务
conn.commit();
System.out.println("转账成功!" + fromUser + "向" + toUser + "转账" +
money + "元");
} else {
// 7. 操作未完全成功,手动回滚
conn.rollback();
System.out.println("转账失败:用户不存在或余额不足");
}
} catch (SQLException e) {
e.printStackTrace();
// 8. 发生 SQL 异常时,回滚事务
try {
if (conn != null) {
conn.rollback();
System.out.println("转账异常,事务已回滚");
}
} catch (SQLException ex) {
ex.printStackTrace();
}
} finally {
// 9. 关闭资源(按后创建先关闭顺序)
try {
if (pstmt2 != null) {
pstmt2.close();
}
if (pstmt1 != null) {
pstmt1.close();
}
if (conn != null) {
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
// 测试转账:zhangsan 向 lisi 转账 100 元
transferMoney("zhangsan", "lisi", 100.0);
}
}
事务示例关键说明
-
连接声明位置:
Connection需在try外部声明,否则catch中无法调用rollback(); -
异常回滚:无论发生
SQL异常还是业务逻辑异常(如余额不足),都需手动回滚事务,避免数据不一致; -
模拟异常:若打开代码中“
int error = 1 / 0”的注释,会触发算术异常,事务会回滚,zhangsan的余额不会减少; -
余额字段:示例中用到了
balance字段,需先在user表中添加该字段:ALTER TABLE user ADD balance DOUBLE DEFAULT 0;。
4.2 批处理(Batch Processing):提升批量操作性能
当需要执行大量SQL语句(如批量插入1000条数据)时,逐条执行会频繁与数据库交互,导致性能低下。批处理允许将多条SQL语句打包,一次性发送给数据库执行,大幅减少网络交互次数,提升效率。
批处理核心API
-
addBatch():将SQL语句添加到批处理队列; -
executeBatch():执行批处理队列中的所有SQL语句,返回一个int[]数组,每个元素表示对应SQL的影响行数; -
clearBatch():清空批处理队列。
示例:批量插入1000条用户数据
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
/**
* JDBC 批处理示例(批量插入)
*/
public class JdbcBatchDemo {
private static final String URL = "jdbc:mysql://localhost:3306/jdbc_demo?serverTimezone=UTC&useSSL=false&rewriteBatchedStatements=true";
private static final String USER = "root";
private static final String PASSWORD = "123456";
public static void main(String[] args) {
long startTime = System.currentTimeMillis(); // 记录开始时间
try (
Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
// 预编译批量插入SQL
PreparedStatement pstmt = conn.prepareStatement("INSERT INTO user (username, password, age, balance) VALUES (?, ?, ?, ?)");
) {
// 关闭自动提交(批量操作建议关闭自动提交,进一步提升性能)
conn.setAutoCommit(false);
// 1. 循环添加1000条SQL到批处理队列
for (int i = 1; i <= 1000; i++) {
String username = "user_" + i;
String password = "pwd_" + i;
int age = 18 + (i % 20); // 年龄在18-37之间
double balance = 1000 + (i * 10); // 余额随i递增
// 给占位符赋值
pstmt.setString(1, username);
pstmt.setString(2, password);
pstmt.setInt(3, age);
pstmt.setDouble(4, balance);
// 将当前SQL添加到批处理队列
pstmt.addBatch();
// 可选:每100条执行一次批处理(避免队列过大)
if (i % 100 == 0) {
pstmt.executeBatch();
pstmt.clearBatch(); // 清空队列,准备下一批
}
}
// 2. 执行剩余的批处理(若总数不是100的倍数)
int[] affectedRowsArray = pstmt.executeBatch();
// 3. 提交事务
conn.commit();
// 4. 统计成功插入的条数
int successCount = 0;
for (int affectedRows : affectedRowsArray) {
if (affectedRows > 0) successCount++;
}
long endTime = System.currentTimeMillis(); // 记录结束时间
System.out.println("批量插入完成!成功插入" + successCount + "条数据,耗时:" + (endTime - startTime) + "ms");
} catch (SQLException e) {
e.printStackTrace();
}
}
}
批处理关键优化点
-
URL 参数:必须添加
rewriteBatchedStatements=true,MySQL 驱动默认不开启批处理优化,添加该参数后才会真正将多条 SQL 打包发送; -
关闭自动提交:批量操作时关闭
autoCommit,避免每条 SQL 都触发一次提交,减少数据库 IO; -
分批执行:若数据量极大(如 10 万条),建议每 100-1000 条执行一次
executeBatch(),避免批处理队列占用过多内存; -
性能对比:批量插入 1000 条数据,批处理耗时通常在 100ms 以内,而逐条插入可能需要几秒,性能差距显著。
4.3 数据库连接池:解决连接创建销毁开销问题
4.3.1 为什么需要连接池?
通过DriverManager获取连接时,每次都会创建一个新的物理连接(与数据库建立 TCP 连接),而连接的创建和销毁需要消耗大量资源(时间、CPU、内存)。在高并发场景下(如 1000 个用户同时访问),频繁创建连接会导致系统性能急剧下降,甚至数据库崩溃。
连接池的核心思想是:预先创建一定数量的连接,存储在 “连接池” 中,用户需要时直接从池子里获取,使用完后归还,避免重复创建和销毁。
4.3.2 常用连接池介绍
Java 中常用的数据库连接池有 3 种,均实现了javax.sql.DataSource接口(推荐使用DataSource获取连接,而非DriverManager):
-
C3P0:老牌连接池,稳定性好,但性能一般,配置复杂;
-
DBCP:Apache 开源连接池,轻量级,配置简单,但功能较少;
-
HikariCP:目前性能最好的连接池(Spring Boot 默认集成),速度快、内存占用低、配置简洁,推荐生产环境使用。
4.3.3 HikariCP 实战示例
下面以 HikariCP 为例,讲解如何通过连接池获取连接并操作数据库。
步骤 1:引入 HikariCP 依赖(Maven)
<!-- HikariCP 连接池 -->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>5.0.1</version> <!-- 最新稳定版 -->
</dependency>
<!-- MySQL 驱动(若已引入可忽略) -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
步骤 2:通过 HikariCP 获取连接并执行查询
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;
/**
* HikariCP 连接池实战示例
*/
public class HikariCpDemo {
// 1. 初始化HikariDataSource(连接池核心对象,建议全局单例)
private static final HikariDataSource dataSource;
static {
// 2. 配置连接池参数
HikariConfig config = new HikariConfig();
// 数据库连接信息
config.setJdbcUrl("jdbc:mysql://localhost:3306/jdbc_demo?serverTimezone=UTC&useSSL=false");
config.setUsername("root");
config.setPassword("123456");
// 连接池核心参数
config.setDriverClassName("com.mysql.cj.jdbc.Driver");
config.setMaximumPoolSize(10); // 连接池最大连接数(根据服务器性能调整,推荐10-20)
config.setMinimumIdle(5); // 连接池最小空闲连接数(保证有足够的空闲连接备用)
config.setIdleTimeout(300000); // 空闲连接超时时间(5分钟,超时后自动关闭)
config.setMaxLifetime(1800000); // 连接最大存活时间(30分钟,避免连接过期)
config.setConnectionTimeout(30000); // 获取连接的超时时间(30秒,超时则抛出异常)
// 3. 创建HikariDataSource(单例,全局共享)
dataSource = new HikariDataSource(config);
}
// 4. 获取连接的工具方法
public static Connection getConnection() throws SQLException {
// 从连接池获取连接(无需手动创建,用完后归还)
return dataSource.getConnection();
}
// 5. 测试:查询用户数据
public static void queryUser() {
try (
// 从连接池获取连接
Connection conn = getConnection();
PreparedStatement pstmt = conn.prepareStatement("SELECT username, age, balance FROM user LIMIT 5");
ResultSet rs = pstmt.executeQuery();
) {
System.out.println("查询前5条用户数据:");
while (rs.next()) {
String username = rs.getString("username");
int age = rs.getInt("age");
double balance = rs.getDouble("balance");
System.out.printf("用户名:%s,年龄:%d,余额:%.2f%n", username, age, balance);
}
} catch (SQLException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
// 执行查询
queryUser();
// 关闭连接池(仅在应用停止时调用,如Web应用的销毁方法)
// dataSource.close();
}
}
连接池关键说明
-
单例模式:
HikariDataSource是连接池的核心对象,创建成本高,必须全局单例(如通过static初始化),避免重复创建连接池; -
连接归还:通过
try-with-resources获取Connection,使用完后会自动调用close()方法,但此时并非关闭物理连接,而是将连接归还给连接池; -
参数调优:
-
maximumPoolSize:最大连接数,需根据数据库性能和并发量调整(如 Tomcat 默认 200 线程,连接池可设为 20-50); -
idleTimeout:空闲超时时间,避免空闲连接长期占用资源; -
connectionTimeout:获取连接超时时间,防止线程因等待连接而阻塞过久。
-
五、JDBC 常见问题与优化建议
在实际开发中,使用 JDBC 时容易遇到各种问题,下面总结常见问题及解决方案,并提供优化建议。
5.1 常见问题与解决方案
| 问题描述 | 原因分析 | 解决方案 |
|---|---|---|
时区错误:The server time zone value 'XXX' is unrecognized |
MySQL 8.x 驱动要求必须指定时区,否则无法连接 | URL 中添加serverTimezone=UTC或serverTimezone=Asia/Shanghai |
| SQL 注入攻击 | 使用Statement执行动态 SQL,未对用户输入进行转义 |
改用PreparedStatement,通过?占位符传递参数,自动转义特殊字符 |
| 连接泄露 | 未关闭Connection、Statement或ResultSet,导致连接无法归还连接池 |
1. 使用try-with-resources自动关闭资源;2. 手动关闭时按 “ResultSet → Statement → Connection” 顺序,且在finally中关闭 |
| 批处理性能差 | MySQL 驱动默认未开启批处理优化,多条 SQL 仍逐条执行 | URL 中添加rewriteBatchedStatements=true |
| 获取连接超时 | 连接池最大连接数不足,或连接被占用未释放 | 1. 调大maximumPoolSize;2. 检查是否存在连接泄露;3. 调大connectionTimeout(不推荐,需先排查根本原因) |
5.2 性能优化建议
-
优先使用 PreparedStatement:不仅能防止 SQL 注入,还能预编译 SQL 语句,后续执行相同 SQL 时无需重新编译,提升性能;
-
使用连接池:避免频繁创建和销毁连接,推荐 HikariCP,性能最优;
-
批量操作用批处理:大量插入 / 更新时,使用
addBatch()+executeBatch(),并添加rewriteBatchedStatements=true; -
关闭自动提交:批量操作或事务场景下,关闭
autoCommit,减少提交次数; -
合理设置 ResultSet 类型:若仅需读取数据且不回头遍历,使用
ResultSet.TYPE_FORWARD_ONLY(默认类型),性能最优; -
避免查询不必要的字段:
SELECT语句只查询需要的字段,而非SELECT *,减少数据传输量; -
使用连接池监控:生产环境中,通过连接池监控工具(如 HikariCP 的
HikariPoolMXBean)监控连接数、空闲时间等,及时发现问题。
六、总结
本文从 JDBC 的基础概念出发,逐步讲解了环境搭建、核心 API、CRUD 操作、事务管理、批处理和连接池等关键知识点,并提供了完整的示例代码。总结来说:
-
JDBC 核心流程:加载驱动 → 获取连接 → 创建 Statement → 执行 SQL → 处理结果 → 关闭资源;
-
安全与性能:用
PreparedStatement防 SQL 注入,用连接池提升性能,用批处理优化批量操作; -
事务保障:手动控制事务(
setAutoCommit(false)+commit()+rollback()),确保数据一致性; -
生产实践:连接池推荐 HikariCP,资源关闭需依赖
try-with-resources,避免资源泄露; -
问题排查:遇到异常时,优先查看 SQL 语法、连接配置和事务逻辑,结合日志定位问题。
对于初学者,建议先通过基础 CRUD 熟悉 JDBC 核心流程,再逐步掌握事务、批处理和连接池等进阶特性;对于有经验的开发者,需关注性能优化和生产环境稳定性,合理选择连接池参数,避免常见坑点(如 SQL 注入、连接泄露)。
七、JDBC 扩展:从原生 JDBC 到 ORM 框架
原生 JDBC 虽然灵活,但在复杂项目中存在代码冗余、重复劳动多(如手动封装结果集到实体类)等问题。为提升开发效率,实际项目中更多使用ORM(Object-Relational Mapping,对象关系映射)框架,ORM 框架底层基于 JDBC 实现,将 Java 对象与数据库表进行映射,简化数据库操作。
7.1 常见 ORM 框架介绍
-
MyBatis:轻量级 ORM 框架,半自动化(需手动编写 SQL),灵活性高,适合对 SQL 有精细控制的场景,是目前国内互联网公司的主流选择;
-
Hibernate:全自动化 ORM 框架,无需编写 SQL,通过注解或 XML 配置映射关系,适合快速开发,但灵活性较低,复杂 SQL 场景下性能优化难度大;
-
Spring Data JPA:基于 JPA(Java Persistence API)规范的框架,进一步简化操作,支持通过方法名自动生成 SQL(如
findByUsername),适合简单 CRUD 场景。
7.2 原生 JDBC 与 MyBatis 对比(以查询用户为例)
原生 JDBC 实现(需手动封装结果集)
// 原生JDBC查询用户后,手动封装到User实体类
public User queryUserByUsername(String username) {
User user = null;
try (
Connection conn = HikariCpDemo.getConnection();
PreparedStatement pstmt = conn.prepareStatement("SELECT id, username, password, age FROM user WHERE username = ?");
ResultSet rs = pstmt.executeQuery();
) {
pstmt.setString(1, username);
if (rs.next()) {
user = new User();
user.setId(rs.getInt("id"));
user.setUsername(rs.getString("username"));
user.setPassword(rs.getString("password"));
user.setAge(rs.getInt("age"));
}
} catch (SQLException e) {
e.printStackTrace();
}
return user;
}
MyBatis 实现(自动封装结果集)
- 定义 Mapper 接口(无需实现类):
public interface UserMapper {
// MyBatis自动根据方法名和参数生成SQL,或通过XML配置SQL
@Select("SELECT id, username, password, age FROM user WHERE username = #{username}")
User findByUsername(@Param("username") String username);
}
- 调用 Mapper 接口(MyBatis 自动管理连接和结果集封装):
// 通过MyBatis的SqlSession获取Mapper接口
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 直接调用方法,MyBatis自动执行SQL并返回封装后的User对象
User user = userMapper.findByUsername("zhangsan");
对比结论
-
原生 JDBC:需手动处理连接、SQL 执行、结果集封装,代码冗余,适合简单场景或学习 JDBC 原理;
-
ORM 框架(如 MyBatis):简化重复工作,专注业务逻辑,提升开发效率,适合复杂项目。
7.3 处理大字段(BLOB/CLOB)
在实际开发中,有时需要存储大文件(如图片、PDF)或长文本(如文章内容),MySQL 中对应的数据类型为BLOB(二进制大对象)和CLOB(字符大对象)。下面通过原生 JDBC 示例讲解如何操作大字段。
示例 1:插入图片(BLOB 类型)
import java.io.FileInputStream;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.PreparedStatement;
/**
* JDBC 操作 BLOB 类型(插入图片)
*/
public class JdbcBlobDemo {
private static final String URL = "jdbc:mysql://localhost:3306/jdbc_demo?serverTimezone=UTC&useSSL=false";
private static final String USER = "root";
private static final String PASSWORD = "123456";
public static void insertImage(String username, String imagePath) {
String sql = "UPDATE user SET avatar = ? WHERE username = ?"; // avatar字段类型为MEDIUMBLOB
try (
Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
PreparedStatement pstmt = conn.prepareStatement(sql);
// 读取图片文件为输入流
InputStream is = new FileInputStream(imagePath);
) {
// 设置BLOB参数(使用setBinaryStream)
pstmt.setBinaryStream(1, is);
pstmt.setString(2, username);
int rows = pstmt.executeUpdate();
if (rows > 0) {
System.out.println("图片插入成功!");
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
// 插入本地图片(替换为实际图片路径)
insertImage("zhangsan", "D:/test/avatar.jpg");
}
}
示例 2:读取图片(BLOB 类型)
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
/**
* JDBC 操作 BLOB 类型(读取图片)
*/
public class JdbcReadBlobDemo {
private static final String URL = "jdbc:mysql://localhost:3306/jdbc_demo?serverTimezone=UTC&useSSL=false";
private static final String USER = "root";
private static final String PASSWORD = "123456";
public static void readImage(String username, String outputPath) {
String sql = "SELECT avatar FROM user WHERE username = ?";
try (
Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
PreparedStatement pstmt = conn.prepareStatement(sql);
) {
pstmt.setString(1, username);
ResultSet rs = pstmt.executeQuery();
if (rs.next()) {
// 读取BLOB字段为输入流
InputStream is = rs.getBinaryStream("avatar");
// 将输入流写入本地文件
try (OutputStream os = new FileOutputStream(outputPath)) {
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) != -1) {
os.write(buffer, 0, len);
}
System.out.println("图片读取成功!");
}
}
rs.close();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
// 读取图片并保存到本地(替换为实际输出路径)
readImage("zhangsan", "D:/test/zhangsan_avatar.jpg");
}
}
大字段操作注意事项
-
字段类型选择:
TINYBLOB(最大 255 字节)、BLOB(最大 64KB)、MEDIUMBLOB(最大 16MB)、LONGBLOB(最大 4GB),根据文件大小选择合适类型; -
性能问题:大字段会增加数据库存储和网络传输压力,建议将大文件(如图片、视频)存储在文件服务器(如 MinIO、OSS),数据库仅存储文件 URL,而非直接存储文件内容;
-
流操作:操作 BLOB 时需使用输入流(
InputStream)和输出流(OutputStream),避免将大文件加载到内存导致 OOM(内存溢出)。
八、附录:常用工具类封装
为减少重复代码,实际开发中通常会封装 JDBC 工具类,统一管理连接获取、资源关闭等操作。下面提供一个基于 HikariCP 的工具类示例。
JDBC 工具类(HikariCP 版)
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
/**
* JDBC 工具类(基于HikariCP连接池)
*/
public class JdbcUtils {
// 静态HikariDataSource(全局单例)
private static final HikariDataSource DATA_SOURCE;
// 静态代码块初始化连接池
static {
HikariConfig config = new HikariConfig();
// 建议从配置文件(如application.properties)读取配置,避免硬编码
config.setJdbcUrl("jdbc:mysql://localhost:3306/jdbc_demo?serverTimezone=UTC&useSSL=false");
config.setUsername("root");
config.setPassword("123456");
config.setDriverClassName("com.mysql.cj.jdbc.Driver");
config.setMaximumPoolSize(10);
config.setMinimumIdle(5);
config.setIdleTimeout(300000);
config.setMaxLifetime(1800000);
config.setConnectionTimeout(30000);
DATA_SOURCE = new HikariDataSource(config);
}
/**
* 获取数据库连接
* @return Connection
* @throws SQLException 连接获取失败时抛出异常
*/
public static Connection getConnection() throws SQLException {
return DATA_SOURCE.getConnection();
}
/**
* 关闭资源(Statement + Connection)
* @param stmt Statement对象(可null)
* @param conn Connection对象(可null)
*/
public static void close(Statement stmt, Connection conn) {
close(null, stmt, conn);
}
/**
* 关闭资源(ResultSet + Statement + Connection)
* @param rs ResultSet对象(可null)
* @param stmt Statement对象(可null)
* @param conn Connection对象(可null)
*/
public static void close(ResultSet rs, Statement stmt, Connection conn) {
// 按后创建先关闭顺序关闭资源
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (stmt != null) {
try {
stmt.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (conn != null) {
try {
conn.close(); // 归还连接到连接池,非关闭物理连接
} catch (SQLException e) {
e.printStackTrace();
}
}
}
/**
* 关闭连接池(仅在应用停止时调用)
*/
public static void closeDataSource() {
if (DATA_SOURCE != null && !DATA_SOURCE.isClosed()) {
DATA_SOURCE.close();
}
}
}
工具类使用示例(查询用户)
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class JdbcUtilsTest {
public static void main(String[] args) {
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
// 1. 获取连接(通过工具类)
conn = JdbcUtils.getConnection();
// 2. 创建PreparedStatement
String sql = "SELECT username, age FROM user WHERE id = ?";
pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, 1);
// 3. 执行查询
rs = pstmt.executeQuery();
// 4. 处理结果
if (rs.next()) {
String username = rs.getString("username");
int age = rs.getInt("age");
System.out.printf("ID=1的用户:用户名=%s,年龄=%d%n", username, age);
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
// 5. 关闭资源(通过工具类)
JdbcUtils.close(rs, pstmt, conn);
}
}
}
工具类设计思路
-
单例连接池:通过
static代码块初始化HikariDataSource,确保全局唯一; -
配置解耦:实际项目中,建议将 URL、用户名、密码等配置放在
application.properties或db.properties文件中,通过Properties类读取,避免硬编码; -
资源关闭统一:提供重载的
close()方法,统一处理资源关闭,减少重复代码; -
异常处理:在工具类中打印异常信息,方便排查问题,同时避免上层代码重复捕获关闭异常。
九、结语
JDBC 是 Java 操作关系型数据库的基础,掌握 JDBC 不仅能解决简单的数据库交互需求,更能帮助理解 ORM 框架的底层原理(如 MyBatis 如何通过 JDBC 执行 SQL)。本文从基础到进阶,覆盖了 JDBC 的核心知识点和实战技巧,包括 CRUD、事务、批处理、连接池、大字段操作等,同时提供了工具类封装和 ORM 框架的扩展方向。
在实际开发中,需根据项目规模和需求选择合适的技术方案:
-
小型项目或学习场景:可使用原生 JDBC + 工具类;
-
中大型项目:推荐使用 MyBatis 等 ORM 框架,结合 HikariCP 连接池,兼顾灵活性和开发效率;
-
大文件存储:优先使用文件服务器,避免数据库存储大字段。
希望本文能帮助开发者系统掌握 JDBC 操作 MySQL 的技能,在实际项目中规避常见坑点,写出高效、稳定的数据库交互代码。
(注:文档部分内容由 AI 生成!自行测试后再使用!)
更多推荐



所有评论(0)