JDBC(Java Database Connectivity)

🔍 一、为什么必须学 JDBC?------不是"过时",而是"根基"

在 Spring Boot 时代,我们用 `@Mapper`、`JdbcTemplate`、`MyBatis-Plus` 三行代码就完成增删改查。那为什么还要啃 JDBC?

> 💡 答案很朴素:它是所有 Java 数据库框架的底层基石!

> - MyBatis 的 `SqlSession` 是对 `Connection + PreparedStatement` 的封装;

> - Spring 的 `JdbcTemplate` 是对 `PreparedStatement` 和 `ResultSet` 的模板化抽象;

> - 连接池(Druid/Hikari)、事务管理器(`DataSourceTransactionManager`)全基于 JDBC 接口构建。

📌 一句话总结:

> ❗️不学 JDBC,永远只能调 API;学透 JDBC,才能真正看懂框架、写出高性能 SQL、解决事务死锁、排查连接泄漏!

📘 二、JDBC 是什么?------一套标准,不是某个工具!

✅ 官方定义

JDBC(Java Database Connectivity)是由 Sun 公司制定的一套访问关系型数据库的标准接口(API 规范),属于 Java SE 的一部分。

角色 说明
JDBC(接口) java.sql.* 包下的 Connection, Statement, ResultSet 等------你写的代码依赖它
JDBC Driver(驱动实现) MySQL 的 com.mysql.cj.jdbc.Driver、Oracle 的 oracle.jdbc.driver.OracleDriver ------厂商提供,你引入 jar 包

> ⚠️ 类比理解:

> JDBC = USB 接口标准(Type-C / USB-A)

> 驱动 = 手机厂商(华为/小米)为自家手机生产的 Type-C 数据线

> ❌ 没有驱动 → 插上 USB 线也传不了数据!

🧩 三、JDBC 核心架构图解(4 大核心接口)

复制代码
graph LR
A[DriverManager] -->|获取| B[Connection]
B -->|创建| C[Statement]
B -->|预编译| D[PreparedStatement]
B -->|调用存储过程| E[CallableStatement]
C & D & E -->|执行| F[ResultSet]
接口 职责 关键方法 使用场景
DriverManager 驱动注册中心 & 连接工厂 getConnection(url, user, pwd) 加载驱动、获取连接(只用一次
Connection 物理数据库会话 createStatement(), prepareStatement(), setAutoCommit(false) 控制事务、创建执行器
Statement 普通 SQL 执行器 executeQuery(), executeUpdate() 简单、无参数、一次性 SQL(不推荐生产使用
PreparedStatement 预编译 SQL 执行器 setString(1,"xxx"), executeQuery() ✅ 防 SQL 注入、✅ 性能高(缓存执行计划)、✅ 支持批量操作
ResultSet 查询结果游标 next(), getString("name"), getInt(1) 遍历、取值、关闭(必须 close!

> ✅ 最佳实践口诀:

> "先连库,再预编,设参查,游标遍,最后关!"

> (Connection → PreparedStatement → setXxx → executeQuery → next + getXxx → close)

🛠 四、JDBC 操作六步法(含完整代码)

> ✅ 务必按顺序执行,资源关闭顺序必须「反向」!

> `ResultSet` → `Statement` → `Connection`(否则可能报 `Closed Connection` 异常)

✅ Step 1:加载驱动(MySQL 8+)

复制代码
// ✅ MySQL 8.x 推荐写法(自动注册,可省略)
Class.forName("com.mysql.cj.jdbc.Driver");

// ⚠️ MySQL 5.x 写法(已过时,仅兼容)
// Class.forName("com.mysql.jdbc.Driver");

✅ Step 2:获取连接(URL 参数很重要!)

复制代码
String url = "jdbc:mysql://localhost:3306/jdbc_db?" +
        "useSSL=false&" +           // 关闭 SSL(本地开发)
        "serverTimezone=Asia/Shanghai&" + // 时区(避免时间错乱)
        "characterEncoding=utf8";   // 字符集(防中文乱码)

Connection conn = DriverManager.getConnection(url, "root", "1111");

✅ Step 3:创建 PreparedStatement(强烈推荐!)

复制代码
String sql = "INSERT INTO student (id, name, age, sex, address) VALUES (?, ?, ?, ?, ?)";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, 101);
pstmt.setString(2, "张三");
pstmt.setInt(3, 25);
pstmt.setString(4, "男");
pstmt.setString(5, "北京市朝阳区");

✅ Step 4:执行操作

操作类型 方法 返回值 示例
查询(SELECT) pstmt.executeQuery() ResultSet rs = pstmt.executeQuery();
增/删/改(DML) pstmt.executeUpdate() int(影响行数) int rows = pstmt.executeUpdate();

✅ Step 5:处理 ResultSet(注意游标初始位置!)

复制代码
ResultSet rs = pstmt.executeQuery();
// ⚠️ 游标初始在第 0 行(第一行前),必须 next() 才能取值!
while (rs.next()) {
    int id = rs.getInt("id");          // ✅ 推荐:用列名(更可读)
    String name = rs.getString("name");
    // 或用索引(从 1 开始!)→ rs.getString(2)
}

✅ Step 6:关闭资源(必须 try-finally 或 try-with-resources!)

复制代码
finally {
    if (rs != null) rs.close();
    if (pstmt != null) pstmt.close();
    if (conn != null) conn.close(); // 归还连接池(若使用)
}

> 💡 进阶技巧:使用 try-with-resources(JDK 7+)自动关闭

复制代码
try (Connection conn = DBUtils.getConn();
     PreparedStatement pstmt = conn.prepareStatement(sql);
     ResultSet rs = pstmt.executeQuery()) {
    
    while (rs.next()) { /* 处理结果 */ }
} catch (SQLException e) {
    e.printStackTrace();
} // 自动 close()!

🚫 五、致命风险:SQL 注入攻击与防御(必考!)

❌ 危险写法(字符串拼接)→ 账户密码全裸奔!

复制代码
// 用户输入:username = "admin' -- ",password = "123"
String sql = "SELECT * FROM sys_user WHERE username='" + username + "' AND password='" + password + "'";
// 实际执行:
// SELECT * FROM sys_user WHERE username='admin' -- ' AND password='123'
// → 注释掉密码校验!直接登录成功!

✅ 正确方案:PreparedStatement 占位符(唯一可靠方案!)

复制代码
String sql = "SELECT * FROM sys_user WHERE username = ? AND password = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, username); // ✅ 参数化,数据库当「值」而非「SQL 片段」
pstmt.setString(2, password);
ResultSet rs = pstmt.executeQuery();

> ✅ 原理:PreparedStatement 将 SQL 与参数分离,数据库先编译 SQL 模板,再安全绑定参数值,彻底杜绝注入!

💰 六、事务控制:转账案例(ACID 四大特性落地)

📌 需求

A 向 B 转账 1000 元,要求:

✅ A 扣款成功 & B 加款成功 → 全部提交

❌ A 扣款成功 & B 加款失败 → 全部回滚(钱不能丢!)

✅ 核心代码(手动事务)

复制代码
Connection conn = null;
try {
    conn = DBUtils.getConn();
    conn.setAutoCommit(false); // 🔑 关闭自动提交,开启事务

    String sql1 = "UPDATE account SET amount = amount - 1000 WHERE aid = 1";
    String sql2 = "UPDATE account SET amount = amount + 1000 WHERE aid = 2";

    PreparedStatement p1 = conn.prepareStatement(sql1);
    PreparedStatement p2 = conn.prepareStatement(sql2);

    p1.executeUpdate(); // 扣款
    // int i = 1 / 0; // ✅ 模拟异常:此处抛异常则触发回滚
    p2.executeUpdate(); // 加款

    conn.commit(); // ✅ 全部成功,提交事务
    System.out.println("转账成功!");
} catch (Exception e) {
    e.printStackTrace();
    try {
        if (conn != null) conn.rollback(); // 🔑 回滚!
        System.out.println("转账失败,已回滚!");
    } catch (SQLException ex) {
        ex.printStackTrace();
    }
} finally {
    DBUtils.close(conn);
}

✅ ACID 特性对应实现:

特性 JDBC 实现方式
原子性(Atomicity) commit() / rollback() 控制整体成败
一致性(Consistency) 事务内约束(如外键、唯一键)自动校验
隔离性(Isolation) conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE)
持久性(Durability) 提交后数据写入磁盘(数据库保证)

🐬 七、性能瓶颈:为什么需要数据库连接池?

❌ 传统模式痛点

复制代码
每次请求 → 创建 Connection(TCP 握手 + 认证)→ 执行 SQL → close()(四次挥手)  
→ 高并发下:连接创建/销毁耗时 > SQL 执行耗时 → 系统雪崩!

✅ 连接池原理(复用连接)

复制代码
graph LR
A[应用] -->|借| B[连接池]
B --> C[空闲连接1]
B --> D[空闲连接2]
B --> E[空闲连接3]
C & D & E -->|归还| B
方案 特点 现状
c3p0 / DBCP 老牌开源池,配置复杂 ❌ 已基本淘汰
Druid(阿里) 监控强大(Web 控制台)、中文文档好、Spring Boot 默认集成 国内主流选择
HikariCP(日本) 性能最强(号称"光速")、Spring Boot 2.0+ 官方默认 国际主流 & 推荐首选

🧱 八、企业级封装:BaseDAO ------ 手写你的第一个"轻量 ORM"

> 💡 目标:用一个 `BaseDao<T>`,支持任意实体类的通用增删改查 + 分页,零 SQL 硬编码!

✅ Step 1:定义通用分页对象 `PageInfo<T>`

复制代码
public class PageInfo<T> {
    private List<T> data;   // 当前页数据
    private Long total;     // 总条数
    private Integer pageNum; // 当前页码
    private Integer pageSize;// 每页条数

    // getter/setter...
}

✅ Step 2:BaseDao 核心封装(反射 + 泛型 + PreparedStatement)

复制代码
public class BaseDao {

    // ✅ 通用查询列表(支持 ? 占位符)
    public <T> List<T> selectList(String sql, Class<T> cls, Object... params) {
        List<T> list = new ArrayList<>();
        try (Connection conn = DBUtils.getConn();
             PreparedStatement ps = conn.prepareStatement(sql)) {

            // 设置参数
            for (int i = 0; i < params.length; i++) {
                ps.setObject(i + 1, params[i]);
            }

            try (ResultSet rs = ps.executeQuery()) {
                ResultSetMetaData meta = rs.getMetaData();
                int columnCount = meta.getColumnCount();

                while (rs.next()) {
                    T obj = cls.getDeclaredConstructor().newInstance();
                    for (int i = 1; i <= columnCount; i++) {
                        String colName = meta.getColumnLabel(i); // 获取列别名(如 createTime)
                        Object value = rs.getObject(colName);
                        Field field = cls.getDeclaredField(colName);
                        field.setAccessible(true);
                        field.set(obj, value);
                    }
                    list.add(obj);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return list;
    }

    // ✅ 通用分页查询(自动计算 count + limit)
    public <T> PageInfo<T> selectPage(String sql, Class<T> cls, int pageNum, int pageSize) {
        PageInfo<T> page = new PageInfo<>();

        // 1. 查询当前页数据
        String pageSql = sql + " LIMIT " + (pageNum - 1) * pageSize + "," + pageSize;
        List<T> dataList = selectList(pageSql, cls);

        // 2. 查询总条数(子查询包装)
        String countSql = "SELECT COUNT(*) FROM (" + sql + ") AS tmp";
        long total = 0;
        try (Connection conn = DBUtils.getConn();
             PreparedStatement ps = conn.prepareStatement(countSql);
             ResultSet rs = ps.executeQuery()) {
            if (rs.next()) total = rs.getLong(1);
        } catch (Exception e) {
            e.printStackTrace();
        }

        page.setData(dataList);
        page.setTotal(total);
        page.setPageNum(pageNum);
        page.setPageSize(pageSize);
        return page;
    }

    // ✅ 通用更新(INSERT/UPDATE/DELETE)
    public boolean update(String sql, Object... params) {
        try (Connection conn = DBUtils.getConn();
             PreparedStatement ps = conn.prepareStatement(sql)) {
            for (int i = 0; i < params.length; i++) {
                ps.setObject(i + 1, params[i]);
            }
            return ps.executeUpdate() > 0;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
}

✅ Step 3:业务 DAO 继承使用(极简!)

复制代码
public class UserDao extends BaseDao {
    // ✅ 不用写 SQL!复用父类方法
    public User findByUsernameAndPassword(String username, String password) {
        String sql = "SELECT * FROM sys_user WHERE username = ? AND password = ?";
        return selectOne(sql, User.class, username, password);
    }

    public void updateUserState(Long id, Integer deleted) {
        String sql = "UPDATE sys_user SET deleted = ?, deleted_time = NOW() WHERE id = ?";
        update(sql, deleted, id);
    }

    public PageInfo<User> findPage(int pageNum, int pageSize) {
        String sql = "SELECT * FROM sys_user WHERE deleted = 0";
        return selectPage(sql, User.class, pageNum, pageSize);
    }
}

> ✅ 优势总结:

> - ✅ 零 SQL 硬编码(SQL 写在方法里,非 XML)

> - ✅ 强类型安全(泛型 `<T>` 编译期检查)

> - ✅ 自动映射(字段名 ↔ 列名一致即可)

> - ✅ 开箱即用分页(count + limit 一键搞定)

> - ✅ 为后续接入 MyBatis 奠定认知基础

🌈 九、总结:JDBC 学习路线图(附学习建议)

阶段 掌握目标 推荐练习
入门 能手写 6 步完成 CRUD、理解 PreparedStatement 防注入 学生管理系统增删改查
进阶 掌握事务控制、连接池配置(Druid/Hikari)、批处理 addBatch() 银行转账 + 并发压力测试
高手 封装 BaseDAO、理解 JDBC 四大接口
相关推荐
2501_933923252 分钟前
Spring Bean作用域揭秘:单例、原型、请求与会话的区别
java·后端·spring·java-ee
wuyk55514 分钟前
【Socket 进阶之路】第 7 章 IO 多路复用 select & poll 完整实战|多 fd 监听、超时等待、优缺点横向对比
服务器·开发语言·网络·数据库·物联网
学编程就要猛19 分钟前
基于Spring AI 的智能聊天机器人
java·spring ai·chat robot
修炼室21 分钟前
Java开发工程师笔试经验贴【高频知识】
java
木井巳34 分钟前
【记忆化搜索】最长递增子序列
java·算法·leetcode·深度优先·剪枝·推荐算法
文人sec36 分钟前
grant 之后要跟着 flush privileges 吗?要不要使用分区表?
数据库·mysql·adb·数据分析
杨杨杨大侠41 分钟前
知识库已经有了,Java 程序员还要做什么?Spring AI RAG 实战
java·openai·ai编程
伊信1 小时前
Spring AI 基础学习与应用
java·后端
Omics Pro1 小时前
上海AI Lab孙思琦×高张阳:虚拟细胞代码库智能体
数据库·人工智能·算法·机器学习·自然语言处理
夜郎king1 小时前
基于 Java + Playwright 实现网站自动访问与数据采集(以腾讯云开发者社区为例)
java·playwright·网页数据获取