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()。

Java 数据库访问技术分层与调用链 应用代码 / 业务服务 调用 ORM 接口或直接使用 JDBC,表达“要什么数据” ORM 框架层:MyBatis / Hibernate / Spring Data JPA 对象-关系映射、SQL 生成或映射、一级/二级缓存、延迟加载 连接池与 JDBC 工具层:HikariCP / Druid / DbUtils 连接复用、超时控制、SQL 监控、结果集封装 JDBC API(java.sql):Connection / PreparedStatement / ResultSet JDK 标准接口,定义连接、执行 SQL、处理结果、事务的规范 JDBC Driver(厂商实现) mysql-connector-j / postgresql 关系型数据库 MySQL / PostgreSQL / Oracle 上层依赖下层,ORM 最终仍通过 JDBC 与数据库通信

二、JDBC 与 ORM 的关系

可以用一句话概括:JDBC 是标准与通道,ORM 是封装与便利。JDBC 提供的是“能力”,ORM 提供的是“效率”。

使用原生 JDBC 查询一张用户表,需要手动加载驱动、获取连接、创建 PreparedStatement、绑定参数、遍历 ResultSet、逐列取值并封装成对象,最后在 finally 中关闭资源。代码冗长且容易遗漏关闭。而使用 MyBatis,只需定义接口方法和映射 SQL,框架自动完成参数绑定与结果映射;使用 JPA,甚至可以直接调用 repository.findById(id)。

但便利是有代价的:ORM 隐藏了 SQL 细节,遇到慢查询、复杂联表、批量操作时,如果开发者不理解底层 JDBC 行为,就很难定位问题。因此本课程的学习路径是:先掌握 JDBC 的完整生命周期,再理解连接池如何优化它,最后学习 ORM 如何在它之上构建抽象。

三、开发环境准备

本课程统一使用以下环境,建议版本尽量一致,避免因版本差异导致驱动类名、时区、SSL 参数等细节问题。

组件推荐版本作用
JDK17(LTS)编译运行 Java 代码,提供 java.sql 包
Maven3.9.x依赖管理与构建,引入 JDBC 驱动
IDEIntelliJ IDEA 2023+编码、调试、数据库工具窗口
MySQL8.0.x示例数据库,默认端口 3306
JDBC 驱动mysql-connector-j 8.3.0MySQL 的 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);
MySQL 8 的驱动类名为 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 资源管理的最佳实践。

如果连接失败,优先检查四件事:MySQL 服务是否启动、端口是否为 3306、用户名密码是否正确、驱动依赖是否已下载到本地仓库。报错信息中 Access denied 通常是账号密码问题,Communications link failure 通常是服务未启动或端口不通,Unknown database 则是库名写错。

到这里,你已经完成了从环境搭建到第一条 JDBC 查询的完整闭环。这条链路虽然简单,却是后续所有数据库访问技术的共同基础:连接池优化的是 Connection 的创建,ORM 封装的是 PreparedStatement 与 ResultSet 的处理,事务管理控制的则是 Connection 的提交与回滚。理解这一层,后面的学习就有了稳固的支点。