1. 课程导入与工程环境搭建
在正式进入 Java 数据库访问与 ORM 工程的学习之前,需要先建立一张清晰的技术全景图:Java 程序究竟通过哪些层次与数据库通信,JDBC 与 ORM 各自处于什么位置,它们之间是替代关系还是协作关系。只有把这条链路看清楚,后续学习连接池、MyBatis、JPA、事务管理时,才不会把不同层次的职责混在一起。
一、Java 数据库访问技术全景
Java 访问关系型数据库,本质上要解决四件事:建立连接、发送 SQL、处理结果、管理资源。围绕这四件事,技术栈从底层到高层大致分为四层。
最底层是数据库驱动,即各数据库厂商提供的 JDBC Driver,例如 MySQL 的 mysql-connector-j、PostgreSQL 的 postgresql。驱动负责把 Java 调用翻译成数据库能识别的网络协议。再往上是JDBC API,它由 JDK 提供(java.sql 包),定义了 Connection、Statement、PreparedStatement、ResultSet 等接口,是 Java 官方标准。第三层是连接池与 JDBC 工具,如 HikariCP、Druid、Apache Commons DbUtils,解决频繁创建连接的开销和重复代码问题。最上层是ORM 框架,如 MyBatis、Hibernate、Spring Data JPA,把 SQL 或对象映射关系进一步封装,让开发者以对象方式操作数据。
需要特别强调的是:ORM 并没有取代 JDBC。无论 MyBatis 还是 Hibernate,最终都通过 JDBC 与数据库通信。ORM 是在 JDBC 之上的一层抽象,理解 JDBC 是理解 ORM 行为的基础。例如 MyBatis 的 SqlSession 底层仍然持有 JDBC 的 Connection,事务提交最终仍调用 Connection.commit()。
二、JDBC 与 ORM 的关系
可以用一句话概括:JDBC 是标准与通道,ORM 是封装与便利。JDBC 提供的是“能力”,ORM 提供的是“效率”。
使用原生 JDBC 查询一张用户表,需要手动加载驱动、获取连接、创建 PreparedStatement、绑定参数、遍历 ResultSet、逐列取值并封装成对象,最后在 finally 中关闭资源。代码冗长且容易遗漏关闭。而使用 MyBatis,只需定义接口方法和映射 SQL,框架自动完成参数绑定与结果映射;使用 JPA,甚至可以直接调用 repository.findById(id)。
但便利是有代价的:ORM 隐藏了 SQL 细节,遇到慢查询、复杂联表、批量操作时,如果开发者不理解底层 JDBC 行为,就很难定位问题。因此本课程的学习路径是:先掌握 JDBC 的完整生命周期,再理解连接池如何优化它,最后学习 ORM 如何在它之上构建抽象。
三、开发环境准备
本课程统一使用以下环境,建议版本尽量一致,避免因版本差异导致驱动类名、时区、SSL 参数等细节问题。
| 组件 | 推荐版本 | 作用 |
|---|---|---|
| JDK | 17(LTS) | 编译运行 Java 代码,提供 java.sql 包 |
| Maven | 3.9.x | 依赖管理与构建,引入 JDBC 驱动 |
| IDE | IntelliJ IDEA 2023+ | 编码、调试、数据库工具窗口 |
| MySQL | 8.0.x | 示例数据库,默认端口 3306 |
| JDBC 驱动 | mysql-connector-j 8.3.0 | MySQL 的 JDBC 实现 |
在 Maven 项目的 pom.xml 中加入驱动依赖:
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.3.0</version>
</dependency>
数据库侧先准备一个示例库和表。以下 SQL 属于教学示例,可在本地 MySQL 中执行:
CREATE DATABASE IF NOT EXISTS course_db DEFAULT CHARSET utf8mb4;
USE course_db;
CREATE TABLE t_user (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL,
email VARCHAR(100),
age INT
);
INSERT INTO t_user (username, email, age) VALUES
('alice', 'alice@example.com', 24),
('bob', 'bob@example.com', 31);
com.mysql.cj.jdbc.Driver,连接 URL 建议显式指定时区与字符集,例如 jdbc:mysql://localhost:3306/course_db?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useSSL=false,否则可能出现时区报错或中文乱码。四、第一个 JDBC 连接程序
下面这段代码完成一次完整的 JDBC 查询:加载驱动、建立连接、执行查询、读取结果、关闭资源。请重点观察每一步的对象类型和资源释放顺序。
import java.sql.*;
public class FirstJdbcDemo {
private static final String URL =
"jdbc:mysql://localhost:3306/course_db?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useSSL=false";
private static final String USER = "root";
private static final String PASSWORD = "123456";
public static void main(String[] args) {
String sql = "SELECT id, username, email, age FROM t_user WHERE age > ?";
try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setInt(1, 20);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
long id = rs.getLong("id");
String name = rs.getString("username");
String email = rs.getString("email");
int age = rs.getInt("age");
System.out.printf("id=%d, username=%s, email=%s, age=%d%n",
id, name, email, age);
}
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
运行后,控制台应输出两行记录,分别对应 alice 和 bob。判断程序是否成功,可以看三个信号:没有抛出 SQLException;输出行数与表中满足 age > 20 的记录数一致;字段值与数据库中的实际数据吻合。
这段代码中有几个关键点值得注意。第一,DriverManager.getConnection 会自动通过 SPI 机制加载驱动,因此现代 JDBC 4.0 之后通常不需要显式写 Class.forName("com.mysql.cj.jdbc.Driver"),但理解这一步仍然重要。第二,使用 PreparedStatement 并通过 setInt 绑定参数,而不是拼接字符串,可以避免 SQL 注入。第三,try-with-resources 保证 Connection、PreparedStatement、ResultSet 按逆序自动关闭,这是 JDBC 资源管理的最佳实践。
Access denied 通常是账号密码问题,Communications link failure 通常是服务未启动或端口不通,Unknown database 则是库名写错。到这里,你已经完成了从环境搭建到第一条 JDBC 查询的完整闭环。这条链路虽然简单,却是后续所有数据库访问技术的共同基础:连接池优化的是 Connection 的创建,ORM 封装的是 PreparedStatement 与 ResultSet 的处理,事务管理控制的则是 Connection 的提交与回滚。理解这一层,后面的学习就有了稳固的支点。