很多初学者学 Java 数据库编程,第一步就是拿 JDBC 连 MySQL,但真正动手时会发现坑一个接一个——驱动类加载报错、连接超时、中文乱码、时区问题,甚至好不容易连上了,增删改查又不知道该从哪里下手。这篇内容就是用最直接的方式,把 Java 连接 MySQL 并完成数据交互的完整链路拆开讲透。里面包含我实际反复用过的连接代码、踩过的坑、以及一套可以直接拿去改的连接工具类,不管是准备课程设计、做期末项目,还是想搞懂 JDBC 底层原理,都值得收藏一份。
1. 整体设计与思路拆解
1.1 为什么第一课必须从 JDBC 开始
很多人问我,现在开发都用 MyBatis、Spring Data JPA,为什么还要学原生的 JDBC?我的看法很明确:框架只是 JDBC 的封装,底层再怎么花哨,最终还是走的 java.sql 包里的那套接口。学习 JDBC 不是为了让你以后写项目都手写连接,而是让你在遇到问题的时候,能顺着调用链往下追,而不是对着报错信息干瞪眼。
举个例子。你用 MyBatis 执行一条 insert,突然报了一个事务相关的错误。如果你不知道 MyBatis 底层拿到的其实就是 Connection,不知道它背后调用了 setAutoCommit(false) 和 commit(),你根本不知道去哪里查问题。我见过不少工作了两年的人,让他解释 MyBatis 的 SqlSession 跟 JDBC 的 Connection 之间的关系,依然一脸懵。这就是没打好底子的典型表现。
另外,JDBC 这套流程非常固定,一共就六步:加载驱动、获取连接、创建 Statement、执行 SQL、处理结果集、释放资源。只要你把这六步吃透,后面学什么数据库中间件都很快,因为所有的工具都是在替你简化这六步中的某几步。所以我的建议是:第一课必须手写一遍 JDBC,哪怕你已经会用 MyBatis,也值得从头走一遍,把每个环节的细节搞清楚。
1.2 JDBC 直连与 ORM 框架的取舍
既然题目是“数据交互”,就绕不开一个话题:到底是直接用 JDBC,还是上 ORM 框架?我给不同阶段的读者一个比较务实的结论:
- 如果你的目标是快速完成一个课程设计或者小工具,对并发和工程化要求不高,直接用 JDBC 完全够用,甚至代码量更少。
- 如果你是在做企业项目,需要处理复杂查询、分页、多表关联,直接用 JDBC 会写吐,这时上 MyBatis 或 JPA 更合理。
- 但无论哪种情况,你都应该知道 JDBC 写法的基本范式,因为框架的 bug 报错底层往往指向 JDBC 层,不懂底层排查成本极高。
从工程角度对比一下:
| 对比维度 | JDBC 原生 | ORM 框架(MyBatis 为例) |
|---|---|---|
| 学习成本 | 低,核心概念就几个 | 高,需要理解代理、映射、生命周期 |
| SQL 控制力 | 完全手动,灵活 | SQL 写在 XML 中,可控性也不错 |
| 开发效率 | 低,重复代码多 | 高,大量模板代码被自动处理 |
| 调试难度 | 出问题好定位 | 有一层封装,需要追源码 |
| 连接管理 | 自己管 | 框架集成连接池自动管 |
你会发现,JDBC 的唯一劣势是“重复代码多”,但这不是 JDBC 本身的问题,而是缺少封装。实际上你只要写一个 DBUtil 连接工具类和一个统一的 BaseDao,重复代码就能压到很低。这篇博客后面给到的示例,就采用了这种“原生 JDBC + 轻量封装”的方式,一方面是贴近课程设计的需求,另一方面也能让读者看清每一步在做什么。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与核心参数解析
2.1 版本选型:JDK、MySQL、驱动的三角关系
我遇到过太多版本不匹配导致的诡异报错,所以先把版本问题放在最前面。当前时间点写这篇文章,我的推荐组合是:
- JDK 8 或 11。JDK 8 是兼容性最好的版本,绝大多数的教材和企业老项目都在用;JDK 11 属于 LTS 版本,想用新版语法可以用它。
- MySQL 5.7 或 8.0。如果你是想体验 MySQL 8 的新特性,直接装 8.0;如果你想减少认证、字符集方面的麻烦,5.7 是老牌稳定选择。不过新项目我建议直接 8.0,别再用老版本了。
- MySQL Connector/J 驱动版本。这个最关键,8.0.x 的驱动和 5.1.x 的驱动写法完全不同。
具体来说,驱动类型和连接 URL 的对应关系如下:
| MySQL 版本 | 驱动类 | 连接 URL 关键差异 |
|---|---|---|
| 5.x | com.mysql.jdbc.Driver | jdbc:mysql://localhost:3306/dbname |
| 8.x | com.mysql.cj.jdbc.Driver | 需要加 serverTimezone 参数 |
如果你的 MySQL 是 8.0,但是驱动包用的还是旧的 5.1.x,可能会遇到比较奇怪的行为;反过来,如果你用 8.x 的驱动去连 5.7 的数据库,倒是基本兼容。所以我的习惯是:如果是新项目,一律用官方最新的 8.x 驱动,省心。
获取驱动包的几种方式,我也列一下:
- 去 Maven 中央仓库下载
mysql-connector-j-8.0.x.jar,然后放到项目的 lib 目录。 - 用 Maven 构建项目时,直接在
pom.xml里加依赖:
xml复制<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.0.33</version>
</dependency>
如果你是手动导入 jar 包的“非 Maven 项目”,记得在 IDEA 里右键 jar 包,选 Add as Library,否则编译会报找不到包。
2.2 连接 URL 的每个参数到底在干什么
很多教程给了连接 URL,但没人告诉你那一串参数有什么用。我在这里拆一下最常用的几个,以后你遇到问题也知道该调谁。
java复制String url = "jdbc:mysql://localhost:3306/blog_demo"
+ "?useUnicode=true"
+ "&characterEncoding=utf8"
+ "&useSSL=false"
+ "&serverTimezone=Asia/Shanghai"
+ "&allowPublicKeyRetrieval=true";
useUnicode=true和characterEncoding=utf8:解决中文乱码的关键。加上之后,客户端发给 MySQL 的 SQL 语句和读取的数据都会用 UTF-8 编码。useSSL=false:MySQL 8.0 默认开启 SSL,但你本地开发根本没有证书,不关掉会报 SSL 连接警告,虽说一般是 warning,但日志会非常吵。serverTimezone=Asia/Shanghai:MySQL 8.0 的驱动强制要求设置时区,不加会直接报The server time zone value '�й���ʱ��' is unrecognized。这里的乱码其实是“中国标准时间”的编码问题,设成Asia/Shanghai是通用做法。allowPublicKeyRetrieval=true:使用 MySQL 8.0 默认的caching_sha2_password认证插件时,客户端需要先从服务器获取公钥,这个参数不加会报Public Key Retrieval is not allowed。
连接参数看着不起眼,但每一个都是在实际报错中总结出来的。建议你把这一串直接复制到自己的工具类里,能少走很多弯路。
3. 从建库到完整增删改查的实操
3.1 建库建表 SQL
在写 Java 代码之前,先把数据库准备好。我这里建一个简单的用户表,字段不要太多,方便演示。
sql复制CREATE DATABASE IF NOT EXISTS blog_demo DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE blog_demo;
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`username` VARCHAR(50) NOT NULL COMMENT '用户名',
`email` VARCHAR(100) NOT NULL COMMENT '邮箱',
`age` INT DEFAULT 0 COMMENT '年龄',
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
这里有两个细节值得说明。第一,数据库和表的字符集都用了 utf8mb4,这是 MySQL 8.0 的默认字符集,支持完整的 Unicode 编码(包括 emoji),比老的 utf8 更全面。第二,id 字段用 BIGINT 自增,而不是 INT,从长远看更抗造,特别是做管理系统,数据量起来之后 INT 很容易撑不住。
3.2 手写一个连接工具类
连接不能每次用到都现写一遍 DriverManager.getConnection(...),那样代码冗余且不好维护。我习惯封装一个 DBUtil,把驱动加载写在静态代码块里,保证类加载时只执行一次。
java复制import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class DBUtil {
private static final String URL = "jdbc:mysql://localhost:3306/blog_demo"
+ "?useUnicode=true"
+ "&characterEncoding=utf8"
+ "&useSSL=false"
+ "&serverTimezone=Asia/Shanghai"
+ "&allowPublicKeyRetrieval=true";
private static final String USER = "root";
private static final String PASSWORD = "你的密码";
static {
try {
Class.forName("com.mysql.cj.jdbc.Driver");
} catch (ClassNotFoundException e) {
throw new ExceptionInInitializerError("MySQL 驱动加载失败,请检查 jar 包是否引入");
}
}
public static Connection getConnection() throws SQLException {
return DriverManager.getConnection(URL, USER, PASSWORD);
}
public static void close(AutoCloseable... resources) {
for (AutoCloseable resource : resources) {
if (resource != null) {
try {
resource.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
注意里面的 AutoCloseable... resources,这样调用方可以把 Connection、Statement、ResultSet 一次性传进来,统一关掉。为什么要这样?因为 JDBC 的资源如果只关 Connection 不关 ResultSet,在一定并发量下会慢慢累积资源占用,最后把数据库连接池拖垮。
重要提示:
Class.forName("com.mysql.cj.jdbc.Driver")这行,在 MySQL Connector/J 8.x 中,驱动类已经改名为com.mysql.cj.jdbc.Driver,以前老教程里的com.mysql.jdbc.Driver已经过时。虽然 8.x 驱动保留了对旧类名的兼容,但会打印一条 deprecation 警告。新代码直接写新的类名就好。
3.3 用 PreparedStatement 完成增删改查
连接类准备好后,业务代码就简单了。这里我强烈建议直接用 PreparedStatement,而不是 Statement。两者的区别用一个例子说清楚:
如果你用 Statement,需要手动拼接 SQL:
java复制String sql = "INSERT INTO user(username, email, age) VALUES('" + username + "', '" + email + "', " + age + ")";
这样写的问题有两个。第一,如果 username 里包含单引号,SQL 直接语法错误;如果包含恶意内容,比如 '; DROP TABLE user; --,那就是典型的 SQL 注入,后果非常严重。第二,每次执行都要经历完整的 SQL 解析流程,性能上也吃亏。
而 PreparedStatement 使用占位符 ?,参数通过 setXxx() 方法绑定,既不手动拼字符串,又会有预编译优化:
java复制import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
public class UserDao {
public int insert(String username, String email, int age) {
String sql = "INSERT INTO user(username, email, age) VALUES(?, ?, ?)";
try (Connection conn = DBUtil.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, username);
ps.setString(2, email);
ps.setInt(3, age);
return ps.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
return 0;
}
}
public int updateById(Long id, String email, int age) {
String sql = "UPDATE user SET email = ?, age = ? WHERE id = ?";
try (Connection conn = DBUtil.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, email);
ps.setInt(2, age);
ps.setLong(3, id);
return ps.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
return 0;
}
}
public int deleteById(Long id) {
String sql = "DELETE FROM user WHERE id = ?";
try (Connection conn = DBUtil.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setLong(1, id);
return ps.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
return 0;
}
}
public User findById(Long id) {
String sql = "SELECT id, username, email, age, created_at FROM user WHERE id = ?";
try (Connection conn = DBUtil.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setLong(1, id);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
User user = new User();
user.setId(rs.getLong("id"));
user.setUsername(rs.getString("username"));
user.setEmail(rs.getString("email"));
user.setAge(rs.getInt("age"));
user.setCreatedAt(rs.getTimestamp("created_at").toLocalDateTime());
return user;
}
}
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
public List<User> findAll() {
String sql = "SELECT id, username, email, age, created_at FROM user ORDER BY id DESC";
List<User> list = new ArrayList<>();
try (Connection conn = DBUtil.getConnection();
PreparedStatement ps = conn.prepareStatement(sql);
ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
User user = new User();
user.setId(rs.getLong("id"));
user.setUsername(rs.getString("username"));
user.setEmail(rs.getString("email"));
user.setAge(rs.getInt("age"));
user.setCreatedAt(rs.getTimestamp("created_at").toLocalDateTime());
list.add(user);
}
} catch (SQLException e) {
e.printStackTrace();
}
return list;
}
}
这里用到了 try-with-resources 语法,也就是 try(...) 的写法,它的好处是代码块结束后自动调用 close(),不需要你在 finally 里手动关了。不过注意,ResultSet 在 PreparedStatement 关闭后也会自动关闭,所以把 rs 也放在 try 后面的括号里没有坏处。
我特别想说的一个细节是 rs.getLong("id") 这种按列名取值的写法,比 rs.getLong(1) 这种按索引取值可读性好很多。一旦 SQL 字段顺序调整,索引取值会静默出错,而列名取值更安全,可维护性也更好。
3.4 事务与批处理
"数据交互"如果只有单表增删改查,肯定不够。实际业务里事务是绕不开的。我用一个经典的转账场景来说明 JDBC 怎么处理事务。
假设有两个账户,A 要给 B 转账 100 元,那数据库层面要做两次更新:A 扣 100,B 加 100。如果 A 扣款成功但 B 加款失败,这笔钱就凭空消失了。事务就是保证这两条 SQL 要么都成功,要么都失败。
java复制public void transfer(Long fromId, Long toId, BigDecimal amount) {
String sqlFrom = "UPDATE account SET balance = balance - ? WHERE id = ?";
String sqlTo = "UPDATE account SET balance = balance + ? WHERE id = ?";
try (Connection conn = DBUtil.getConnection()) {
conn.setAutoCommit(false);
try (PreparedStatement psFrom = conn.prepareStatement(sqlFrom);
PreparedStatement psTo = conn.prepareStatement(sqlTo)) {
psFrom.setBigDecimal(1, amount);
psFrom.setLong(2, fromId);
psFrom.executeUpdate();
// 模拟异常,比如账户不存在时抛出
if (toId == null) {
throw new SQLException("转入账户不存在,事务回滚");
}
psTo.setBigDecimal(1, amount);
psTo.setLong(2, toId);
psTo.executeUpdate();
conn.commit();
} catch (SQLException e) {
conn.rollback();
throw e;
}
} catch (SQLException e) {
e.printStackTrace();
}
}
这里的关键是 conn.setAutoCommit(false),它告诉 MySQL:先不要自动提交,等我把两条 SQL 都执行完,再收到 conn.commit() 才真正落盘。一旦中间抛出异常,就 conn.rollback() 回滚,让之前执行过的 SQL 全部作废。注意 rollback() 必须放在新的 try-catch 里,因为如果事务已经提交,rollback() 会抛异常。
另一个常用的数据交互技巧是批量插入。如果你要往数据库里插几千条数据,一条一条 executeUpdate 性能极差。用 addBatch 和 executeBatch 可以显著提升速度:
java复制public void batchInsert(List<User> users) {
String sql = "INSERT INTO user(username, email, age) VALUES(?, ?, ?)";
try (Connection conn = DBUtil.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
conn.setAutoCommit(false);
for (User user : users) {
ps.setString(1, user.getUsername());
ps.setString(2, user.getEmail());
ps.setInt(3, user.getAge());
ps.addBatch();
}
ps.executeBatch();
conn.commit();
} catch (SQLException e) {
e.printStackTrace();
}
}
实测下来,批量插入 1 万条数据的耗时通常比逐条插入快一个数量级,原因是减少了客户端和服务端的网络往返次数。这也是为什么在数据初始化、数据迁移场景里,批量处理几乎是标配。
4. 常见问题与排查技巧实录
4.1 错误速查表
下面这些报错,是我在教同事和带实习生时出现频率最高的,也是网上搜得最多的。我整理成一张速查表,你可以直接按图索骥。
| 报错信息 | 常见原因 | 解决办法 |
|---|---|---|
| ClassNotFoundException: com.mysql.cj.jdbc.Driver | JDBC 驱动 jar 没有引入 | 把 mysql-connector-j 的 jar 包放到项目依赖里 |
| Access denied for user 'root'@'localhost' | 用户名或密码错误 | 检查 DBUtil 里的 USER 和 PASSWORD |
| Unknown database 'blog_demo' | 数据库没有创建 | 先执行建库 SQL |
| Communications link failure | 数据库没启动,或端口不对 | 确认 MySQL 服务已启动,端口是 3306 |
| Public Key Retrieval is not allowed | MySQL 8 的 caching_sha2_password 认证问题 | URL 加 allowPublicKeyRetrieval=true |
| The server time zone value is unrecognized | MySQL 8 驱动要求设置时区 | URL 加 serverTimezone=Asia/Shanghai |
| Column 'xxx' cannot be null | 非空字段写入 null | 检查插入数据,或调整表结构 |
| Duplicate entry 'xxx' for key 'username' | 唯一键冲突 | 先查重,或改用 update 逻辑 |
这张表覆盖了新手 90% 的报错。遇到问题别急着复制整个错误去问别人,先看是什么类型的异常——ClassNotFoundException 是类加载问题,SQLException 才是 SQL 执行问题,方向对了排查就快。
4.2 踩坑实录
除了上面的速查表,我再分享几个实际踩过的坑,这些坑在文档里很难找到明确说明。
第一个坑是时区问题在 MySQL 8 下的隐蔽表现。我之前帮人看一个项目,连接数据库偶尔报 The server time zone value,偶尔又正常,排查了很久才发现是系统环境变量里的 TZ 没设统一。解决办法就是 URL 里写成固定 serverTimezone=Asia/Shanghai,不要依赖系统默认。
第二个坑是 getConnection() 和 close() 的顺序。如果你用 try-with-resources,里面的资源关闭顺序和声明顺序是相反的,也就是说后声明的先关闭。在你同时使用了 Connection、PreparedStatement、ResultSet 时,一定要保证 Connection 最后关闭,否则 ResultSet 还没来得及读取数据,连接就断了。用 try-with-resources 时,把 Connection 写在第一个,就能保证它最后被关闭。
第三个坑是中文乱码,这其实是两段问题。一个是数据库表字符集不是 utf8mb4,写入时就变成了问号;另一个是连接 URL 没加 characterEncoding=utf8。要一次性排查完,不能只改一边。判断方法很简单:命令行里直接 SELECT 一下这条数据,如果命令行显示中文正常,那就是连接配置的问题;如果命令行本身就是问号,那就是建表字符集的问题。
第四个坑是连接泄漏。很多初学者写代码时 conn 获取了没有关闭,开发环境数据量小感觉不出来,一旦跑线上,连接池里的连接被占满,系统就卡死了。我在 DBUtil 里用 close(AutoCloseable...) 的原因就在这里,宁可多关也不能不关。如果你用的是连接池(比如 HikariCP),泄漏连接会更隐蔽,因为它显示的池状态一直是活跃连接数上涨,很难定位到具体是哪个方法没释放。
5. 从控制台到图形界面与工程化
5.1 用 Swing 做一个简单的数据交互界面
很多课程设计都要求 "数据库管理系统 + 图形界面",所以我简单演示一下怎么把上面写的 UserDao 接到一个 Swing 界面上。完整代码量较大,我挑核心逻辑说明。
界面上放一个输入框、一个按钮和一个表格区域。点击"查询"按钮时,调用 UserDao.findAll() 拿到 List,然后填充到 JTable 的 TableModel 里。
java复制import javax.swing.*;
import javax.swing.table.DefaultTableModel;
import java.awt.*;
import java.util.List;
public class UserFrame extends JFrame {
private UserDao userDao = new UserDao();
private JTable table;
private DefaultTableModel tableModel;
public UserFrame() {
setTitle("用户数据管理");
setSize(600, 400);
setDefaultCloseOperation(EXIT_ON_CLOSE);
tableModel = new DefaultTableModel(new String[]{"ID", "用户名", "邮箱", "年龄"}, 0);
table = new JTable(tableModel);
add(new JScrollPane(table), BorderLayout.CENTER);
JButton refreshBtn = new JButton("刷新数据");
refreshBtn.addActionListener(e -> refreshData());
add(refreshBtn, BorderLayout.SOUTH);
refreshData();
}
private void refreshData() {
tableModel.setRowCount(0);
List<User> users = userDao.findAll();
for (User user : users) {
tableModel.addRow(new Object[]{
user.getId(), user.getUsername(), user.getEmail(), user.getAge()
});
}
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> new UserFrame().setVisible(true));
}
}
这里有一个细节,就是界面刷新一定要在 SwingUtilities.invokeLater 里做,因为 Swing 的组件不是线程安全的。如果之后你加了耗时查询,直接在事件线程里跑会导致界面卡死,更优雅的做法是配合 SwingWorker 后台执行。不过课程设计阶段,先跑通核心逻辑更重要。
5.2 从手写连接池到 HikariCP
如果你用 JDBC 做数据交互,到了并发量稍微上来一点,就会发现每次 DriverManager.getConnection() 的耗时非常可观。MySQL 建立连接本身就是一个重量级操作,需要 TCP 握手、认证、会话初始化,一次可能就要几十毫秒甚至上百毫秒。如果每个请求都重新建连接,系统的吞吐量会非常难看。
实际项目中,推荐直接用 HikariCP 这种成熟连接池。它可以理解成一个"保存连接的容器",用的时候租出去,用完还回来,避免重复建立和销毁的开销。引入 HikariCP 后,把 DBUtil 改造成从连接池获取连接即可。
java复制import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
public final class DataSourceHolder {
private static final HikariDataSource dataSource;
static {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/blog_demo"
+ "?useUnicode=true&characterEncoding=utf8"
+ "&useSSL=false&serverTimezone=Asia/Shanghai");
config.setUsername("root");
config.setPassword("你的密码");
config.setDriverClassName("com.mysql.cj.jdbc.Driver");
config.setMaximumPoolSize(10);
config.setMinimumIdle(2);
config.setConnectionTimeout(30000);
config.addDataSourceProperty("cachePrepStmts", "true");
config.addDataSourceProperty("prepStmtCacheSize", "250");
config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
dataSource = new HikariDataSource(config);
}
public static Connection getConnection() throws Exception {
return dataSource.getConnection();
}
}
这样改造之后,原有 UserDao 里的 DBUtil.getConnection() 只需要替换为 DataSourceHolder.getConnection(),其他代码完全不用动。这就是封装的好处——接口没变,底层实现随便换。这也是为什么我建议一开始就要写好 getConnection() 的封装方法,而不是把 DriverManager 散落在每个 DAO 方法里。
6. 补充经验:多表关联查询的 JDBC 写法
有些课程设计和实际项目还涉及多表查询,这里我补充一个常见场景:用户表和订单表关联查询。假设你要查询每个用户下过多少单,SQL 可以写成:
sql复制SELECT u.id, u.username, COUNT(o.id) AS order_count
FROM user u
LEFT JOIN `order` o ON o.user_id = u.id
GROUP BY u.id, u.username;
在 JDBC 里执行这种查询,逻辑和单表查询几乎完全一样,只是 ResultSet 要多取几个字段:
java复制public List<Map<String, Object>> findUserOrderCount() {
String sql = "SELECT u.id, u.username, COUNT(o.id) AS order_count "
+ "FROM user u LEFT JOIN `order` o ON o.user_id = u.id "
+ "GROUP BY u.id, u.username";
List<Map<String, Object>> list = new ArrayList<>();
try (Connection conn = DBUtil.getConnection();
PreparedStatement ps = conn.prepareStatement(sql);
ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
Map<String, Object> row = new HashMap<>();
row.put("id", rs.getLong("id"));
row.put("username", rs.getString("username"));
row.put("orderCount", rs.getInt("order_count"));
list.add(row);
}
} catch (SQLException e) {
e.printStackTrace();
}
return list;
}
这里返回 Map 而非 User,是因为查询结果已经不匹配单表实体类了。如果这种查询很多,就可以考虑引入 MyBatis 或者自己封装一个 RowMapper 接口来抽象行的映射逻辑。但在学习阶段,先体会最纯正的 ResultSet 操作,会让你对整个链路有更深的肌肉记忆。
我个人在带新人时总会说一句话:先把 JDBC 写吐,再谈框架优化。数据库访问是所有业务系统的地基,地基没打好,楼上盖得越高越危险。这篇内容覆盖了从环境配置、连接参数、增删改查到事务、批处理、常见报错排查,还有工程化方向上的连接池使用建议。你可以在本地照着敲一遍,然后尝试把用户表改成自己项目里的业务表,把插入和查询的 SQL 换成自己的需求,跑通一遍才算真正掌握。等这步完成了,再去学习 MyBatis,你会发现框架的很多概念轻轻一点就通了。
