Skip to content

S09-07 MySQL-JDBC ​

[TOC]

体系概述 ​

JDBC(Java Database Connectivity) 是 Java 平台连接并操作关系型数据库的官方规范。它的核心目标是提供一套与具体数据库无关的标准 API,使 Java 开发者可以使用统一的代码逻辑访问不同的数据库系统。

设计背景 ​

在统一规范出现之前,每种数据库系统(如 Oracle、MySQL、SQL Server)都拥有专属的通信协议与客户端 API。如果应用程序需要更换数据库,通常需要重构全部底层数据交互代码。

JDBC 采用了面向接口编程与桥接模式的思想:

  • 解耦契约与实现:Java 官方仅负责制定接口标准规范,并不提供针对具体数据库的网络通信实现。
  • 厂商提供驱动:各数据库厂商根据自身专有协议,实现这套标准接口并打包为驱动 jar 包。
  • 统一接入体验:应用程序只需面向标准接口编程,更换底层数据库时通常仅需替换驱动依赖与连接配置。

架构分层 ​

JDBC 体系划分为清晰的两层架构,位于应用程序与物理数据库之间。

  • 面向开发者的应用接口层(JDBC API):提供给应用程序调用的标准接口集合(如 java.sql 与 javax.sql),负责表达数据访问意图。
  • 驱动调度中枢(DriverManager):负责管理各个厂商注册进来的驱动程序,根据连接字符串(JDBC URL)将请求分发给匹配的驱动。
  • 面向厂商的驱动接口层(JDBC Driver API):厂商驱动程序实现的协议适配层,负责把标准的 JDBC 调用翻译成数据库底层的网络通信报文。

驱动类型 ​

根据实现方式与底层依赖,JDBC 规范定义了四类驱动程序:

驱动分类实现机制优势劣势现状
Type 1: JDBC-ODBC 桥接器通过 ODBC 驱动转发调用初期过渡方案,可复用现有 ODBC 驱动性能低、依赖客户端本地环境配置Java 8 起已彻底移除
Type 2: 本地 API 驱动部分 Java 代码 + 本地 C/C++ 客户端库性能优于 Type 1客户端必须安装对应数据库的本地客户端库已基本淘汰
Type 3: 网络协议驱动纯 Java 编写,连接通用的中间件服务器客户端轻量,中间件可集中做安全与负载均衡增加了额外的网络传输节点仅见于特定企业级中间件
Type 4: 纯 Java 本地协议驱动纯 Java 编写,直接利用 Socket 与数据库通信部署简单、无本地依赖、性能最高需要针对不同数据库协议单独提供实现当今绝对主流(如 MySQL Connector/J)

SPI 机制 ​

早期版本的 JDBC(3.0 及以前)需要开发者在代码中显式加载驱动类,以触发其静态代码块向 DriverManager 注册自身:

java
// 1. 传统方式:显式加载类并触发驱动注册
Class.forName("com.mysql.cj.jdbc.Driver");

// 2. 根据 JDBC URL 前缀匹配驱动并创建连接
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/demo_db", "root", "123456");

从 JDBC 4.0 开始,引入了 Java SPI(Service Provider Interface)服务发现机制:

  1. 数据库驱动 Jar 包内部包含 META-INF/services/java.sql.Driver 文本文件。

  2. 该文件中记录了该驱动实现类的全限定类名(例如 com.mysql.cj.jdbc.Driver)。

  3. 当调用 DriverManager 时,其内部利用 ServiceLoader 自动扫描类路径下的所有驱动并完成注册,无需再编写 Class.forName()。

核心组件交互 ​

JDBC 规范由几个核心组件协同运作,完成从初始化到执行 SQL 的完整闭环:

  1. 驱动加载与注册:通过 SPI 机制或静态代码块,将厂商的 Driver 实现类注册到全局的 DriverManager 中。

  2. 连接分发与建立:调用 DriverManager.getConnection(url, user, pwd) 时,管理器依次询问已注册的驱动能否识别该 URL(例如以 jdbc:mysql:// 开头),匹配成功后由该驱动创建底层的 Socket 会话并返回 Connection 对象。

  3. 语句执行载体构建:通过 Connection 工厂方法创建 Statement 或预编译执行器 PreparedStatement。

  4. 报文传输与结果集封装:执行器将 SQL 及其参数按数据库协议打包发送至服务端,并将返回的数据流封装为带游标指针的 ResultSet 提供给上层读取。

基础操作 ​

标准流程 ​

标准的 JDBC 执行操作遵循以下五个阶段:

  1. 注册驱动:在应用类路径中引入数据库驱动依赖(JDBC 4.0+ 依托 Java SPI 机制自动完成发现与注册)。

  2. 建立连接:通过连接字符串、用户名和密码向数据库发起 TCP 握手并创建会话。

  3. 创建执行器:基于当前连接创建用于发送 SQL 的语句对象。

  4. 执行语句:调用对应方法向数据库发送查询或更新指令。

  5. 处理结果并释放资源:提取数据并显式关闭占用底层套接字及游标的资源。

java
// 1. 使用 try-with-resources 自动管理资源释放
try (Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/demo", "root", "123456");
   Statement stmt = conn.createStatement();
   // 2. 执行静态 SQL 查询
   ResultSet rs = stmt.executeQuery("SELECT id, name FROM sys_user")) {
  while (rs.next()) {
    // 3. 按字段名称提取数据
    int id = rs.getInt("id");
    String name = rs.getString("name");
  }
} catch (SQLException e) {
  e.printStackTrace();
}

image-20260822102142592

连接获取 ​

DriverManager 负责解析 JDBC URL,并调度匹配的底层驱动程序建立网络物理连接。

JDBC URL 结构解析

  • jdbc::固定协议前缀。

  • mysql::子协议,指明目标数据库类型。

  • //localhost:3306/demo_db:数据库服务的主机地址、通信端口与具体库名。

  • ?useSSL=false&serverTimezone=UTC:连接参数,用于指定字符编码、时区以及加密选项。

    java
    // 1. 配置数据库连接参数
    String url = "jdbc:mysql://localhost:3306/demo_db?useSSL=false&serverTimezone=UTC";
    String user = "root";
    // 2. 通过 DriverManager 建立物理网络连接
    Connection conn = DriverManager.getConnection(url, user, "123456");

连接获取方式 ​

获取数据库连接(Connection)是进行所有 JDBC 操作的前提。在 Java 发展历程中,获取连接的方式经历了从“底层直连”到“统一管理”,再到“自动化发现”的五个演进阶段。

方式1:直接实例化 ​

直接创建特定数据库厂商提供的 Driver 实现类对象,并调用其底层的 connect() 方法建立物理网络连接。

  • 实现原理:绕过 DriverManager 调度中心,由开发者直接操作厂商驱动类。

  • 主要缺陷:代码中强硬编码了具体数据库的驱动实现类(如 com.mysql.cj.jdbc.Driver),导致代码在编译期与特定数据库高度耦合,无法灵活切换底层数据库。

    java
    // 1. 显式创建特定厂商的驱动实例
    Driver driver = new com.mysql.cj.jdbc.Driver();
    Properties info = new Properties();
    info.setProperty("user", "root");
    info.setProperty("password", "123456");
    // 2. 调用驱动底层 connect 方法建立连接
    Connection conn = driver.connect("jdbc:mysql://localhost:3306/demo_db", info);

方式2:反射实例化 ​

利用 Java 反射机制动态加载驱动类的字节码并创建实例,随后同样调用 connect() 方法。

  • 实现原理:通过传入全限定类名字符串动态生成 Driver 实例。

  • 优劣分析:解除了编译期的类依赖,类名可以通过外部配置文件动态传入;但仍需手动构建连接属性并直接调用 driver.connect(),未享受驱动管理器的路由能力。

    java
    // 1. 通过反射动态加载并实例化驱动类
    Class<?> clazz = Class.forName("com.mysql.cj.jdbc.Driver");
    Driver driver = (Driver) clazz.getDeclaredConstructor().newInstance();
    Properties info = new Properties();
    info.setProperty("user", "root");
    info.setProperty("password", "123456");
    // 2. 通过反射生成的实例建立网络连接
    Connection conn = driver.connect("jdbc:mysql://localhost:3306/demo_db", info);

方式3:显式注册 ​

先实例化驱动对象,再显式调用 DriverManager.registerDriver() 将其加入全局驱动列表,最后由 DriverManager 获取连接。

  • 实现原理:开始引入 DriverManager 集中调度驱动。

  • 主要缺陷:在执行 new com.mysql.cj.jdbc.Driver() 时,该驱动类的静态代码块已经自动向 DriverManager 注册了一次自身;外部再次显式调用 registerDriver 会导致驱动被重复注册两次,且依然存在编译期硬编码。

    java
    // 1. 显式向驱动管理器注册驱动实例(会导致重复注册)
    DriverManager.registerDriver(new com.mysql.cj.jdbc.Driver());
    String url = "jdbc:mysql://localhost:3306/demo_db";
    // 2. 通过驱动管理器统一获取连接
    Connection conn = DriverManager.getConnection(url, "root", "123456");

方式4:隐式注册 ​

仅通过 Class.forName() 触发驱动类的加载,利用其类内部的 static 静态代码块自动向 DriverManager 注册,随后统一由 DriverManager.getConnection() 获取连接。

  • 实现原理:各厂商驱动类内部均包含静态代码块(static { DriverManager.registerDriver(new Driver()); }),只要类被加载进 JVM 就会自动注册且仅注册一次。

  • 核心优势:彻底移除了对具体驱动类的代码强依赖,驱动类名与 URL 均可提取至外部 .properties 配置文件中,是 JDBC 3.0 时代最经典的规范写法。

    java
    // 1. 加载类触发静态代码块向 DriverManager 自动注册
    Class.forName("com.mysql.cj.jdbc.Driver");
    String url = "jdbc:mysql://localhost:3306/demo_db";
    // 2. 由 DriverManager 调度连接
    Connection conn = DriverManager.getConnection(url, "root", "123456");

源码分析:静态代码块触发:

调用 Class.forName(driverName) 时,底层默认传入 initialize = true,强制 JVM 执行该类的静态初始化。

在 MySQL 驱动类(com.mysql.cj.jdbc.Driver)的源码中,静态代码块完成了自身的实例化与注册:

java
// 1. JVM 类加载时自动执行静态初始化块
static {
  try {
    // 2. 实例化当前驱动并向驱动管理器注册
    java.sql.DriverManager.registerDriver(new Driver());
  } catch (SQLException E) {
    throw new RuntimeException("Can't register driver!");
  }
}
  • 执行时机:只要该类被加载进内存,静态块有且仅执行一次。
  • 解耦效果:业务代码无需在编译期 import 具体的驱动类,仅通过类名字符串即可完成驱动装载。

方式5:SPI 自动发现 ​

从 JDBC 4.0(Java 6)开始引入 Java SPI 服务发现机制,彻底省略了显式的类加载代码。

  • 实现原理:数据库驱动 Jar 包内置了 META-INF/services/java.sql.Driver 文件,里面记录了驱动的全限定类名。当首次调用 DriverManager 时,其内部的 ServiceLoader 会自动扫描类路径下的所有驱动并完成注册。

  • 核心优势:代码最精炼,无需编写任何驱动加载逻辑,只需在工程中引入对应依赖即可直接调用 getConnection,为现代开发标准写法。

    java
    // 1. 依托 JDBC 4.0+ SPI 服务发现机制自动加载驱动
    String url = "jdbc:mysql://localhost:3306/demo_db";
    // 2. 直接获取数据库连接,无须任何前置注册代码
    Connection conn = DriverManager.getConnection(url, "root", "123456");

演进对比 ​

获取方式注册机制编译期耦合度注册次数历史地位与评价
方式一:直接实例化手动调用 connect,不注册高(硬编码驱动类)0早期直连,不推荐
方式二:反射实例化手动调用 connect,不注册低(支持配置解耦)0解耦类依赖,但未用管理器
方式三:显式注册DriverManager.registerDriver高(硬编码驱动类)2(重复注册)存在冗余注册缺陷
方式四:隐式注册Class.forName 触发静态块低(完全解耦)1JDBC 3.0 经典标准
方式五:SPI 自动发现ServiceLoader 扫描驱动文件无需显式代码1JDBC 4.0+ 现行标准

查询操作 ​

数据查询使用 Statement.executeQuery() 方法,该方法向服务端发送 DQL(数据查询语言),并返回包装为游标机制的 ResultSet 结果集对象。

sql
SELECT id, username, email
  FROM sys_user
  WHERE status = 1
  ORDER BY id DESC;

结果集提取机制

  • rs.next():将游标从当前位置向下移动一行。初次调用时,游标从第一行之前移动到第一行;若存在有效记录则返回 true,遍历结束返回 false。

  • rs.getXxx(columnLabel):根据列名或列索引(从 1 开始)提取对应数据类型的字段值。

    java
    // 1. 创建用于发送 SQL 的执行器
    Statement stmt = conn.createStatement();
    String sql = "SELECT id, username FROM sys_user WHERE status = 1";
    // 2. 执行查询并返回包含游标的结果集
    ResultSet rs = stmt.executeQuery(sql);
    while (rs.next()) {
      // 3. 游标移动到当前行,按字段名读取数据
      int id = rs.getInt("id");
      String name = rs.getString("username");
    }

更新操作 ​

数据的增、删、改(DML)以及表结构变更(DDL)均使用 Statement.executeUpdate() 方法。

sql
UPDATE sys_user
  SET status = 0, updated_at = NOW()
  WHERE id = 1001;

返回值说明

  • 执行 DML(INSERT、UPDATE、DELETE)时,方法返回受影响的物理行数(int 类型)。

  • 执行 DDL(CREATE TABLE、DROP TABLE)时,方法固定返回 0。

    java
    // 1. 构建 DML 数据更新语句与执行器
    Statement stmt = conn.createStatement();
    String sql = "UPDATE sys_user SET status = 0 WHERE id = 1001";
    // 2. 执行更新并获取受影响的行数
    int affectedRows = stmt.executeUpdate(sql);

资源释放 ​

数据库连接、执行器和结果集均持有底层的 Socket 描述符和数据库服务端内存资源。若未及时释放,会导致连接泄露并耗尽连接数。

释放规则

  • 释放顺序遵循“后创建、先释放”的原则:ResultSet 先关闭,其次是 Statement,最后是 Connection。

  • 推荐使用 Java 7+ 提供的 try-with-resources 语法,所有实现 java.lang.AutoCloseable 接口的 JDBC 对象均会在代码块结束时自动触发逆序关闭。

    java
    // 1. 使用 try-with-resources 自动管理连接、执行器与结果集
    try (Connection conn = DriverManager.getConnection(url, user, "123456");
       Statement stmt = conn.createStatement();
       // 2. 结果集在代码块执行完毕后自动触发 close
       ResultSet rs = stmt.executeQuery(sql)) {
      while (rs.next()) {
        // 3. 提取结果数据
        String username = rs.getString("username");
      }
    } catch (SQLException e) {
      e.printStackTrace();
    }

Statement 体系 ​

Statement 是 JDBC 中用于向数据库发送静态 SQL 语句并返回执行结果的基础接口。它充当 Java 应用程序与数据库执行引擎之间的指令传输载体。

体系概述 ​

java.sql.Statement 位于 JDBC 核心接口继承体系的顶层,构成了所有 SQL 执行器的基础规范。

  • Statement:顶级接口,用于执行不带参数的静态 SQL 语句。
  • PreparedStatement:继承自 Statement,扩展了预编译与动态占位符(?)参数绑定能力。
  • CallableStatement:继承自 PreparedStatement,进一步扩展了对数据库存储过程和函数的调用支持。

创建与生命周期 ​

Statement 实例必须依赖已建立的 Connection 物理连接通过工厂方法创建。

创建形式

  • 默认创建:调用 conn.createStatement(),生成只读且仅支持向前遍历(TYPE_FORWARD_ONLY、CONCUR_READ_ONLY)的结果集执行器。
  • 定制创建:调用 conn.createStatement(resultSetType, resultSetConcurrency),指定游标滚动方式与并发更新模式。

生命周期管理

Statement 在其关联的 Connection 关闭时会自动关闭;当通过该 Statement 执行新的查询时,先前关联的 ResultSet 也会被隐式关闭。为防止游标泄露和句柄耗尽,必须使用 try-with-resources 显式管理其关闭。

java
// 1. 使用 try-with-resources 确保执行器与结果集正常释放
try (Statement stmt = conn.createStatement();
   ResultSet rs = stmt.executeQuery("SELECT id, username FROM sys_user")) {
  // 2. 遍历查询结果
  while (rs.next()) {
    int id = rs.getInt("id");
  }
}

核心执行方法 ​

Statement 提供了针对不同 SQL 类型的专用执行方法:

sql
SELECT id, username, role
  FROM sys_user
  WHERE is_active = 1
  ORDER BY created_at DESC;
  • executeQuery(String sql):专门用于执行 DQL(数据查询语句,如 SELECT),返回 ResultSet 结果集。若传入非查询 SQL 则会抛出 SQLException。

  • executeUpdate(String sql):用于执行 DML(INSERT、UPDATE、DELETE)或 DDL(CREATE、DROP)语句。执行 DML 返回受影响的物理行数(int);执行 DDL 固定返回 0。

  • execute(String sql):通用执行入口,用于执行返回多个结果集、结构未知或动态构建的复杂 SQL。返回 boolean 值(true 表示返回了结果集,false 表示返回了影响行数或无结果)。

  • executeLargeUpdate(String sql):JDBC 4.2+ 引入,专为大数据量操作设计,返回类型为 long,避免受影响行数超出 32 位整型上限。

    java
    // 1. 执行 DML 更新语句返回受影响行数
    int rows = stmt.executeUpdate("UPDATE sys_user SET status = 1 WHERE id = 10");
    
    // 2. 执行未知类型的 SQL 语句
    boolean hasResultSet = stmt.execute("SELECT id FROM sys_user");
    if (hasResultSet) {
      // 3. 获取返回的结果集
      ResultSet rs = stmt.getResultSet();
    }

批处理操作 ​

批处理机制允许将多条同构或异构的静态 SQL 语句暂存在客户端缓冲区中,最后一次性打包发送至服务端执行,从而减少网络 I/O 往返次数。

操作步骤

  1. 调用 stmt.addBatch(sql) 将静态 SQL 加入待执行列表。

  2. 调用 stmt.executeBatch() 发送批处理请求,服务端返回每个语句受影响行数组成的整型数组(int[])。

  3. 调用 stmt.clearBatch() 清空当前缓冲区中未执行或已执行的 SQL。

    java
    // 1. 连续添加多条无参数依赖的静态 SQL
    stmt.addBatch("INSERT INTO sys_log (content) VALUES ('login')");
    stmt.addBatch("INSERT INTO sys_log (content) VALUES ('logout')");
    
    // 2. 批量发送至数据库一次性执行
    int[] results = stmt.executeBatch();
    
    // 3. 清理已执行的本地批处理缓存
    stmt.clearBatch();

控制与调优 ​

Statement 提供了多种运行时控制参数,用于防止慢查询拖垮应用和避免客户端内存溢出。

  • setQueryTimeout(int seconds):设置单条 SQL 在服务端执行的最长等待超时时间(单位为秒)。超时后驱动向数据库发送取消信号并抛出异常。

  • setMaxRows(int max):设置 ResultSet 允许读取的最大数据行数。超出该阈值后多余数据被直接丢弃,常用于防止无 LIMIT 的全表扫描击穿内存。

  • setFetchSize(int rows):提示驱动程序每次从数据库网络套接字中分批拉取的记录数,用于平衡内存消耗与网络请求频次。

    java
    // 1. 设置 SQL 执行最长等待超时时间(秒)
    stmt.setQueryTimeout(5);
    
    // 2. 限制服务端最大返回行数以防内存溢出
    stmt.setMaxRows(500);
    
    // 3. 设置每次网络分批拉取的记录数
    stmt.setFetchSize(100);

安全风险与对比 ​

在实际生产开发中,应尽量避免直接使用基础的 Statement。

sql
SELECT id, username
  FROM sys_user
  WHERE username = 'admin' OR '1'='1' AND password = 'xxx';
  • SQL 注入隐患:Statement 依赖外部字符串拼接构造 SQL。若恶意用户输入包含单引号、注释符或 OR 1=1 等片段,会直接篡改 SQL 的抽象语法树(AST),造成敏感数据泄露或越权。
  • 缺少编译缓存:每次调用 Statement.execute() 时,数据库服务端都必须重新完成词法分析、语法解析、语义校验并生成执行计划,高并发下数据库 CPU 开销大。
核心特性StatementPreparedStatement
SQL 构建方式外部字符串直接拼接模板化 SQL + ? 参数占位符
SQL 注入防护无防护,极易遭受注入攻击编译期语法树固定,参数按字面量转义,完全免疫
执行计划缓存服务端无法有效命中执行计划缓存服务端对同构 SQL 模板编译一次并缓存执行计划
二进制/大字段支持拼接困难,需手动 Base64 或 Hex 转换原生支持 setBlob()、setBinaryStream()
适用场景仅限于结构固定的静态 DDL / DQL 脚本绝大多数生产业务中的 DML 与 DQL 交互

预编译与防注入 ​

PreparedStatement 是 JDBC 体系中专用于执行参数化预编译 SQL 的核心接口。它通过将 SQL 模板的“语法结构”与具体的“数据参数”彻底分离,从底层切断了 SQL 注入攻击路径,并提升了数据库高频执行同构 SQL 的性能。

预编译原理 ​

数据库处理一条 SQL 语句需要经历词法分析、语法分析、生成抽象语法树(AST)、优化器选择执行路径以及生成执行计划等耗时步骤。

  • 传统 Statement 的执行路径:每次执行都会将拼接好的整串 SQL 发送给数据库,服务端必须针对每一条 SQL 重新进行完整的语法分析与编译优化。
  • PreparedStatement 的执行路径:
    1. 模板预编译:先将包含参数占位符(?)的 SQL 模板发送至服务端,服务端提前完成语法解析并生成固定结构的执行计划缓存。

    2. 参数独立绑定:后续执行时仅需将参数值单独传输给数据库,引擎直接将参数填充到已编译好的执行计划中运行。

    3. 性能收益:在批量操作或高并发同构 SQL(仅参数值不同)场景下,大幅降低了数据库服务端的 CPU 解析开销。

防注入机制 ​

SQL 注入的本质是:应用程序使用字符串拼接 SQL 时,数据库将外部输入的“数据内容”误识别为“语法指令”,从而改变了原有的抽象语法树结构。

sql
SELECT id, username, email
  FROM sys_user
  WHERE username = 'admin' OR '1'='1' AND password = 'xxx';

预编译如何彻底免疫注入

  1. 语法树提前固化:在执行 conn.prepareStatement(sql) 时,SQL 语句的结构骨架在数据库内部已经完成解析,语法树节点数量与逻辑关系(如 WHERE 包含几个条件分支)已被锁定。

  2. 纯文本字面量处理:随后通过 setXxx() 绑定的参数,无论传入何种特殊字符(如 ' OR '1'='1、-- 、; DROP TABLE),数据库在执行阶段均将其严格视作纯文本字面常量(Literal Value),绝不会再次触发词法分析,因此根本无法改变既定的 SQL 执行逻辑。

参数绑定规范 ​

PreparedStatement 使用问号(?)作为占位符,并提供了一系列强类型的 Setter 方法用于安全绑定参数。

核心规则与方法

  • 从 1 开始的参数索引:占位符位置索引从 1 开始,从左至右依次递增。

  • 显式类型映射:根据 Java 类型与数据库列类型选择对应的绑定方法:

    • setString(int parameterIndex, String x):绑定字符串类型。

    • setInt(int parameterIndex, int x):绑定整型数据。

    • setBigDecimal(int parameterIndex, BigDecimal x):绑定高精度数值。

    • setNull(int parameterIndex, int sqlType):向指定占位符显式写入 SQL NULL。

    • setObject(int parameterIndex, Object x):通用参数绑定(驱动自动推断类型)。

    java
    // 1. 创建预编译执行器并传入 SQL 模板
    try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
      // 2. 按问号索引顺序绑定安全字面量
      pstmt.setString(1, "admin' OR '1'='1");
      pstmt.setInt(2, 1);
      // 3. 执行查询并遍历返回结果集
      try (ResultSet rs = pstmt.executeQuery()) {
        while (rs.next()) {
          String name = rs.getString("username");
        }
      }
    }

预编译底层分类 ​

在 MySQL 等主流数据库的 JDBC 驱动中,预编译存在客户端模拟与服务端真实预编译两种实现模式:

模式分类开启方式交互协议底层工作机制
客户端模拟预编译默认模式(无需额外配置)Text 文本协议驱动本地使用特定转义规则替换 ?,将转义后的完整 SQL 文本一次性发送给服务端。依然具备防注入能力,但未在服务端缓存执行计划。
服务端真正预编译连接串添加 useServerPrepStmts=trueBinary 二进制协议驱动发送 COM_STMT_PREPARE 命令在服务端生成预编译语句 ID,随后发送 COM_STMT_EXECUTE 传入二进制参数,实现真正的执行计划缓存。

生产连接池调优配置

若要最大化发挥服务端预编译的性能优势,建议在 JDBC 连接 URL 中组合配置预编译缓存参数:

properties
jdbc:mysql://localhost:3306/demo_db?useServerPrepStmts=true&cachePrepStmts=true&prepStmtCacheSize=250&prepStmtCacheSqlLimit=2048
  • cachePrepStmts=true:开启客户端对 PreparedStatement 对象的本地复用缓存。
  • prepStmtCacheSize=250:指定每个物理连接最多缓存的预编译 SQL 数量。
  • prepStmtCacheSqlLimit=2048:指定允许被缓存的单条 SQL 最大字符长度。

占位符使用局限 ​

PreparedStatement 的 ? 占位符只能用于替换 SQL 语法中的字面值(Values / Literals),不能用于替换数据库对象或语法关键字。

占位符禁止使用的场景

  • 表名与库名:SELECT * FROM ?(语法错误)
  • 字段列名:SELECT ? FROM sys_user(会被解析为常量字符串 'username',而非读取该列数据)
  • 排序方向:ORDER BY id ?(语法错误)
  • SQL 关键字与运算符:WHERE age ? 18(语法错误)

动态场景的安全防御策略

对于无法使用 ? 占位符的场景(如动态分表、动态排序字段),严禁直接拼接未经检验的外部入参,必须采用白名单校验(Whitelist Validation)或强类型枚举映射:

java
// 1. 对不可预编译的动态字段使用严格白名单校验
if (!ALLOWED_SORT_COLUMNS.contains(sortField)) {
  throw new IllegalArgumentException("Invalid sort column: " + sortField);
}
// 2. 安全拼接校验通过的白名单列名,参数部分依然预编译
String sql = "SELECT id, username FROM sys_user WHERE status = ? ORDER BY " + sortField;
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
  pstmt.setInt(1, 1);
  // 3. 执行安全过滤后的混合查询
  try (ResultSet rs = pstmt.executeQuery()) {
    while (rs.next()) {
      String name = rs.getString("username");
    }
  }
}

提取结果集 ​

ResultSet 结果集处理是 JDBC 数据检索的核心环节,负责将数据库返回的行列表格数据逐行读取并转换为 Java 实体对象。

游标移动机制 ​

ResultSet 内部维护着一个行级游标指针。在查询刚执行完成时,游标默认停留在第一行记录之前(beforeFirst 位置)。

  • 初始状态:游标未指向任何实际行,直接读取数据会抛出异常。

  • 逐行向下:调用 rs.next() 使游标前移一行。如果当前行存在有效数据,返回 true;若已到达结果集末尾,返回 false 并结束迭代。

  • 单行与多行查询:多条记录遍历使用 while (rs.next());主键或唯一索引等单条记录查询使用 if (rs.next())。

    sql
    SELECT id, username, score, age
      FROM sys_user
      WHERE status = 1
      ORDER BY id ASC;

字段数据提取 ​

当游标定位在指定数据行时,通过与字段数据类型对应的 getXxx() 系列方法提取具体列的值。

提取方式对比

  • 按字段名称读取(推荐):如 rs.getString("username")。使用 SQL 语句中的列名或 AS 别名(columnLabel),代码可读性高且不受数据库表结构字段增删调序的影响。
  • 按字段索引读取:如 rs.getInt(1)。索引从 1 开始递增,执行效率略高于名称寻址,但在 SQL 增减查询字段时容易发生列错位。

常用类型转换对照

SQL 数据类型推荐 Java 提取方法映射目标 Java 类型
VARCHAR / TEXTgetString()String
INT / INTEGERgetInt()int
BIGINTgetLong()long
DECIMAL / NUMERICgetBigDecimal()BigDecimal
DATE / TIMESTAMPgetObject(col, LocalDateTime.class)LocalDateTime

空值安全校验 ​

Java 的基本数据类型(如 int、double、boolean)不支持接收 null。当数据库中字段存储为 SQL NULL 时,JDBC 规范会将其隐式赋为默认初值(如 0、0.0、false),这容易导致业务逻辑误判。

wasNull 校验规范

  • 先调用 getXxx() 正常提取该字段。

  • 紧随其后调用 rs.wasNull(),用于判定刚刚读取的那个列在数据库中是否真实为 NULL。

  • 若确定为 NULL,可手动转换为包装类 null 或重置为业务默认值。

    java
    // 1. 循环移动游标逐行读取数据
    while (rs.next()) {
      int id = rs.getInt("id");
      // 2. 优先通过列名称提取数据
      String username = rs.getString("username");
      int age = rs.getInt("age");
      // 3. 校验上一次读取的基本类型是否为 SQL NULL
      if (rs.wasNull()) {
        age = 0;
      }
    }

实体对象映射 ​

在实际业务开发中,提取出的离散字段通常需要转换为强类型的业务实体对象(POJO / DTO)。

标准映射步骤

  1. 在结果集遍历外部创建用于保存数据的集合容器(如 List<T>)。

  2. 在 while (rs.next()) 循环体内实例化单个数据传输对象。

  3. 调用实体的 Setter 方法或构造函数将 ResultSet 字段依次注入。

  4. 将封装完整的实体对象添加到列表中。

    java
    // 1. 创建容器存放映射后的实体对象
    List<UserDTO> userList = new ArrayList<>();
    while (rs.next()) {
      UserDTO user = new UserDTO();
      // 2. 提取字段并注入实体属性
      user.setId(rs.getLong("id"));
      user.setUsername(rs.getString("username"));
      user.setScore(rs.getBigDecimal("score"));
      userList.add(user);
    }

资源释放管理 ​

ResultSet 底层持有数据库服务端打开的游标指针与网络传输缓冲区。如果未及时关闭,极易造成数据库游标耗尽与应用内存泄漏。

  • 释放顺序:遵循“后开启、先关闭”的逆序原则,依次关闭 ResultSet、Statement、Connection。

  • 自动管理:推荐统一采用 try-with-resources 语法块,JVM 会在代码块执行结束时自动调用其 close() 方法完成底层资源的清理。

    java
    String sql = "SELECT id, username, score FROM sys_user WHERE id = ?";
    // 1. 将执行器与结果集置于 try 括号内实现自动关闭
    try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
      pstmt.setLong(1, 1001L);
      // 2. 执行预编译查询并遍历结果
      try (ResultSet rs = pstmt.executeQuery()) {
        if (rs.next()) {
          String name = rs.getString("username");
        }
      }
    }

SQLException ​

SQLException 是所有 JDBC 数据交互操作抛出的标准受检异常(Checked Exception),提供了丰富的数据库诊断信息。

核心排查方法

  • getMessage():获取异常描述文本。

  • getSQLState():获取遵循 ANSI SQL 或 X/Open 标准的 5 字符状态码(如 23000 表示违反完整性约束)。

  • getErrorCode():获取数据库厂商自定义的底层专有错误编号(例如 MySQL 错误码 1062 表示主键冲突)。

  • getNextException():JDBC 异常采用链表结构组织,通过该方法可递归提取执行批处理或多语句时产生的后续异常。

    java
    // 1. 捕获标准的数据库操作受检异常
    try (Connection conn = DriverManager.getConnection(url, user, pwd)) {
      conn.createStatement().execute("SELECT 1");
    } catch (SQLException ex) {
      // 2. 提取厂商专有错误代码与 SQL 标准状态码
      System.err.println("SQLState: " + ex.getSQLState());
      System.err.println("ErrorCode: " + ex.getErrorCode());
      // 3. 处理多重链式异常信息
      SQLException next = ex.getNextException();
      while (next != null) {
        System.err.println("Next: " + next.getMessage());
        next = next.getNextException();
      }
    }

事务控制 ​

JDBC 中的事务控制用于管理一组不可分割的数据库操作,确保多条 SQL 语句作为一个原子工作单元执行,要么全部持久化成功,要么在发生异常时全部撤销回滚。

事务是数据库并发控制与数据恢复的基本逻辑单元。JDBC 事务严格遵循关系型数据库的 ACID 规范。

核心控制 API ​

JDBC 的事务控制完全由 java.sql.Connection 接口承载与管理。

sql
UPDATE sys_account
  SET balance = balance - 500.00
  WHERE user_id = 1001;

核心方法汇总

  • void setAutoCommit():(boolean autoCommit),设置自动提交模式。传入 false 开启手动事务,传入 true 开启单语句自动提交。

  • boolean getAutoCommit():(),获取当前自动提交状态。返回当前连接是否处于自动提交模式。

  • void commit():(),提交事务。将自上次提交或回滚以来发生的所有变更永久持久化到数据库。

  • void rollback():(),完整回滚事务。撤销当前事务中执行的所有 SQL 变更并释放数据库锁。

  • void rollback():(Savepoint savepoint),局部回滚至保存点。撤销指定保存点之后发生的所有 SQL 变更,保留保存点之前的变更。

  • Savepoint setSavepoint():(),创建匿名保存点。在当前事务中创建一个由系统分配编号的未命名保存点。

  • Savepoint setSavepoint():(String name),创建具名保存点。在当前事务中创建一个指定名称的保存点。

  • void releaseSavepoint():(Savepoint savepoint),释放保存点。从事务状态机中移除指定的保存点以回收资源。

  • void setTransactionIsolation():(int level),设置事务隔离级别。修改当前会话的事务隔离等级(常量定义在 Connection 接口中)。

  • int getTransactionIsolation():(),获取当前事务隔离级别。返回当前会话正在生效的事务隔离常量整数值。

标准控制流程 ​

手动事务控制遵循严格的生命周期步骤:

  1. 关闭自动提交:调用 conn.setAutoCommit(false) 显式界定事务起点。

  2. 执行业务操作:在同一连接会话内依次执行多个 DML 更新。

  3. 提交事务:当全部操作均无异常时,调用 conn.commit() 完成持久化。

  4. 异常回滚:在 catch 块中捕获 SQLException 并调用 conn.rollback()。

  5. 重置状态与释放连接:在 finally 块中恢复自动提交模式并关闭连接(防止连接归还连接池后污染后续业务)。

    java
    // 1. 关闭自动提交以开启手动事务控制
    conn.setAutoCommit(false);
    try {
      // 2. 在同一连接下执行一系列业务更新
      pstmt1.executeUpdate();
      pstmt2.executeUpdate();
      conn.commit();
    } catch (SQLException e) {
      // 3. 发生异常时回滚所有未提交的操作
      conn.rollback();
      throw e;
    } finally {
      // 4. 恢复连接的默认自动提交模式
      conn.setAutoCommit(true);
    }

事务隔离级别 ​

为解决并发事务可能导致的脏读(读取了未提交数据)、不可重复读(同事务内两次读取同一行数据不一致)与幻读(同事务内两次范围查询记录数不一致),JDBC 提供了四种标准隔离级别常量:

隔离级别常量级别数值脏读不可重复读幻读性能
TRANSACTION_READ_UNCOMMITTED1允许允许允许最高
TRANSACTION_READ_COMMITTED2杜绝允许允许较高(Oracle/PG 默认)
TRANSACTION_REPEATABLE_READ4杜绝杜绝允许中等(MySQL 默认)
TRANSACTION_SERIALIZABLE8杜绝杜绝杜绝最低
java
// 1. 设置当前连接的事务隔离级别为可重复读
conn.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);
conn.setAutoCommit(false);

保存点机制 ​

保存点(Savepoint) 允许将一个大型事务切分为多个细粒度阶段。当后续阶段出现非致命异常时,支持仅回滚至局部节点,而无需废弃整个事务。

sql
UPDATE sys_order
  SET pay_status = 1
  WHERE order_id = 9001;

局部回滚执行步骤

  1. 开启事务并执行阶段一操作。

  2. 调用 conn.setSavepoint() 标记当前执行进度。

  3. 执行阶段二操作;若阶段二发生异常,调用 conn.rollback(savepoint) 仅撤销阶段二的改动。

  4. 调用 conn.commit() 提交阶段一成功的数据。

    java
    // 1. 开启事务并执行首阶段操作
    conn.setAutoCommit(false);
    pstmt1.executeUpdate();
    // 2. 设置局部回滚保存点
    Savepoint sp = conn.setSavepoint("Savepoint1");
    try {
      pstmt2.executeUpdate();
    } catch (SQLException e) {
      // 3. 局部异常仅回滚到指定保存点
      conn.rollback(sp);
    }
    // 4. 提交第一阶段成功的变更
    conn.commit();

生产开发规范 ​

在企业级高并发架构中,事务管理需要注意以下核心准则:

  • 同一物理连接约束:参与同一事务的所有 DAO 操作必须持有同一个 Connection 实例,通常借助 ThreadLocal 传递或使用 Spring 的声明式事务管理器(PlatformTransactionManager)。
  • 控制事务粒度:严禁在事务边界内执行耗时的网络 RPC 调用、复杂文件 I/O 或大循环计算,防止长时间持有数据库行锁引发连接池排队与死锁。
  • 连接池状态重置:在使用 HikariCP 或 Druid 时,借出的连接若修改了 autoCommit 或 isolation,必须在连接归还连接池前恢复默认值,防止状态污染影响后续线程。

实战:转账业务 ​

经典转账业务是验证事务原子性(Atomicity)与一致性(Consistency)的标准场景:转出方扣款与转入方收款必须绑定在同一个物理连接中,二者要么全部成功生效,要么中途出错全部撤销。

核心执行流程

转账事务的标准控制流程分为五个步骤:

  1. 开启事务:调用 conn.setAutoCommit(false) 关闭自动提交。

  2. 扣除余额:执行 DML 更新,扣除 Alice 账户 100 元。

  3. 增加余额:执行 DML 更新,增加 Bob 账户 100 元。

  4. 提交事务:两步均执行成功后,调用 conn.commit() 统一将数据写入磁盘。

  5. 异常回滚:若扣款后发生网络故障或代码异常,进入 catch 块调用 conn.rollback(),撤销已扣款操作。

image-20260822163713001


完整代码实现

java
// 1. 获取物理连接并显式开启手动事务控制
try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD)) {
  conn.setAutoCommit(false);
  String deductSql = "UPDATE account SET balance = balance - ? WHERE name = ?";
  String addSql = "UPDATE account SET balance = balance + ? WHERE name = ?";
  // 2. 在同一事务内依次执行扣款与收款
  try (PreparedStatement psDeduct = conn.prepareStatement(deductSql);
     PreparedStatement psAdd = conn.prepareStatement(addSql)) {
    psDeduct.setBigDecimal(1, new BigDecimal("100.00"));
    psDeduct.setString(2, "Alice");
    psDeduct.executeUpdate();
    psAdd.setBigDecimal(1, new BigDecimal("100.00"));
    psAdd.setString(2, "Bob");
    psAdd.executeUpdate();
    // 3. 两步操作均成功完成,统一提交事务
    conn.commit();
  } catch (Exception e) {
    // 4. 发生任何异常立即回滚撤销所有变更
    conn.rollback();
    throw e;
  }
} catch (Exception e) {
  e.printStackTrace();
}

异常回滚验证

  • 正常执行:Alice 余额变为 900 元,Bob 余额变为 1100 元,账户资金总和保持 2000 元不变。
  • 模拟异常:若在扣款成功后插入人为异常(如 int error = 10 / 0;),程序进入 catch 块执行 conn.rollback(),数据库自动撤销 Alice 的扣款操作,两方余额均恢复为 1000 元。

批处理优化 ​

JDBC 批处理(Batch Processing) 是一种将多条 SQL 语句或参数集暂存在客户端缓冲区,随后一次性打包发送至数据库执行的优化机制。在大数据量插入、更新或删除场景下,批处理能成倍降低网络 I/O 延迟并减轻数据库服务端的处理负荷。

痛点与核心概念 ​

在常规的 JDBC 循环操作中,执行单条 SQL 存在显著的性能瓶颈。

  • 单条执行的痛点:若循环执行 10,000 次 executeUpdate(),应用程序与数据库之间会发生 10,000 次网络请求与响应(Round-Trip Time),伴随 10,000 次 TCP 报文交互、事务日志写入与服务端上下文切换,导致整体耗时极长。
  • 批处理的机制:将同构或异构的 SQL 语句在客户端内存中汇聚成一个批次(Batch),通过一次网络 I/O 请求直接发送给数据库批量执行,将网络开销从 O(N)O(N) 降至 O(1)O(1) 或 O(N/BatchSize)O(N/BatchSize)。

核心 API ​

JDBC 批处理的标准操作由 Statement / PreparedStatement 提供的三个核心方法完成:

  • void addBatch():(String sql),追加批处理命令。将指定的静态 SQL 字符串缓存在当前 Statement 内部的批处理命令列表中。

  • int[] executeBatch():(),执行批量 SQL。将批处理队列中的所有 SQL 一次性提交到底层数据库执行,返回记录每条语句影响行数的整型数组。

  • void clearBatch():(),清空批处理队列。清空此前通过 addBatch() 积攒的所有 SQL 语句缓存。

  • long[] executeLargeBatch():(),执行海量批量 SQL。将批处理队列中的所有 SQL 一次性提交执行,以 long[] 形式返回各条语句影响的行数。

标准执行流程 ​

批处理遵循以下的执行流程:

  1. 关闭连接的自动提交模式(conn.setAutoCommit(false)),将整批操作纳入同一事务控制。

  2. 循环填充数据,并调用 addBatch() 持续将数据压入批处理缓冲区。

  3. 达到预设阈值(如每 1,000 条)时,调用 executeBatch() 触发网络发送,并紧随其后调用 clearBatch() 释放本地缓存。

  4. 循环结束后执行剩余未满批次的数据,最后调用 conn.commit() 统一提交事务。

    java
    // 1. 关闭自动提交开启批处理事务
    conn.setAutoCommit(false);
    String sql = "INSERT INTO sys_log (content, level) VALUES (?, ?)";
    // 2. 循环绑定参数并加入本地批处理缓存
    try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
      for (int i = 1; i <= 5000; i++) {
        pstmt.setString(1, "log_entry_" + i);
        pstmt.setInt(2, 1);
        pstmt.addBatch();
        if (i % 1000 == 0) {
          // 3. 达到阈值分批发送并清空缓存
          pstmt.executeBatch();
          pstmt.clearBatch();
        }
      }
      pstmt.executeBatch();
      conn.commit();
    }

MySQL 重写机制 ​

在 MySQL 环境中使用 PreparedStatement.addBatch() 时,默认情况下底层驱动依然会将批处理拆分为多条独立的 INSERT INTO ... VALUES (...) 语句依次发送,无法发挥多值插入的高性能优势。

sql
INSERT INTO sys_log (content, level)
  VALUES ('log_1', 1),
       ('log_2', 1),
       ('log_3', 1);

连接串参数关键配置

必须在 JDBC 连接 URL 末尾显式添加 rewriteBatchedStatements=true 参数:

properties
jdbc:mysql://localhost:3306/demo_db?rewriteBatchedStatements=true&useServerPrepStmts=false
  • 未开启重写:驱动向 MySQL 发送多个独立的 SQL 请求包(如 INSERT INTO ...; INSERT INTO ...;),性能提升有限。
  • 开启重写后:MySQL Connector/J 驱动会在本地自动将多条 INSERT 模板动态重写为单条多值插入语句(INSERT INTO ... VALUES (...), (...), (...)),执行效率可提升数十倍。

分批与内存控制 ​

如果一次性将数十万甚至数百万条数据全部调用 addBatch() 压入内存后再执行,极易引发 JVM 堆内存溢出(OOM)或触发数据库的数据包大小限制。

容量与性能权衡

  • 分批提交大小(Batch Size):建议将分批阈值设定在 500 至 2,000 之间。批次过小无法有效摊平网络开销,批次过大则会导致客户端内存占用激增以及单次 SQL 构建时间过长。

  • 数据包上限配置:重写为单条多值 INSERT 后,SQL 文本长度可能超过 MySQL 服务端的 max_allowed_packet(默认通常为 16MB 或 64MB)。若单批次数据过大,会直接抛出 PacketTooBigException 异常。

    java
    if (count % 1000 == 0) {
      pstmt.executeBatch();
      pstmt.clearBatch();
    }
    // 1. 每累积执行 5000 条进行一次事务阶段性提交
    if (count % 5000 == 0) {
      conn.commit();
    }

异常处理与调优规范 ​

在批处理执行期间,如果某一条数据违反唯一索引或外键约束,将抛出 BatchUpdateException。

核心排查与规范

  • 错误行定位:捕获 BatchUpdateException 并调用 ex.getUpdateCounts()。返回数组中值为 Statement.EXECUTE_FAILED(-3)的项即为执行失败的记录。

  • 事务隔离与回滚:批处理必须配合手动事务;一旦发生异常,应在 catch 块中立即调用 conn.rollback(),防止局部数据产生脏写入。

  • 对象复用:在整个循环过程中复用同一个 PreparedStatement 实例,严禁在循环体内部重复创建 Statement。

    java
    // 1. 捕获专用的批处理异常
    try {
      pstmt.executeBatch();
      conn.commit();
    } catch (BatchUpdateException bue) {
      // 2. 检查每条语句的执行状态数组
      int[] updateCounts = bue.getUpdateCounts();
      // 3. 统一回滚整个批次保证数据一致性
      conn.rollback();
      throw bue;
    }

源码分析 ​

JDBC 批处理的核心优化逻辑主要体现在驱动层(以主流的 MySQL Connector/J 为例)。它通过在客户端内存中暂存参数集,并在触发执行时利用 SQL 语法树重写机制,将离散的单条插入合并为一条多值插入语句,从底层减少网络交互与事务日志开销。

调用链总览 ​

在 MySQL 驱动源码中,PreparedStatement 批处理的底层核心实现类为 ClientPreparedStatement,整个执行链路分为四个核心阶段:

  1. 参数暂存:调用 addBatch(),将当前已绑定的参数状态(QueryBindings)进行深拷贝,存入 batchedArgs 列表。

  2. 路由分发:调用 executeBatch(),进入 executeBatchInternal() 校验配置项(如 rewriteBatchedStatements)与 SQL 类型。

  3. 语句重写:若命中重写条件,路由至 executeBatchedInserts(),将多组参数拼装为单条多值 INSERT 语句并处理数据包分块。

  4. 清理缓存:执行完毕或调用 clearBatch(),清空 batchedArgs 列表以释放堆内存。

    sql
    INSERT INTO sys_log (content, level, created_at)
      VALUES ('msg_1', 1, NOW()),
           ('msg_2', 1, NOW());

addBatch 缓存机制 ​

在 ClientPreparedStatement 中,每次调用 setXxx() 绑定的参数会暂存在当前的 QueryBindings 对象中。调用 addBatch() 时,驱动会将其克隆并添加到本地参数列表中:

java
// 1. 将当前绑定的参数对象深拷贝并存入列表
public void addBatch() throws SQLException {
  synchronized (checkClosed().getConnectionMutex()) {
    // 2. 克隆当前绑定的参数状态
    QueryBindings<?> queryBindings = this.query.getQueryBindings().clone();
    this.batchedArgs.add(queryBindings);
  }
}
  • 深拷贝隔离:通过 clone() 方法创建参数镜像,确保后续对同一 PreparedStatement 重新调用 setXxx() 时不会覆盖已加入批次的旧数据。
  • 纯客户端内存开销:此阶段完全在 JVM 内存中进行,不产生任何底层网络 I/O 交互。

image-20260822173132272

executeBatch 路由分发 ​

调用 executeBatch() 时,驱动层会根据连接属性和 SQL 模板结构进行策略路由:

java
// 1. 检查是否开启重写参数且属于预编译批处理
if (this.session.getPropertySet().getBooleanProperty(PropertyKey.rewriteBatchedStatements).getValue()
    && this.statementExecutingBatch) {
  if (this.query.hasValuesClause()) {
    // 2. 匹配 INSERT 模板,进入重写分支
    return executeBatchedInserts(batchTimeout);
  }
}
// 3. 未开启重写或非 INSERT 模板时,退化为逐条发送
return executeBatchSerially(batchTimeout);
  • 未开启重写(executeBatchSerially):驱动只能通过 for 循环依次向 MySQL 发送单独的 SQL 指令,网络交互次数依然为 O(N)O(N)。
  • 开启重写(executeBatchedInserts):只有检测到 SQL 属于 INSERT ... VALUES(或 INSERT ... ON DUPLICATE KEY UPDATE / REPLACE)结构时,才会激活高性能的多值合并逻辑。

executeBatchedInserts 重写核心 ​

executeBatchedInserts() 是性能提升数十倍的关键所在。驱动在此处将原本结构相同的多条 INSERT INTO table VALUES (?, ?) 模板重写为一条 INSERT INTO table VALUES (?, ?), (?, ?), ... 语句。

java
// 1. 提取 VALUES 之后的占位符模板并拼接为多值形式
StringBuilder valuesBuffer = new StringBuilder();
for (int i = 0; i < batchSize; i++) {
  valuesBuffer.append(i == 0 ? " VALUES " : ", ").append(template);
  // 2. 检查拼接后的总长度是否超出网络数据包限制
  if (valuesBuffer.length() >= maxAllowedPacket) {
    flushCurrentBatch();
  }
}
// 3. 发送重写后的单条多值 SQL 并展平绑定全部参数
execute(rewrittenSql, flattenedBindings);
  • 模板截取与动态拼装:解析原 SQL 的静态头部(INSERT INTO table (cols)),保留 VALUES 之后的括号结构,并在循环中追加 , (?, ...)。
  • 参数展平(Flattening):将二维的参数列表 List<QueryBindings> 展平为一维参数流,对应重写后 SQL 中所有扩展出来的 ? 占位符。
  • 最大数据包防护(maxAllowedPacket):驱动在内存拼接时会动态计算生成的 SQL 字节长度。若即将超过 MySQL 服务端的 max_allowed_packet 阈值,驱动会自动在此截断并立即发送当前批次,随后开启新包继续拼接,防止报文过大被服务端断开。

clearBatch 内存清理 ​

批处理执行完毕后,必须释放客户端持有的参数对象,避免在长生命周期对象中造成内存泄漏:

java
// 1. 清空本地参数批处理集合
public void clearBatch() throws SQLException {
  synchronized (checkClosed().getConnectionMutex()) {
    if (this.batchedArgs != null) {
      // 2. 释放集合中持有的所有 QueryBindings 参数对象
      this.batchedArgs.clear();
    }
  }
}
  • 释放对象引用:清空 batchedArgs 列表,使其中保存的参数对象能够被 JVM 的垃圾收集器(GC)及时回收。
  • 状态复位:重置批处理计数器与内部缓冲区,准备承载下一批数据。

连接池技术 ​

数据库连接池(Connection Pool) 是解决传统 DriverManager 每次请求都需建立底层 TCP 物理连接与安全认证开销过大的核心技术。它通过在内存中预先创建并维护一定数量的数据库连接,供应用程序高效复用。

核心原理 ​

在没有连接池的高并发场景下,每次数据库交互都要经历完整的建连与断连流程。

  • 物理连接瓶颈:每次调用 DriverManager.getConnection() 都会触发三次握手、SSL 协商、用户名密码校验及服务端的线程分配,频繁操作会严重消耗 CPU 与内存。

  • 连接池复用机制:应用启动时按配置初始化一定数量的物理连接放入“池”中;当业务需要时直接借用(Borrow),使用完毕后归还(Return)至池中而非真正销毁,从而将建连开销降为零。

    1. 应用程序通过 DataSource.getConnection() 从连接池获取连接;
    2. 连接池从空闲连接队列中取出连接,放入活跃连接集合并返回给应用;
    3. 应用使用连接执行业务,完成后调用 close() 释放连接;
    4. 连接并不真正关闭,而是被重置状态后放回空闲连接队列;
    5. 维护线程会定期创建新连接、回收多余/超时连接,并校验连接有效性;
    6. 通过复用物理连接,减少频繁创建和销毁连接的开销,提升系统性能。

核心规范接口 ​

Java 官方在 javax.sql 包中定义了标准的连接池及数据源接口规范:

  • javax.sql.DataSource:数据源接口,替代传统的 DriverManager 成为获取连接的标准入口。所有成熟的连接池框架(如 HikariCP、Druid)均实现该接口。

  • javax.sql.PooledConnection:底层物理连接的包装器,用于管理物理连接的生命周期与事件监听。

    java
    // 1. 初始化标准 DataSource 连接池实例
    HikariDataSource dataSource = new HikariDataSource();
    dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/demo_db");
    dataSource.setUsername("root");
    dataSource.setPassword("123456");
    // 2. 从连接池获取连接(实际为借用而非新建)
    try (Connection conn = dataSource.getConnection()) {
      // 执行业务 SQL 操作
    } // 3. 这里的 close 不会关闭物理连接,而是将连接归还至池中

主流连接池 ​

在 Java 生态中,诞生了多款优秀的连接池实现:

连接池名称核心特点性能表现现状与生态
DBCPApache 早期开源项目,配置项丰富表现中规中矩较老旧,部分老系统在使用
C3P0历史悠久的开源连接池,支持 robust 异常恢复性能一般逐渐被现代高性能连接池取代
Druid阿里巴巴开源,具备极其强大的监控与 SQL 防火墙功能性能良好国内企业级项目广泛使用
HikariCP号称史上最快连接池,极致精简字节码与无锁化设计极致领先现代 Java / Spring Boot 默认标准

C3P0~ ​

C3P0 是一个历史悠久且功能完备的开源 JDBC 数据库连接池与语句缓存框架。它实现了 javax.sql.DataSource 接口规范,曾作为 Hibernate 等早期主流持久层框架的官方推荐默认连接池。

在传统的 JDBC 交互中,频繁建立与销毁物理数据库连接会导致大量网络握手与身份校验开销。

  • 资源池化复用:C3P0 在应用启动时预先创建并维护一批物理连接,业务线程按需“借用”,用完后“归还”至池中。
  • Statement 缓存:支持对 PreparedStatement 对象的生命周期进行池化缓存,降低数据库服务端重复编译 SQL 模板的开销。
  • 连接容错与自愈:具备完善的空闲连接保活机制,能够自动检测并剔除由于数据库重启、网络闪断或超时而被动断开的失效连接。

核心架构 ​

C3P0 内部由多个协作模块组成,主要包含以下核心组件:

  • ComboPooledDataSource:C3P0 暴露给应用开发者的最核心入口类,实现了 javax.sql.DataSource 接口,支持通过 JavaBean 属性注入或自动读取配置文件完成初始化。
  • ResourcePool:底层的通用对象池引擎,负责连接的借出(Checkout)、归还(Checkin)、扩容以及收缩管理。
  • ThreadPoolAsynchronousRunner:异步辅助线程池。C3P0 将所有耗时的物理建连、物理断连和 Statement 销毁操作交由独立的后台守护线程异步执行,防止阻塞核心业务线程。
  • StatementCache:语句缓存管理器,为每个物理连接维护专用的执行器缓存映射表。

核心参数 ​

C3P0 提供了极其丰富的参数用于精细化控制连接池行为:

参数名称默认值作用与说明
initialPoolSize3连接池初始化时默认创建的物理连接数
minPoolSize3连接池在运行期间保持的最小物理连接数
maxPoolSize15连接池允许持有的最大物理连接上限
acquireIncrement3当池中可用连接耗尽时,一次性批量向数据库申请新建的连接数
checkoutTimeout0客户端从池中申请借用连接的最大等待时间(毫秒),设为 0 表示无限期阻塞排队
maxIdleTime0空闲连接在池中保留的最大时长(秒),超出后将被销毁回收,0 表示永不丢弃
maxStatements0全局缓存的 PreparedStatement 最大总数,0 表示关闭语句缓存
maxStatementsPerConnection0单个物理连接允许缓存的最大语句数

初始化配置 ​

C3P0 支持纯代码配置与外部配置文件两种初始化方式。

方式一:纯 Java 代码配置

java
// 1. 实例化 C3P0 核心数据源对象
ComboPooledDataSource cpds = new ComboPooledDataSource();
cpds.setDriverClass("com.mysql.cj.jdbc.Driver");
// 2. 配置数据库连接凭证与池容量
cpds.setJdbcUrl("jdbc:mysql://localhost:3306/demo_db?useSSL=false&serverTimezone=UTC");
cpds.setUser("root");
cpds.setPassword("123456");
cpds.setInitialPoolSize(5);
cpds.setMinPoolSize(5);
cpds.setMaxPoolSize(20);
// 3. 从连接池获取可复用的物理连接
try (Connection conn = cpds.getConnection()) {
  // 执行业务 SQL 操作
}

方式二:c3p0-config.xml 自动加载(推荐)

将 c3p0-config.xml 放置在类路径(resources)根目录下,实例化 new ComboPooledDataSource() 时会自动读取 <default-config> 配置:

xml
<c3p0-config>
  <default-config>
    <property name="driverClass">com.mysql.cj.jdbc.Driver</property>
    <property name="jdbcUrl">jdbc:mysql://localhost:3306/demo_db?useSSL=false&amp;serverTimezone=UTC</property>
    <property name="user">root</property>
    <property name="password">123456</property>
    <property name="initialPoolSize">5</property>
    <property name="minPoolSize">5</property>
    <property name="maxPoolSize">20</property>
    <property name="acquireIncrement">3</property>
    <property name="checkoutTimeout">30000</property>
  </default-config>
</c3p0-config>

保活与检测 ​

MySQL 等数据库默认在连接闲置超过 8 小时(wait_timeout)后单方面关闭底层 Socket。若连接池不知情,应用再次使用该死连接时会抛出通信链路异常。

sql
SELECT 1 FROM DUAL;

连接健康检测配置策略

  1. 测试 SQL 设置:配置 preferredTestQuery(如 SELECT 1),替代全表查询以提升探活性能。

  2. 定时后台探活(推荐):配置 idleConnectionTestPeriod=60,后台辅助线程每隔 60 秒自动向数据库发送轻量查询探活,提前剔除死连接。

  3. 借还检测(慎用):

    • testConnectionOnCheckout=false:借出连接时检测。每次借连接都会增加一次额外的网络请求,在高并发场景下严重降低吞吐量,建议关闭。
    • testConnectionOnCheckin=false:归还连接时检测。同样会产生额外的网络开销,通常建议保持关闭,优先使用后台定时探活。

Druid~ ​

Druid(德鲁伊) 是阿里巴巴开源的高性能数据库连接池项目,集成了连接池、SQL 解析引擎、性能监控统计以及 SQL 防火墙等多项企业级功能,在 Java 中文生态与企业级后台系统中应用极为广泛。

与传统的纯连接池(如 DBCP、C3P0)相比,Druid 并非单纯的物理连接管理器,而是一套综合性的数据库访问治理中间件。

  • 高性能连接管理:实现了 javax.sql.DataSource 接口规范,具备完善的连接池预热、动态扩缩容、故障自愈与泄漏排查能力。
  • SQL 解析引擎(Druid SQL Parser):内置强大的 SQL 词法与语法解析器,支持对各大主流关系型数据库方言的语法树提取与分析。
  • 多维监控统计:能够精准记录连接借还次数、活跃连接数、事务吞吐量、SQL 执行耗时分布以及并发峰值。
  • SQL 防火墙(WallFilter):基于 AST 抽象语法树在应用层拦截可疑的 SQL 注入攻击与危险全表操作。

架构与 Filter 链 ​

Druid 采用了经典的责任链模式(Filter-Chain) 来构建底层扩展体系。

  • DruidDataSource:核心数据源实现类,负责管理物理连接池的生命周期,对外暴露获取连接的接口。
  • DruidConnectionHolder:物理连接的持有包装类,记录连接的创建时间、最后活跃时间与使用次数。
  • FilterChain(过滤器链):位于应用调用与底层 JDBC 驱动之间,支持拦截所有的数据库操作:
    • StatFilter:采集 SQL 执行时间、慢 SQL 统计、影响行数与异常频次。
    • WallFilter:基于 SQL 语义分析进行黑白名单拦截与防注入检测。
    • Slf4jLogFilter / Log4j2Filter:输出规范的数据库操作日志与执行耗时。
    • ConfigFilter:支持数据库敏感密码的公私钥加解密。

核心配置参数 ​

Druid 提供了丰富的属性用于调优连接池行为与异常容错:

配置参数默认值作用说明
initialSize0连接池启动时初始化的物理连接数
minIdle0连接池长期保持的最小空闲连接数
maxActive8连接池允许借出的最大活跃连接上限
maxWait-1获取连接的最大等待超时时间(毫秒),-1 表示无限排队
timeBetweenEvictionRunsMillis1 分钟后台检测线程运行周期的间隔时间(毫秒)
minEvictableIdleTimeMillis30 分钟空闲连接在池中保留的最小存活时间,超出则被回收
validationQuery无检测连接是否有效的测试 SQL(如 SELECT 1)
testWhileIdletrue空闲时是否执行保活检测(建议开启)
testOnBorrowfalse借出连接时是否执行测试 SQL(开启会显著降低并发性能)
testOnReturnfalse归还连接时是否执行测试 SQL
poolPreparedStatementsfalse是否缓存 PreparedStatement(MySQL 环境建议关闭)

核心操作 ​

在实际开发中,通常采用外部 properties 配置文件配合工厂类进行初始化。

Druid 的核心操作建立在标准 javax.sql.DataSource 接口与过滤器链(Filter-Chain)扩展机制之上,涵盖连接初始化与生命周期管理、慢 SQL 统计、SQL 防火墙拦截、凭证加解密以及泄漏连接强制回收等企业级运维能力。

初始化与连接获取 ​

Druid 采用工厂模式或属性注入方式构建数据源。应用通过 getConnection() 借出连接,使用完毕后调用 close() 将物理连接归还至池中。

核心执行流程

  1. 读取外部属性配置文件(druid.properties)或构建配置对象。

  2. 调用 DruidDataSourceFactory.createDataSource(properties) 初始化连接池。

  3. 从连接池借出连接并执行业务 SQL。

  4. 在 try-with-resources 块结束时自动触发 conn.close() 归还连接。

    properties
    druid.driverClassName=com.mysql.cj.jdbc.Driver
    druid.url=jdbc:mysql://localhost:3306/demo_db?useSSL=false&serverTimezone=UTC&characterEncoding=UTF-8
    druid.username=root
    druid.password=123456
    druid.initialSize=5
    druid.minIdle=5
    druid.maxActive=20
    druid.maxWait=60000
    druid.timeBetweenEvictionRunsMillis=60000
    druid.minEvictableIdleTimeMillis=300000
    druid.validationQuery=SELECT 1
    druid.testWhileIdle=true
    druid.filters=stat,wall,slf4j
    java
    // 1. 读取外部属性配置文件流
    Properties prop = new Properties();
    try (InputStream in = DruidDemo.class.getClassLoader().getResourceAsStream("druid.properties")) {
      prop.load(in);
      // 2. 使用工厂类构建全局唯一的 DataSource 实例
      DataSource dataSource = DruidDataSourceFactory.createDataSource(prop);
      // 3. 从连接池借用连接并在代码块结束时自动归还
      try (Connection conn = dataSource.getConnection()) {
        // 执行业务数据操作
      }
    }
慢 SQL 与监控拦截 ​

通过在过滤器链中激活 StatFilter,Druid 能够无侵入地采集所有 SQL 的执行耗时、影响行数、并发峰值,并精准记录慢查询。

sql
SELECT id, username, score
  FROM sys_user
  WHERE score >= 90
  ORDER BY score DESC;

核心配置项

  • slowSqlMillis:慢 SQL 判定阈值(单位毫秒),超过该时间的查询会被记录。

  • logSlowSql:是否将慢 SQL 详情输出至日志系统。

  • mergeSql:开启同构 SQL 合并(将 WHERE id = 1 与 WHERE id = 2 聚合为 WHERE id = ?),便于统一统计吞吐量。

    java
    // 1. 实例化 SQL 统计过滤器并设置慢查询阈值(毫秒)
    StatFilter statFilter = new StatFilter();
    statFilter.setSlowSqlMillis(2000);
    statFilter.setLogSlowSql(true);
    // 2. 开启 SQL 合并以聚合参数不同的同构 SQL
    statFilter.setMergeSql(true);
    DruidDataSource dataSource = new DruidDataSource();
    // 3. 将过滤器注入数据源代理链
    dataSource.setProxyFilters(Collections.singletonList(statFilter));
SQL 防火墙拦截 ​

WallFilter 基于 Druid 内置的 SQL Parser 词法与语法分析引擎,能够在应用层对即将发送至数据库的 SQL 语法树(AST)进行实时安全审计。

sql
UPDATE sys_user
  SET status = 0
  WHERE id = 1001;

核心拦截策略

  • 防御永真注入:拦截包含 OR 1=1、OR 'a'='a' 等恶意篡改语法树的布尔注入。

  • 拦截无条件更新:默认禁止不带 WHERE 条件的 UPDATE 与 DELETE 语句,防止整表数据被误损。

  • 多语句拦截:禁止以分号(;)拼接多条堆叠 SQL 执行。

  • 系统表防护:禁止普通业务 SQL 探查 information_schema 等底层敏感元数据表。

    java
    // 1. 实例化 SQL 防火墙过滤器与配置对象
    WallFilter wallFilter = new WallFilter();
    WallConfig wallConfig = new WallConfig();
    wallConfig.setMultiStatementAllow(false);
    // 2. 将安全规则挂载至防火墙过滤器
    wallFilter.setConfig(wallConfig);
    // 3. 将防火墙注入数据源代理链
    dataSource.setProxyFilters(Collections.singletonList(wallFilter));
敏感凭证加解密 ​

为防止数据库连接明文密码泄露,Druid 提供了基于非对称加密算法(RSA)的 ConfigFilter,支持对配置文件中的密码进行加密存储并在运行时动态解密。

操作步骤

  1. 命令行调用 Druid 工具类生成 RSA 公钥、私钥及加密后的密文密码:

    java -cp druid-xx.jar com.alibaba.druid.filter.config.ConfigTools 123456

  2. 在属性文件中配置密文密码与公钥:

    druid.password=密文密码

    druid.publicKey=公钥字符串

    druid.filters=config

    druid.connectProperties=config.decrypt=true;config.key=${druid.publicKey}

  3. 连接池启动时,ConfigFilter 自动利用公钥将密文还原为真实凭据并建立底层连接。

连接泄漏强制回收 ​

当业务代码因异常分支或设计缺陷借出连接后忘记执行 close(),连接池中的可用连接数会逐步耗尽。Druid 提供了 removeAbandoned 机制用于自动排查并强制回收泄漏连接。

核心控制参数

  • removeAbandoned:开启连接泄漏强制回收。

  • removeAbandonedTimeout:指定连接被借出后未归还的最大允许超时时长(秒)。

  • logAbandoned:强制回收连接时,向日志输出该连接被借出时的完整 Java 代码调用栈,便于准确定位泄漏源头。

    java
    // 1. 开启未关闭连接的强制超时回收机制
    dataSource.setRemoveAbandoned(true);
    // 2. 设置借出后未归还的最大存活超时时间(秒)
    dataSource.setRemoveAbandonedTimeout(180);
    // 3. 在强制回收时输出发生连接泄漏的业务代码堆栈
    dataSource.setLogAbandoned(true);
连接池销毁与停机 ​

在 JVM 进程停止或 Web 应用卸载时,必须显式调用 close() 方法释放连接池占用的系统资源。

  • 终止守护线程:停止内部的连接创建线程(CreateConnectionThread)与定时探活销毁线程(DestroyConnectionThread)。

  • 物理断连:遍历并关闭池中持有的所有底层 Socket 物理连接。

  • 重置计数器:注销注册在 JMX(Java Management Extensions)中的监控 Bean。

    java
    // 1. 应用停机钩子中显式触发连接池销毁
    if (dataSource instanceof DruidDataSource) {
      // 2. 终止后台保活线程并断开所有底层物理 Socket 连接
      ((DruidDataSource) dataSource).close();
    }

监控与防火墙 ​

Druid 内置了开箱即用的可视化 Web 监控控制台(StatViewServlet)以及安全防护拦截。

Web 监控控制台(StatViewServlet)

在 Spring Boot 或 Web 应用中注册 StatViewServlet 后,可直接通过浏览器访问控制台(默认路径 /druid/index.html):

  • 数据源监控:实时查看连接池当前的活跃数、并发峰值、连接创建与销毁总数。
  • SQL 监控与慢 SQL 抓取:展示执行频次最高与耗时最长的 SQL 语句列表,包含最大耗时、读取行数和事务状态。
  • URI 监控:统计各个 Web 请求路径关联触发的 SQL 次数与执行开销。

SQL 防火墙(WallFilter)机制

sql
UPDATE sys_user SET status = 0 WHERE id = 1001;
  • WallFilter 在客户端对 SQL 执行语义解析,默认禁止无 WHERE 条件的 UPDATE 或 DELETE 语句,防止误操作破坏整表数据。
  • 拦截永真条件(如 1 = 1)的恶意布尔注入以及跨库系统表(如 information_schema)的探查行为。

核心参数调优 ​

连接池的性能高度依赖参数配置,核心参数设置不当容易导致线程阻塞或数据库被打垮:

  • maximumPoolSize(最大连接数):连接池允许同时维护的最大物理连接数。并非越大越好,通常根据 CPU 核心数与数据库承载能力计算(公式:CPU核心数 * 2 + 有效磁盘数)。

  • minimumIdle(最小空闲连接数):连接池保持的最小空闲连接数。

  • connectionTimeout(连接超时时间):客户端向连接池申请连接时的最大等待时间(毫秒),超时未获取到连接则抛出异常。

  • idleTimeout(空闲连接存活时间):连接允许在池中保持空闲的最大时长,超过后会被回收。

    java
    // 1. 配置 HikariCP 核心参数优化高并发性能
    HikariConfig config = new HikariConfig();
    config.setMaximumPoolSize(20);
    config.setMinimumIdle(5);
    // 2. 设置获取连接最长等待超时时间(毫秒)
    config.setConnectionTimeout(30000);

常见生产陷阱 ​

在连接池的使用过程中,如果不规范编码极易引发严重故障:

  • 连接泄漏(Connection Leak):借出连接后由于异常导致未执行 conn.close() 归还,导致可用连接数耗尽。必须严格使用 try-with-resources 语法。
  • 连接池满导致线程阻塞:当 maximumPoolSize 设置过小或 SQL 执行过慢时,大量业务线程在 getConnection() 处排队等待。
  • 状态污染:归还连接前若修改了事务隔离级别或 autoCommit 状态,未能在归还时重置,会污染后续复用该连接的其他业务线程。

元数据与 ORM 演进 ​

JDBC 提供了动态探查数据库结构与结果集属性的反射机制,这是所有现代持久层框架的底层基石。

  • DatabaseMetaData:通过 conn.getMetaData() 获取,包含数据库版本、表结构、主键及支持的隔离级别。

  • ResultSetMetaData:通过 rs.getMetaData() 获取,包含查询结果的列数、列名、别名及对应 SQL 数据类型。

    java
    try (ResultSet rs = stmt.executeQuery("SELECT id, username FROM sys_user")) {
      // 1. 获取结果集的结构元数据对象
      ResultSetMetaData meta = rs.getMetaData();
      int count = meta.getColumnCount();
      for (int i = 1; i <= count; i++) {
        // 2. 动态读取列名与对应的 SQL 数据类型
        String columnName = meta.getColumnLabel(i);
        String typeName = meta.getColumnTypeName(i);
      }
    }

持久层框架演进路线

  1. 原生 JDBC:控制粒度最细,但存在大量模板代码与手动映射开销。

  2. Apache Commons DBUtils / Spring JdbcTemplate:基于元数据封装通用的 RowMapper,消除重复的连接管理与异常处理。

  3. MyBatis:半自动 ORM,将 SQL 与 Java 业务代码解耦,支持动态 SQL 与自动化结果集映射。

  4. Hibernate / Spring Data JPA:全自动 ORM,基于实体对象映射屏蔽具体 SQL,提供完整的对象生命周期管理。

实现 JDBCUtils ​

JDBCUtils 是 JDBC 编程中的基础工具类,用于封装驱动加载、配置解析、连接获取、统一资源释放以及事务控制,旨在消除业务代码中重复的模板化配置。

设计目标 ​

在原生 JDBC 开发中,重复硬编码连接信息和繁琐的手动资源释放会导致代码冗余且难以维护。

sql
SELECT id, username, balance
  FROM sys_account
  WHERE status = 1
  ORDER BY created_at DESC;
  • 配置解耦:将数据库连接参数抽取至独立属性文件(properties),修改配置无需重新编译代码。
  • 集中初始化:利用静态代码块(static)在类加载时单次完成驱动加载或连接池初始化。
  • 统一释放资源:封装安全关闭 Connection、Statement、ResultSet 的工具方法,避免连接泄漏。
  • 事务跨层传递:引入 ThreadLocal 绑定当前线程的数据库连接,保证业务层(Service)与数据访问层(DAO)在同一事务中协同运作。

配置文件设计 ​

将数据库连接凭证与环境配置保存在类路径(resources)下的 jdbc.properties 文件中。

properties
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/demo_db?useSSL=false&serverTimezone=UTC&characterEncoding=UTF-8
jdbc.username=root
jdbc.password=123456

基础版实现 ​

基础版 JDBCUtils 适用于单体测试或轻量级教学环境,依托 DriverManager 管理连接。

java
// 1. 静态代码块随类加载仅执行一次初始化配置
static {
  try (InputStream in = JDBCUtils.class.getClassLoader().getResourceAsStream("jdbc.properties")) {
    Properties prop = new Properties();
    prop.load(in);
    // 2. 加载驱动类并读取连接信息
    Class.forName(prop.getProperty("jdbc.driver"));
    url = prop.getProperty("jdbc.url");
    user = prop.getProperty("jdbc.username");
    password = prop.getProperty("jdbc.password");
  } catch (Exception e) {
    // 3. 初始化失败抛出运行时异常阻断程序
    throw new ExceptionInInitializerError("Failed to initialize JDBCUtils: " + e.getMessage());
  }
}

提供统一的资源释放重载方法

java
// 1. 按照后打开先关闭的逆序统一释放资源
public static void close(ResultSet rs, Statement stmt, Connection conn) {
  try {
    if(rs != null) close(rs);
    if(stmt != null) close(stmt);
    // 2. 释放物理连接
    if(conn != null) close(conn);
  } catch(SQLException e) {
    throw new RuntimeException("Failed to close resources: " + e.getMessage())
  }
}

事务与线程绑定 ​

当 Service 业务方法需要调用多个 DAO 方法(例如转账扣款与收款)时,所有 DAO 操作必须使用同一个物理连接才能确保事务原子性。

使用 ThreadLocal<Connection> 将连接实例与当前请求线程绑定,无需在方法形参中层层传递 Connection。

java
// 1. 使用 ThreadLocal 保证同一线程内共享同一数据库连接
private static final ThreadLocal<Connection> CONNECTION_HOLDER = new ThreadLocal<>();

public static void beginTransaction() throws SQLException {
  Connection conn = getConnection();
  // 2. 开启手动事务
  conn.setAutoCommit(false);
}

public static void commit() throws SQLException {
  Connection conn = CONNECTION_HOLDER.get();
  // 3. 提交事务并清理当前线程绑定的连接
  if (conn != null) {
    conn.commit();
    CONNECTION_HOLDER.remove();
    conn.close();
  }
}

连接池集成 ​

在生产高并发场景下,直接调用 DriverManager 会因频繁建立 TCP 握手与认证导致性能瓶颈。企业级 JDBCUtils 统一集成 DataSource 连接池(如 Druid 或 HikariCP)。

image-20260822151915586

java
// 1. 定义全局唯一的数据源实例
private static DataSource dataSource;

static {
  try (InputStream in = JDBCUtils.class.getClassLoader().getResourceAsStream("druid.properties")) {
    // 2. 基于配置属性工厂初始化连接池
    Properties prop = new Properties();
    prop.load(in);
    dataSource = DruidDataSourceFactory.createDataSource(prop);
  } catch (Exception e) {
    // 3. 捕获异常阻断初始化
    throw new ExceptionInInitializerError("Failed to initialize DataSource: " + e.getMessage());
  }
}

业务实战调用 ​

在实际三层架构中,Service 负责控制事务边界,DAO 负责纯粹的数据操作。

sql
UPDATE sys_account
  SET balance = balance - 100.00
  WHERE id = 1001;
java
// 1. 业务层统一管理事务边界
try {
  JDBCUtils.beginTransaction();
  // 2. 执行多项 DAO 数据库操作(共享同一连接)
  accountDao.decreaseBalance(fromId, amount);
  accountDao.increaseBalance(toId, amount);
  JDBCUtils.commit();
} catch (Exception e) {
  // 3. 发生异常统一回滚
  JDBCUtils.rollback();
  throw new ServiceException("Transfer failed", e);
}

Apache DBUtils ​

Apache Commons DbUtils 是 Apache 开源组织提供的一款轻量级 JDBC 工具类库。它在不改变原生 JDBC 性能的前提下,极大地简化了数据库交互的模板代码,充当了原生 JDBC 与重量级 ORM 框架之间的轻量化桥梁。

原生 JDBC 在日常开发中存在大量重复且易出错的样板代码,例如手动提取连接、根据索引逐个绑定占位符参数、复杂的异常捕获与嵌套资源释放,以及手动将 ResultSet 游标逐列映射为实体类。

sql
SELECT id, username, email, balance
  FROM sys_user
  WHERE status = 1
  ORDER BY created_at DESC;
  • 轻量无侵入:DbUtils 只是对原生 JDBC API 进行了薄层封装,不改变 SQL 的编写方式,也不引入复杂的映射配置文件。
  • 消除样板代码:自动管理 PreparedStatement 参数绑定,自动负责 ResultSet、Statement 与 Connection 的安全关闭。
  • 自动化对象映射:内置丰富的反射映射器,支持将查询结果集自动转换为 JavaBean、List、Map 或基础标量类型。
  • 杜绝资源泄漏:提供静默释放工具,确保在发生异常时也能安全关闭底层套接字。

核心组件 ​

DbUtils 的架构设计非常精简,主要围绕三个核心类与接口展开:

  • QueryRunner:核心 SQL 执行引擎,线程安全。负责发送查询、执行增删改、处理批处理,并调度结果集处理器。
  • ResultSetHandler<T>:结果集处理策略接口。定义了 T handle(ResultSet rs) 方法,负责将数据库返回的行列表格转换为指定的 Java 数据结构。
  • DbUtils:静态辅助工具类。提供了一系列重载的静态方法,用于静默关闭资源(closeQuietly)、静默提交与回滚事务(commitAndCloseQuietly / rollbackAndCloseQuietly)以及加载驱动。

结果集处理器 ​

DbUtils 通过提供多种开箱即用的 ResultSetHandler 实现类,满足不同数据形态的提取需求:

处理器实现类转换目标类型典型应用场景
BeanHandler<T>单个 JavaBean 实体对象根据主键或唯一索引查询单条记录
BeanListHandler<T>List<JavaBean> 实体对象集合条件分页查询、多行列表展示
ScalarHandler<T>单个标量数据(如 Long、String)聚合查询(如 COUNT(*)、MAX())或查询单列值
MapHandlerMap<String, Object>查询单行数据(列名为 Key,字段值为 Value)
MapListHandlerList<Map<String, Object>>动态多表关联查询,未定义专用实体类时
ColumnListHandler<T>List<T> 单列数据集合提取整张表的所有 ID 列表或指定某一列
ArrayHandlerObject[] 数组查询单行数据转换为对象数组
KeyedHandler<K>Map<K, Map<String, Object>>以指定列(如主键)为外层 Key,整行数据为 Value

核心操作 ​

Apache Commons DbUtils 的核心操作主要围绕 QueryRunner 执行器、ResultSetHandler 结果集映射体系以及 DbUtils 辅助工具类展开。

初始化模式 ​

  • QueryRunner QueryRunner():(DataSource ds),绑定数据源构造方法。绑定统一的 javax.sql.DataSource 数据源。调用无需传入 Connection 的方法时自动借出和归还连接。

  • QueryRunner QueryRunner():(),无参构造方法。创建一个不绑定数据源的实例。执行方法时必须由外部显式传入 Connection。常用于需要精确控制事务边界的场景。


QueryRunner 提供了两种初始化工作模式,分别适用于非事务场景与手动事务场景。

  • 数据源自动模式:在构造方法中传入 DataSource。每次执行 SQL 时,QueryRunner 会自动从连接池借出连接,执行完毕后立即自动释放回连接池。

  • 手动连接模式:使用无参构造函数实例化。执行 SQL 时需显式传入 Connection 物理连接,由开发者全权控制事务边界与连接生命周期。

    java
    // 1. 数据源模式:QueryRunner 自动向连接池申请并归还连接(无需手动 close)
    QueryRunner runnerWithDs = new QueryRunner(dataSource);
    // 2. 手动连接模式:适用于多操作处于同一事务下的场景
    QueryRunner runnerManual = new QueryRunner();

DML 增删改 ​

  • int update():(Connection conn, String sql, Object... params),基于外部连接执行单条 DML。执行 INSERT、UPDATE、DELETE 变更并返回受影响行数。

  • int update():(Connection conn, String sql),基于外部连接执行无参 DML/DDL。执行静态 SQL 变更。

  • int update():(String sql, Object... params),基于数据源自动借还执行 DML。从内置数据源借出连接执行更新并自动归还。

  • int update():(String sql),基于数据源自动借还执行无参 DML/DDL。用于快速建表或静态数据修改。


数据的插入(INSERT)、修改(UPDATE)和删除(DELETE)均统一调用 runner.update() 方法完成。

sql
UPDATE sys_user
  SET balance = balance - ?, status = ?
  WHERE id = ?;
  • 变长参数绑定:SQL 中的 ? 占位符按照顺序直接在方法末尾传入变长参数(Object... params),框架内部自动处理类型映射与预编译绑定。

  • 返回值语义:方法固定返回受影响的物理行数(int 类型)。

    java
    // 1. 执行 UPDATE 数据更新操作
    String updateSql = "UPDATE sys_user SET balance = balance - ?, status = ? WHERE id = ?";
    int updatedRows = runner.update(updateSql, 50.00, 1, 1001L);
    
    // 2. 执行 DELETE 数据删除操作
    String deleteSql = "DELETE FROM sys_user WHERE id = ?";
    int deletedRows = runner.update(deleteSql, 1002L);
    
    // 3. 执行 INSERT 数据插入操作(基础新增)
    String insertSql = "INSERT INTO sys_user (username, balance, status) VALUES (?, ?, ?)";
    int insertedRows = runner.update(insertSql, "david", 1000.00, 1);

DQL 查询与映射 ​

  • <T> T query():(Connection conn, String sql, ResultSetHandler<T> rsh, Object... params),基于外部连接与变参执行查询。使用指定连接执行预编译查询,将结果集委托给 ResultSetHandler 处理,执行完毕不关闭连接。

  • <T> T query():(Connection conn, String sql, ResultSetHandler<T> rsh),基于外部连接无参执行查询。执行无需占位符参数绑定的静态查询。

  • <T> T query():(String sql, ResultSetHandler<T> rsh, Object... params),基于内置数据源与变参执行查询。从绑定的 DataSource 借出连接执行查询,完成后自动释放连接。

  • <T> T query():(String sql, ResultSetHandler<T> rsh),基于内置数据源无参执行查询。从绑定的 DataSource 自动借还连接执行静态查询。


数据查询调用 runner.query() 方法,通过传入不同的 ResultSetHandler<T> 策略实现类,将底层结果集转换为对应的 Java 数据形态。

sql
SELECT id, username, balance, status
  FROM sys_user
  WHERE status = ?
  ORDER BY id ASC;
  • BeanListHandler:将多行数据反射封装为指定实体类的 List<T> 集合。

  • BeanHandler:将单行数据反射封装为单个实体对象(无数据时返回 null)。

  • ScalarHandler:提取单行单列标量值(如 COUNT(*)、MAX() 等聚合函数)。

  • MapListHandler:将多行数据转换为 List<Map<String, Object>>(键为列名,值为字段值)。

  • ColumnListHandler:提取某一指定列的所有数据并封装为 List<T>。

    java
    // 1. 查询多行记录并自动映射为实体列表
    List<User> userList = runner.query(
      "SELECT id, username, balance FROM sys_user WHERE status = ?",
      new BeanListHandler<>(User.class),1
    );
    
    // 2. 查询单行记录映射为单个实体
    User user = runner.query(
      "SELECT id, username, balance FROM sys_user WHERE id = ?",
      new BeanHandler<>(User.class), 1001L
    );
    
    // 3. 聚合查询或提取单行单列标量值
    Long totalCount = runner.query(
      "SELECT COUNT(*) FROM sys_user",
      new ScalarHandler<Long>()
    );
    
    // 4. 多表联查映射为 Map 列表
    List<Map<String, Object>> mapList = runner.query(
      "SELECT u.username, o.order_no FROM sys_user u JOIN sys_order o ON u.id = o.user_id",
      new MapListHandler()
    );
    
    // 5. 提取单列数据集合
    List<String> usernameList = runner.query(
      "SELECT username FROM sys_user",
      new ColumnListHandler<String>("username")
    );

自增主键获取 ​

  • <T> T insert():(Connection conn, String sql, ResultSetHandler<T> rsh, Object... params),基于外部连接插入并返回生成的主键。底层自动开启 Statement.RETURN_GENERATED_KEYS,通过传入的 Handler 提取自动生成的自增主键。

  • <T> T insert():(String sql, ResultSetHandler<T> rsh, Object... params),基于数据源插入并返回生成的主键。自动借还连接完成插入并回显自增主键。

  • <T> T insertBatch():(Connection conn, String sql, ResultSetHandler<T> rsh, Object[][] params),基于外部连接批量插入并返回生成的主键集。批量执行插入并由 Handler 提取所有生成的主键集合。

  • <T> T insertBatch():(String sql, ResultSetHandler<T> rsh, Object[][] params),基于数据源批量插入并返回生成的主键集。自动借还连接完成批量主键自增插入。


在 DbUtils 1.6 及以上版本中,新增了专用的 insert() 与 insertBatch() 方法,用于在插入数据时直接返回数据库生成的主键(Generated Keys)。

sql
INSERT INTO sys_user (username, balance, status)
  VALUES (?, ?, ?);
  • 捕获主键:结合 ScalarHandler 即可直接获取插入成功后底层数据库生成的自增 ID,避免二次执行查询。

    java
    // 1. 执行 INSERT 并直接获取数据库生成的自增主键
    String insertSql = "INSERT INTO sys_user (username, balance, status) VALUES (?, ?, ?)";
    // 2. 传入 ScalarHandler 捕获自增 ID
    Long generatedId = runner.insert(insertSql, new ScalarHandler<Long>(), "eve", 2000.00, 1);

批处理操作 ​

  • int[] batch():(Connection conn, String sql, Object[][] params),基于外部连接执行批处理。通过二维数组传入多组参数,一次性分发执行批量更新,返回每组参数影响的行数数组。

  • int[] batch():(String sql, Object[][] params),基于数据源自动借还执行批处理。从数据源获取连接并执行批量更新。


batch() 方法支持一次性发送多组参数执行同构 SQL,底层调用 JDBC 的 addBatch() 与 executeBatch() 以降低网络交互延迟。

sql
INSERT INTO sys_log (content, level, created_at)
  VALUES (?, ?, NOW());
  • 参数矩阵结构:入参为二维数组 Object[][],第一维代表批次记录行数,第二维代表每条 SQL 对应的一组占位符参数。

  • 返回值说明:返回整型数组 int[],记录每条语句影响的物理行数。

    java
    // 1. 构造二维参数矩阵(外层为批次行数,内层为每行的占位符参数)
    Object[][] params = new Object[][] {
      {"login_success", 1},
      {"update_profile", 2}
    };
    // 2. 批量执行 SQL 并返回每行影响记录数的整型数组
    int[] affectedRows = runner.batch("INSERT INTO sys_log (content, level, created_at) VALUES (?, ?, NOW())", params);

事务与资源管理 ​

  • void DbUtils.commitAndCloseQuietly():(Connection conn),静默提交事务并关闭连接。执行事务提交并关闭连接,全程吞咽所有 SQLException。

  • void DbUtils.rollbackAndCloseQuietly():(Connection conn),静默回滚事务并关闭连接。回滚事务并关闭连接,吞咽所有可能产生的异常。

  • void DbUtils.closeQuietly():(Connection conn, Statement stmt, ResultSet rs),静默组合关闭全量 JDBC 资源。按逆序先后静默释放 ResultSet、Statement 以及 Connection。


在多步操作需要保证原子性时,必须使用无参 QueryRunner 并配合 DbUtils 辅助类的静态方法进行静默释放。

事务与异常处理标准流程

  1. 实例化无参 QueryRunner。

  2. 从连接池获取 Connection 并调用 conn.setAutoCommit(false) 开启事务。

  3. 调用重载方法 runner.update(conn, sql, params...) 执行多条业务更新。

  4. 业务完成调用 DbUtils.commitAndCloseQuietly(conn) 提交事务并释放连接。

  5. 捕获异常后调用 DbUtils.rollbackAndCloseQuietly(conn) 撤销操作并安全关闭连接。

sql
UPDATE sys_account
  SET balance = balance - ?
  WHERE id = ?;
java
// 1. 手动创建无参 QueryRunner 并获取物理连接
QueryRunner runner = new QueryRunner();
Connection conn = null;
try {
  conn = dataSource.getConnection();
  // 2. 关闭自动提交开启事务
  conn.setAutoCommit(false);
  runner.update(conn, "UPDATE sys_account SET balance = balance - ? WHERE id = ?", 100.00, 1L);
  runner.update(conn, "UPDATE sys_account SET balance = balance + ? WHERE id = ?", 100.00, 2L);
  // 3. 提交事务并静默关闭连接(内部吞掉 SQLException 避免二次报错)
  DbUtils.commitAndCloseQuietly(conn);
} catch (Exception e) {
  // 4. 发生异常时静默回滚事务并释放连接
  DbUtils.rollbackAndCloseQuietly(conn);
}

自定义 ResultSetHandler ​

在关系型数据库中,一对多查询(如一个订单对应多个订单明细)通过 LEFT JOIN 联表后会产生“主表数据重复”的多行平铺记录。

实现自定义 ResultSetHandler 的核心思路是在遍历结果集时,利用主键标识聚合主对象,并将关联的多条明细数据追加到主对象的集合属性中。

实体模型设计

定义主表实体 Order(订单)与从表实体 OrderItem(订单明细)。主实体中包含从实体的列表集合。

java
public class Order {
  private Long id;
  private String orderNo;
  private List<OrderItem> items = new ArrayList<>();

  // Getter, Setter, toString 省略
  public Long getId() { return id; }
  public void setId(Long id) { this.id = id; }
  public String getOrderNo() { return orderNo; }
  public void setOrderNo(String orderNo) { this.orderNo = orderNo; }
  public List<OrderItem> getItems() { return items; }
  public void setItems(List<OrderItem> items) { this.items = items; }
}

public class OrderItem {
  private Long id;
  private Long orderId;
  private String productName;
  private BigDecimal price;
  private Integer quantity;

  // Getter, Setter, toString 省略
  public Long getId() { return id; }
  public void setId(Long id) { this.id = id; }
  public Long getOrderId() { return orderId; }
  public void setOrderId(Long orderId) { this.orderId = orderId; }
  public String getProductName() { return productName; }
  public void setProductName(String productName) { this.productName = productName; }
  public BigDecimal getPrice() { return price; }
  public void setPrice(BigDecimal price) { this.price = price; }
  public Integer getQuantity() { return quantity; }
  public void setQuantity(Integer quantity) { this.quantity = quantity; }
}

关联查询 SQL

使用 LEFT JOIN 进行多表关联查询,确保没有明细条目的空订单也能被查出,并对主键进行显式别名定义以防列名冲突。

sql
SELECT o.id AS order_id, o.order_no,
     i.id AS item_id, i.product_name, i.price, i.quantity
  FROM orders o
  LEFT JOIN order_items i
    ON o.id = i.order_id
  ORDER BY o.id ASC;

聚合逻辑分析

处理平铺多行数据的步骤如下:

  1. 容器维护:使用 LinkedHashMap<Long, Order> 维护已解析的订单对象,既能根据订单 ID 快速去重索引,又能保持 SQL 查询返回的原生顺序。

  2. 主对象合并:遍历结果集时提取 order_id。若 Map 中不存在该订单,则实例化 Order 并存入 Map;若已存在则直接取出引用。

  3. 从表 NULL 值防护:当订单下无任何明细条目时,LEFT JOIN 返回的 item_id 字段为 SQL NULL。必须通过 rs.wasNull() 进行判断,防止将空数据实例化为虚假明细对象。

  4. 明细追加:解析有效的明细字段,实例化 OrderItem 并调用 order.getItems().add(item) 挂载到当前主对象中。


自定义 Handler 实现

实现 ResultSetHandler<List<Order>> 接口,完成聚合逻辑的封装。

java
// 1. 实现 ResultSetHandler 接口以封装聚合实体列表
public class OrderListHandler implements ResultSetHandler<List<Order>> {
  @Override
  public List<Order> handle(ResultSet rs) throws SQLException {
    // 2. 利用 LinkedHashMap 保持查询结果集的原有排序
    Map<Long, Order> orderMap = new LinkedHashMap<>();
    while (rs.next()) {
      long orderId = rs.getLong("order_id");
      Order order = orderMap.get(orderId);
      if (order == null) {
        order = new Order();
        order.setId(orderId);
        order.setOrderNo(rs.getString("order_no"));
        order.setItems(new ArrayList<>());
        orderMap.put(orderId, order);
      }
      long itemId = rs.getLong("item_id");
      // 3. 检查是否存在关联的明细记录(处理 LEFT JOIN 产生的 NULL)
      if (!rs.wasNull()) {
        OrderItem item = new OrderItem();
        item.setId(itemId);
        item.setOrderId(orderId);
        item.setProductName(rs.getString("product_name"));
        item.setPrice(rs.getBigDecimal("price"));
        item.setQuantity(rs.getInt("quantity"));
        order.getItems().add(item);
      }
    }
    // 4. 将去重聚合后的订单列表统一返回
    return new ArrayList<>(orderMap.values());
  }
}

业务调用验证

将自定义的 OrderListHandler 传递给 QueryRunner 执行查询,直接获取聚合后的结构化对象集合。

java
// 1. 实例化 QueryRunner 并传入数据源
QueryRunner runner = new QueryRunner(dataSource);
String sql = "SELECT o.id AS order_id, o.order_no, i.id AS item_id, i.product_name, i.price, i.quantity "
       + "FROM orders o LEFT JOIN order_items i ON o.id = i.order_id ORDER BY o.id ASC";
// 2. 传入自定义的聚合 Handler 执行多表联查
List<Order> orders = runner.query(sql, new OrderListHandler());
for (Order order : orders) {
  System.out.println("订单号: " + order.getOrderNo() + ", 包含条目数: " + order.getItems().size());
}

对比与演进 ​

对比维度原生 JDBCApache DbUtilsSpring JdbcTemplateMyBatis
代码量极多,大量重复模板极少,仅聚焦 SQL 与参数极少,提供丰富的模板方法极少,SQL 与代码完全分离
SQL 表现形式Java 代码中硬编码Java 代码中硬编码Java 代码中硬编码XML 映射文件 / 注解动态 SQL
结果集映射手动提取游标与字段反射自动映射(内置 Handler)基于 RowMapper 接口映射自动映射,支持级联与复杂关系
框架依赖仅依赖 JDK 与驱动仅依赖 commons-dbutils.jar依赖 Spring 核心上下文依赖 MyBatis 运行时与解析器
适用场景底层框架研发、学习规范独立小工具、轻量脚本、无 Spring 体系的应用基于 Spring 生态的轻量化数据访问企业级业务系统、复杂动态 SQL 查询

核心 API ​

JDBC 核心 API 主要位于 java.sql 与 javax.sql 包中。整套 API 以工厂模式与接口驱动为核心,构成了从建立连接、编译执行 SQL 到提取结果集与异常处理的完整交互链。

体系总览 ​

核心接口之间存在严密的创建依赖与生命周期链条:

  1. DriverManager 根据连接协议调度具体的厂商驱动,创建 Connection 物理连接。

  2. Connection 作为会话载体与工厂,创建 Statement、PreparedStatement 或 CallableStatement。

  3. 执行器向数据库发送 SQL,服务端执行完毕后将数据流封装为 ResultSet 返回。

  4. 全流程中产生的网络中断、语法错误或约束冲突均统一封装为 SQLException 抛出。

API: DriverManager ​

构造方法 ​

  • private DriverManager():(),私有构造方法。工具类设计,禁止外部通过 new 实例化对象。

注意事项:

  1. DriverManager 的所有公开能力均通过静态方法暴露,无需也不允许实例化。
java
import java.lang.reflect.Constructor;
import java.sql.DriverManager;

public class ConstructorInspectionDemo {
  public static void main(String[] args) {
    // 验证构造方法私有性
    Constructor<?>[] constructors = DriverManager.class.getDeclaredConstructors();
    for (Constructor<?> constructor : constructors) {
      System.out.println("构造方法修饰符: " + constructor.getModifiers() + " (私有不可实例化)");
    }
  }
}

驱动注册与管理 ​

  • void DriverManager.registerDriver():(Driver driver),注册驱动。向驱动管理器注册指定的 JDBC 驱动程序。

  • void DriverManager.registerDriver():(Driver driver, DriverAction da),注册驱动带回调。注册驱动的同时绑定驱动被注销时的回调监听器。

  • void DriverManager.deregisterDriver():(Driver driver),注销驱动。从驱动管理器已注册列表中移除指定的驱动程序。

  • Enumeration<Driver> DriverManager.getDrivers():(),获取已注册驱动列表。返回当前调用者类加载器可见的所有已注册 JDBC 驱动枚举。

  • Driver DriverManager.getDriver():(String url),定位匹配驱动。根据传入的 JDBC URL 查找能够识别并处理该格式的驱动程序。

注意事项:

  1. registerDriver 内部会自动将 Driver 封装为 DriverInfo 存入 registeredDrivers。重复注册同一个驱动实例将被忽略。
  2. deregisterDriver 会在容器或应用热卸载(如 Tomcat WebApp Reload)时被调用,以防止类加载器无法回收造成的内存泄露(Metaspace/PermGen 溢出)。
  3. getDriver(url) 若未匹配到合适驱动,会直接抛出 SQLException,而不是返回 null。
java
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Enumeration;

public class DriverManagementDemo {
  public static void main(String[] args) {
    try {
      // 1. 创建并注册自定义 Mock 驱动
      Driver mockDriver = new MockDriver();
      DriverManager.registerDriver(mockDriver);
      System.out.println("成功显式注册驱动: " + mockDriver.getClass().getName());

      // 2. 枚举当前所有可用驱动
      Enumeration<Driver> drivers = DriverManager.getDrivers();
      while (drivers.hasMoreElements()) {
        Driver d = drivers.nextElement();
        System.out.println("已挂载驱动: " + d.getClass().getName() + ",主版本: " + d.getMajorVersion());
      }

      // 3. 根据 URL 定位特定驱动
      String targetUrl = "jdbc:mock://localhost:3306/testdb";
      Driver matchedDriver = DriverManager.getDriver(targetUrl);
      System.out.println("URL 匹配到的驱动为: " + matchedDriver.getClass().getName());

      // 4. 注销驱动
      DriverManager.deregisterDriver(mockDriver);
      System.out.println("成功注销驱动: " + mockDriver.getClass().getName());

    } catch (SQLException e) {
      e.printStackTrace();
    }
  }

  // 辅助测试用的静态内部 Mock 驱动
  private static class MockDriver implements Driver {
    @Override
    public java.sql.Connection connect(String url, java.util.Properties info) { return null; }
    @Override
    public boolean acceptsURL(String url) { return url != null && url.startsWith("jdbc:mock:"); }
    @Override
    public java.sql.DriverPropertyInfo[] getPropertyInfo(String url, java.util.Properties info) { return new java.sql.DriverPropertyInfo[0]; }
    @Override
    public int getMajorVersion() { return 1; }
    @Override
    public int getMinorVersion() { return 0; }
    @Override
    public boolean jdbcCompliant() { return false; }
    @Override
    public java.util.logging.Logger getParentLogger() { return java.util.logging.Logger.getGlobal(); }
  }
}

数据库连接获取 ​

  • Connection DriverManager.getConnection():(String url),URL 获取连接。仅依据包含全部鉴权与参数的 JDBC URL 建立物理连接。

  • Connection DriverManager.getConnection():(String url, String user, String password),凭证获取连接。基于 URL、用户名及明文密码建立物理连接。

  • Connection DriverManager.getConnection():(String url, Properties info),配置属性获取连接。基于 URL 和键值对配置集合建立物理连接。

注意事项:

  1. getConnection() 是同步阻塞操作。若目标数据库网络不通,可能会长时间挂起,务必配合 setLoginTimeout 或驱动专有的 SocketTimeout 属性使用。
  2. getConnection() 每次调用均会通过底层驱动发起一次完整的 TCP 三次握手和数据库认证会话,创建成本极高。生产环境严禁频繁直接调用,必须使用连接池(如 HikariCP、Druid)。
java
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Properties;

public class GetConnectionDemo {
  public static void main(String[] args) {
    String url = "jdbc:h2:mem:demo_get_conn;DB_CLOSE_DELAY=-1";

    // 方式一:直接基于完整 URL 获取连接
    try (Connection conn1 = DriverManager.getConnection(url)) {
      System.out.println("方式一获取连接成功: " + !conn1.isClosed());
    } catch (SQLException e) {
      e.printStackTrace();
    }

    // 方式二:传入用户名与密码获取连接
    try (Connection conn2 = DriverManager.getConnection(url, "sa", "")) {
      System.out.println("方式二获取连接成功: " + !conn2.isClosed());
    } catch (SQLException e) {
      e.printStackTrace();
    }

    // 方式三:通过 Properties 传递详细连接参数
    Properties props = new Properties();
    props.setProperty("user", "sa");
    props.setProperty("password", "");
    props.setProperty("autoCommit", "true");

    try (Connection conn3 = DriverManager.getConnection(url, props)) {
      System.out.println("方式三获取连接成功: " + !conn3.isClosed());
    } catch (SQLException e) {
      e.printStackTrace();
    }
  }
}

日志与超时控制 ​

  • void DriverManager.setLoginTimeout():(int seconds),设置全局登录超时。设定驱动程序尝试连接数据库时的最大等待秒数。

  • int DriverManager.getLoginTimeout():(),获取全局登录超时。返回驱动建立数据库连接的最大超时秒数(默认为 0,代表无限等待或采用驱动默认值)。

  • void DriverManager.setLogWriter():(PrintWriter out),设置日志字符输出流。设置全局 JDBC 追踪日志及驱动调试信息的输出流。

  • PrintWriter DriverManager.getLogWriter():(),获取日志字符输出流。获取当前的全局日志写入器。

  • void DriverManager.println():(String message),打印驱动日志。向当前设置的日志流中输出一条带时间戳的调试信息。

注意事项:

  1. setLoginTimeout 是由具体的底层 JDBC Driver 实现来保证其有效性的。某些不合规的第三方轻量级 Driver 可能会忽略该参数。
  2. setLogWriter 启用后会产生大量同步 I/O 跟踪信息,会对系统吞吐量造成严重衰减,仅用于本地环境排查驱动协议交互与握手问题。
java
import java.io.PrintWriter;
import java.io.StringWriter;
import java.sql.DriverManager;

public class LogAndTimeoutDemo {
  public static void main(String[] args) {
    // 1. 配置并读取登录超时时间
    DriverManager.setLoginTimeout(15);
    int currentTimeout = DriverManager.getLoginTimeout();
    System.out.println("设置的登录超时阈值: " + currentTimeout + " 秒");

    // 2. 配置日志输出流为内存 Writer
    StringWriter logMemoryBuffer = new StringWriter();
    PrintWriter logWriter = new PrintWriter(logMemoryBuffer);
    DriverManager.setLogWriter(logWriter);

    // 3. 输出驱动调试信息
    DriverManager.println("== [JDBC TRACE] 初始化测试上下文 ==");
    DriverManager.println("== [JDBC TRACE] 开始探测目标驱动节点 ==");

    // 4. 检查日志流并打印内容
    if (DriverManager.getLogWriter() != null) {
      DriverManager.getLogWriter().flush();
      System.out.println("捕获到的 DriverManager 日志输出:\n" + logMemoryBuffer.toString());
    }

    // 5. 关闭并重置日志流
    DriverManager.setLogWriter(null);
  }
}

API: Connection ​

语句对象创建 ​

  • Statement createStatement():(),创建普通执行语句。创建一个用于向数据库发送静态 SQL 语句的 Statement 对象。

  • Statement createStatement():(int resultSetType, int resultSetConcurrency),创建指定结果集特性的普通语句。定义游标类型(如仅向前滚动、可双向滚动)及并发修改能力。

  • Statement createStatement():(int resultSetType, int resultSetConcurrency, int resultSetHoldability),创建指定可保持性的普通语句。控制在事务提交时生成的 ResultSet 是否保持打开状态。

  • PreparedStatement prepareStatement():(String sql),创建预编译执行语句。预编译 SQL 骨架并支持以参数占位符(?)安全绑定参数。

  • PreparedStatement prepareStatement():(String sql, int autoGeneratedKeys),创建支持自增主键返回的预编译语句。指定是否允许在执行 INSERT 后取回数据库自动生成的自增键。

  • PreparedStatement prepareStatement():(String sql, int[] columnIndexes),创建指定主键列序号返回的预编译语句。通过返回列的物理索引取回自增主键。

  • PreparedStatement prepareStatement():(String sql, String[] columnNames),创建指定主键列名返回的预编译语句。通过返回列名取回自增主键。

  • CallableStatement prepareCall():(String sql),创建存储过程调用语句。创建一个用于调用数据库底层 Stored Procedure 的 CallableStatement 对象。

注意事项:

  1. 优先使用 prepareStatement(sql) 代替 createStatement()。PreparedStatement 会将 SQL 解析与执行计划缓存在数据库端或驱动端(Server-Side Prepared Statement / Client Cache),同时能彻底杜绝 SQL 注入攻击。
  2. 获取自增主键必须显式传入 Statement.RETURN_GENERATED_KEYS,否则执行 pstmt.getGeneratedKeys() 时底层驱动会抛出 SQLException。
java
import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class StatementCreationDemo {
  private static final String URL = "jdbc:h2:mem:stmt_factory;DB_CLOSE_DELAY=-1";

  public static void main(String[] args) {
    try (Connection conn = DriverManager.getConnection(URL, "sa", "")) {
      // 1. 创建普通静态语句
      try (Statement stmt = conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY)) {
        stmt.execute("CREATE TABLE tb_user (id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(32))");
      }

      // 2. 创建支持自增主键捕获的预编译语句
      String insertSql = "INSERT INTO tb_user (username) VALUES (?)";
      try (PreparedStatement pstmt = conn.prepareStatement(insertSql, Statement.RETURN_GENERATED_KEYS)) {
        pstmt.setString(1, "Developer_Alex");
        pstmt.executeUpdate();

        // 获取自动生成的主键
        try (ResultSet generatedKeys = pstmt.getGeneratedKeys()) {
          if (generatedKeys.next()) {
            System.out.println("成功获取自增主键值: " + generatedKeys.getInt(1));
          }
        }
      }

      // 3. 创建存储过程调用语句
      try (Statement stmt = conn.createStatement()) {
        stmt.execute("CREATE ALIAS GET_USER_COUNT AS $$ int getCount() { return 1; } $$;");
      }
      try (CallableStatement cstmt = conn.prepareCall("{CALL GET_USER_COUNT()}")) {
        try (ResultSet rs = cstmt.executeQuery()) {
          if (rs.next()) {
            System.out.println("存储过程执行返回结果: " + rs.getInt(1));
          }
        }
      }

    } catch (SQLException e) {
      e.printStackTrace();
    }
  }
}

事务与保存点控制 ​

  • void setAutoCommit():(boolean autoCommit),设置自动提交模式。传入 false 开启手动事务,传入 true 开启单语句自动提交。

  • boolean getAutoCommit():(),获取当前自动提交状态。返回当前连接是否处于自动提交模式。

  • void commit():(),提交事务。将自上次提交或回滚以来发生的所有变更永久持久化到数据库。

  • void rollback():(),完整回滚事务。撤销当前事务中执行的所有 SQL 变更并释放数据库锁。

  • void rollback():(Savepoint savepoint),局部回滚至保存点。撤销指定保存点之后发生的所有 SQL 变更,保留保存点之前的变更。

  • Savepoint setSavepoint():(),创建匿名保存点。在当前事务中创建一个由系统分配编号的未命名保存点。

  • Savepoint setSavepoint():(String name),创建具名保存点。在当前事务中创建一个指定名称的保存点。

  • void releaseSavepoint():(Savepoint savepoint),释放保存点。从事务状态机中移除指定的保存点以回收资源。

  • void setTransactionIsolation():(int level),设置事务隔离级别。修改当前会话的事务隔离等级(常量定义在 Connection 接口中)。

  • int getTransactionIsolation():(),获取当前事务隔离级别。返回当前会话正在生效的事务隔离常量整数值。

注意事项:

  1. 在 autoCommit = true 状态下调用 commit()、rollback() 或 setSavepoint() 将直接抛出 SQLException。
  2. 当连接归还给连接池之前,必须确保事务已执行 commit() 或 rollback(),且将 autoCommit 重置为 true,否则该连接上的脏事务状态会污染下一个从连接池借出连接的业务线程(Connection State Leak)。
  3. 一旦调用了全局 commit() 或无参 rollback(),所有此前声明的 Savepoint 将全部自动失效。
java
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Savepoint;
import java.sql.Statement;

public class TransactionControlApiDemo {
  private static final String URL = "jdbc:h2:mem:tx_api_demo;DB_CLOSE_DELAY=-1";

  public static void main(String[] args) {
    try (Connection conn = DriverManager.getConnection(URL, "sa", "")) {
      // 1. 检测并修改隔离级别
      System.out.println("默认隔离级别: " + conn.getTransactionIsolation());
      conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
      System.out.println("修改后隔离级别: " + conn.getTransactionIsolation());

      // 2. 检查并设置自动提交
      System.out.println("默认 autoCommit: " + conn.getAutoCommit());
      conn.setAutoCommit(false);
      System.out.println("设置后 autoCommit: " + conn.getAutoCommit());

      try (Statement stmt = conn.createStatement()) {
        stmt.execute("CREATE TABLE tb_demo (val INT)");
        stmt.executeUpdate("INSERT INTO tb_demo VALUES (10)");

        // 3. 创建具名保存点与匿名保存点
        Savepoint namedSp = conn.setSavepoint("SP_CHECK_1");
        stmt.executeUpdate("INSERT INTO tb_demo VALUES (20)");

        Savepoint anonSp = conn.setSavepoint();
        stmt.executeUpdate("INSERT INTO tb_demo VALUES (30)");

        // 4. 回滚至匿名保存点(撤销 30)
        conn.rollback(anonSp);

        // 5. 释放具名保存点
        conn.releaseSavepoint(namedSp);

        // 6. 最终提交
        conn.commit();
        System.out.println("事务提交完成。");
      } catch (SQLException ex) {
        conn.rollback();
        throw ex;
      } finally {
        // 恢复连接默认环境
        conn.setAutoCommit(true);
      }
    } catch (SQLException e) {
      e.printStackTrace();
    }
  }
}

连接状态与元数据探测 ​

  • void close():(),关闭连接。立即释放当前 Connection 占用的数据库和网络资源。

  • boolean isClosed():(),检测连接是否已显式关闭。判断当前 Connection 实例是否在客户端调用过 close() 方法。

  • boolean isValid():(int timeout),探测物理连接活性。向数据库发送轻量级心跳包,验证底层的物理链路与套接字是否仍真实可用。

  • void abort():(Executor executor),强制终止物理连接。绕过常规清理流程,异步直接切断物理通信会话(用于应对死锁或网络挂死)。

  • DatabaseMetaData getMetaData():(),获取数据库元数据。返回包含底层数据库版本、支持语法、表结构、主外键等综合元数据的对象。

  • void setReadOnly():(boolean readOnly),设置只读提示。向底层驱动发送只读模式优化提示(如路由到读写分离的从库或启用特定的事务优化)。

  • boolean isReadOnly():(),获取只读状态。返回当前连接是否配置为只读。

注意事项:

  1. isClosed() 仅返回 Java 端是否调用了 close()。如果网络网线被拔除、防火墙切断了 TCP 连接或数据库端杀死了进程,isClosed() 依然会返回 false。因此连接池做健康检查时必须使用 isValid(timeout)。
  2. setReadOnly(true) 是一种优化提示(Hint),具体是否在物理层面强制禁止写入取决于数据库厂商驱动的实现(如 MySQL 会在执行写操作时报错,而部分驱动可能仅将其作为内部路由标志)。
java
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.DriverManager;
import java.sql.SQLException;

public class ConnectionStateAndMetaDemo {
  private static final String URL = "jdbc:h2:mem:state_demo;DB_CLOSE_DELAY=-1";

  public static void main(String[] args) {
    try (Connection conn = DriverManager.getConnection(URL, "sa", "")) {
      // 1. 验证物理活性(超时阈值设为 2 秒)
      boolean healthy = conn.isValid(2);
      System.out.println("底层物理链路健康探测状态 (isValid): " + healthy);

      // 2. 设置并检查只读属性
      conn.setReadOnly(true);
      System.out.println("当前连接只读属性 (isReadOnly): " + conn.isReadOnly());

      // 3. 读取数据库核心元数据
      DatabaseMetaData metaData = conn.getMetaData();
      System.out.println("底层数据库名称: " + metaData.getDatabaseProductName());
      System.out.println("底层数据库版本: " + metaData.getDatabaseProductVersion());
      System.out.println("JDBC 驱动主版本: " + metaData.getDriverMajorVersion());

      // 4. 主动调用 close 并检测状态
      conn.close();
      System.out.println("显式调用 close 后的关闭状态 (isClosed): " + conn.isClosed());

    } catch (SQLException e) {
      e.printStackTrace();
    }
  }
}

环境上下文与网络超时 ​

  • void setCatalog():(String catalog),切换当前 Catalog。为支持 Catalog 的数据库(如 MySQL/SQL Server 中的 Database)选择工作空间。

  • String getCatalog():(),获取当前 Catalog。返回当前生效的数据库 Catalog 名称。

  • void setSchema():(String schema),切换当前 Schema。为支持 Schema 的数据库(如 PostgreSQL/Oracle)切换工作模式。

  • String getSchema():(),获取当前 Schema。返回当前生效的模式空间名。

  • void setNetworkTimeout():(Executor executor, int milliseconds),设置网络层通信超时。设定底层套接字等待数据库响应的最大毫秒数。

  • int getNetworkTimeout():(),获取网络层通信超时。获取当前配置的底超时毫秒数。

  • void setClientInfo():(String name, String value),设置客户端会话标签。向数据库会话注入上下文信息(如应用名、操作员 ID,便于 DBA 在慢 SQL 中做链路追踪)。

  • String getClientInfo():(String name),获取客户端会话标签。获取特定名称的会话上下文标签。

注意事项:

  1. setNetworkTimeout 依赖底层 JDBC 驱动对 Socket.setSoTimeout() 的实现。如果传入的 Executor 为 null 或驱动不支持,可能会抛出 SQLFeatureNotSupportedException。
  2. setCatalog 与 setSchema 在不同数据库中的映射定义不同。例如在 MySQL 中 Catalog 对应实际的 Database(如 USE db_name),而在 Oracle/PostgreSQL 中更侧重使用 Schema。
java
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.concurrent.Executors;

public class ContextAndTimeoutDemo {
  private static final String URL = "jdbc:h2:mem:context_demo;DB_CLOSE_DELAY=-1";

  public static void main(String[] args) {
    try (Connection conn = DriverManager.getConnection(URL, "sa", "")) {
      // 1. 设置与获取模式 (Schema) / 目录 (Catalog)
      System.out.println("默认 Catalog: " + conn.getCatalog());
      System.out.println("默认 Schema: " + conn.getSchema());

      conn.setSchema("PUBLIC");
      System.out.println("显式设置后的 Schema: " + conn.getSchema());

      // 2. 配置网络通信超时 (Socket Timeout)
      conn.setNetworkTimeout(Executors.newSingleThreadExecutor(), 5000);
      System.out.println("配置的网络 Socket 超时阈值: " + conn.getNetworkTimeout() + " ms");

      // 3. 注入客户端元数据标签
      conn.setClientInfo("ApplicationName", "OrderPaymentService");
      conn.setClientInfo("ClientUser", "Operator_9527");

      System.out.println("已注入客户端应用标识: " + conn.getClientInfo("ApplicationName"));
      System.out.println("已注入客户端操作员: " + conn.getClientInfo("ClientUser"));

    } catch (SQLException e) {
      e.printStackTrace();
    }
  }
}

高级数据类型与驱动解包 ​

  • Blob createBlob():(),创建二进制大对象。构建一个空的 Blob 实例,用于向数据库写入图片、视频或大型二进制文件。

  • Clob createClob():(),创建字符大对象。构建一个空的 Clob 实例,用于存储大型长文本或 JSON/XML 文档。

  • Array createArrayOf():(String typeName, Object[] elements),创建 SQL 数组类型。将 Java 数组转换为数据库端支持的 Array 结构(如 PostgreSQL 的 TEXT[]、INTEGER[])。

  • Struct createStruct():(String typeName, Object[] attributes),创建 SQL 结构体对象。构建数据库自定义复合类型(User-Defined Type, UDT)。

  • <T> T unwrap():(Class<T> iface),解包原生驱动实现。当连接被连接池代理时,解包获取厂商原始底层连接对象。

  • boolean isWrapperFor():(Class<?> iface),检测是否可解包。判断当前实例是否直接或间接封装了指定的接口或类。

注意事项:

  1. createArrayOf 的第一个参数 typeName 必须使用数据库底层的原生数据类型名称(如 "VARCHAR", "INT"),否则在执行时数据库端会提示类型未定义。
  2. unwrap() 破坏了对 JDBC 标准接口的面向对象抽象,通常仅在必须调用数据库厂商特有功能(如 Oracle 的 XMLType、MySQL 的批量流式传输参数设置)时谨慎使用。
java
import java.sql.Array;
import java.sql.Blob;
import java.sql.Clob;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class AdvancedTypesAndUnwrapDemo {
  private static final String URL = "jdbc:h2:mem:advanced_demo;DB_CLOSE_DELAY=-1";

  public static void main(String[] args) {
    try (Connection conn = DriverManager.getConnection(URL, "sa", "")) {
      // 1. 创建 Clob 并填充字符大文本
      Clob clob = conn.createClob();
      clob.setString(1, "{\"traceId\": \"TX_998877\", \"payload\": \"Large text payload data...\"}");
      System.out.println("成功创建 Clob 长度: " + clob.length() + " 字符");

      // 2. 创建 Blob 并填充二进制数据
      Blob blob = conn.createBlob();
      byte[] binaryData = new byte[]{0x1F, (byte) 0x8B, 0x08, 0x00}; // GZIP 头部特征字节
      blob.setBytes(1, binaryData);
      System.out.println("成功创建 Blob 长度: " + blob.length() + " 字节");

      // 3. 构建 SQL Array 结构
      String[] tags = new String[]{"Java", "JDBC", "Database"};
      Array sqlArray = conn.createArrayOf("VARCHAR", tags);
      System.out.println("成功创建 SQL Array 基类型: " + sqlArray.getBaseTypeName());

      // 4. 驱动解包验证
      if (conn.isWrapperFor(Connection.class)) {
        Connection unwrapped = conn.unwrap(Connection.class);
        System.out.println("Unwrap 目标实例对象: " + unwrapped.getClass().getSimpleName());
      }

    } catch (SQLException e) {
      e.printStackTrace();
    }
  }
}

API: Statement ​

SQL 语句执行与分发 ​

  • boolean execute():(String sql),通用 SQL 执行。向数据库发送任意 SQL 语句。若第一个结果为 ResultSet 则返回 true,若为更新计数或无结果则返回 false。

  • boolean execute():(String sql, int autoGeneratedKeys),通用执行并指定主键捕获。执行 SQL 的同时指定是否返回底层数据库自动生成的主键。

  • ResultSet executeQuery():(String sql),查询 SQL 执行。专门用于执行单条返回单个 ResultSet 的查询语句(如 SELECT)。

  • int executeUpdate():(String sql),更新 SQL 执行。专门用于执行 INSERT、UPDATE、DELETE 等 DML 语句或 CREATE、DROP 等 DDL 语句,返回受影响的行数(DDL 返回 0)。

  • int executeUpdate():(String sql, int autoGeneratedKeys),更新执行并指定主键捕获。执行更新操作并指定是否允许取回自动生成的主键。

  • long executeLargeUpdate():(String sql),大批量更新 SQL 执行。针对影响行数超过 Integer.MAX_VALUE 的海量数据更新场景,以 64 位 long 形式返回受影响的行数。

注意事项:

  1. 严禁使用 executeQuery() 执行 INSERT/UPDATE/DELETE 等 DML 语句或 DDL 语句,否则底层驱动会直接抛出 SQLException: No ResultSet was produced。
  2. 反之,严禁使用 executeUpdate() 执行返回结果集的 SELECT 语句,否则底层驱动会抛出 SQLException: Can not issue executeUpdate() for SELECTs。
  3. 当 SQL 语句类型在运行时完全动态未知(如通用数据库管理工具执行任意控制台 SQL)时,必须使用 execute() 搭配后续的 getResultSet() 和 getUpdateCount() 进行分流处理。
java
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class ExecutionDispatchApiDemo {
  private static final String URL = "jdbc:h2:mem:exec_dispatch_demo;DB_CLOSE_DELAY=-1";

  public static void main(String[] args) {
    try (Connection conn = DriverManager.getConnection(URL, "sa", "");
       Statement stmt = conn.createStatement()) {

      // 1. 使用 execute 执行 DDL 语句(返回 false,因其没有 ResultSet)
      boolean isRs = stmt.execute("CREATE TABLE tb_product (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(64), stock INT)");
      System.out.println("执行 DDL 返回是否包含结果集: " + isRs);

      // 2. 使用 executeUpdate 执行 DML 插入(返回受影响行数)
      int affectedRows = stmt.executeUpdate("INSERT INTO tb_product (name, stock) VALUES ('Laptop', 100)");
      System.out.println("executeUpdate 影响行数: " + affectedRows);

      // 3. 使用 executeLargeUpdate 执行超大行数更新支持
      long largeRows = stmt.executeLargeUpdate("UPDATE tb_product SET stock = stock + 10 WHERE name = 'Laptop'");
      System.out.println("executeLargeUpdate 影响行数: " + largeRows);

      // 4. 使用 executeQuery 执行单查询
      try (ResultSet rs = stmt.executeQuery("SELECT name, stock FROM tb_product WHERE name = 'Laptop'")) {
        if (rs.next()) {
          System.out.println("查询获取数据 -> 名称: " + rs.getString("name") + ",库存: " + rs.getInt("stock"));
        }
      }

    } catch (SQLException e) {
      e.printStackTrace();
    }
  }
}

多结果集与执行控制 ​

  • ResultSet getResultSet():(),获取当前结果集。获取由当前 SQL 执行生成的当前结果集对象;若当前结果为更新计数或无更多结果则返回 null。

  • int getUpdateCount():(),获取当前更新行数。以 int 类型返回当前操作影响的行数。若当前结果为 ResultSet 或已无更多结果,则返回 -1。

  • long getLargeUpdateCount():(),获取大更新行数。以 64 位 long 形式返回当前影响的行数(若为 ResultSet 或无结果则返回 -1)。

  • boolean getMoreResults():(),移动至下一个结果。将处理游标移动至当前 Statement 的下一个结果集或更新计数,并隐式关闭之前通过 getResultSet() 打开的结果集。

  • boolean getMoreResults():(int current),带规则移动至下一个结果。根据传入的指令常量(CLOSE_CURRENT_RESULT、KEEP_CURRENT_RESULT、CLOSE_ALL_RESULTS)决定在推进到下一结果时对已有结果集保留或关闭。

注意事项:

  1. 当调用 execute() 返回 false 时,并不代表执行结束,可能是一个更新计数(Update Count),必须继续调用 getUpdateCount() 判定是否为 -1。
  2. 标准的 JDBC 多结果集遍历必须遵循循环判定法则:只有当 getMoreResults() == false && getUpdateCount() == -1 时,才标志着该 SQL 的所有结果全部排空。
java
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class MultiResultsApiDemo {
  private static final String URL = "jdbc:h2:mem:multi_res_demo;DB_CLOSE_DELAY=-1";

  public static void main(String[] args) {
    try (Connection conn = DriverManager.getConnection(URL, "sa", "");
       Statement stmt = conn.createStatement()) {

      stmt.execute("CREATE TABLE tb_dept (id INT, dname VARCHAR(32));");
      stmt.execute("INSERT INTO tb_dept VALUES (1, 'Tech'), (2, 'HR');");

      // 执行一段复合 SQL 脚本(包含 DML 与 SELECT)
      String compoundSql = "UPDATE tb_dept SET dname = 'R&D' WHERE id = 1; SELECT * FROM tb_dept;";
      boolean isResultSet = stmt.execute(compoundSql);

      int resultIndex = 1;
      while (true) {
        if (isResultSet) {
          // 当前节点为结果集
          try (ResultSet rs = stmt.getResultSet()) {
            System.out.println(">> 结果 [" + resultIndex + "] 为 ResultSet:");
            while (rs.next()) {
              System.out.println("   部门ID: " + rs.getInt("id") + ",名称: " + rs.getString("dname"));
            }
          }
        } else {
          // 当前节点为更新计数或结束
          long updateCount = stmt.getLargeUpdateCount();
          if (updateCount == -1) {
            // 既不是 ResultSet,更新计数也是 -1,代表所有结果处理完毕
            break;
          }
          System.out.println(">> 结果 [" + resultIndex + "] 为更新影响行数: " + updateCount);
        }

        // 推进到下一个结果节点
        isResultSet = stmt.getMoreResults(Statement.CLOSE_CURRENT_RESULT);
        resultIndex++;
      }

    } catch (SQLException e) {
      e.printStackTrace();
    }
  }
}

批处理执行 ​

  • void addBatch():(String sql),追加批处理命令。将指定的静态 SQL 字符串缓存在当前 Statement 内部的批处理命令列表中。

  • void clearBatch():(),清空批处理队列。清空此前通过 addBatch() 积攒的所有 SQL 语句缓存。

  • int[] executeBatch():(),执行批量 SQL。将批处理队列中的所有 SQL 一次性提交到底层数据库执行,返回记录每条语句影响行数的整型数组。

  • long[] executeLargeBatch():(),执行海量批量 SQL。将批处理队列中的所有 SQL 一次性提交执行,以 long[] 形式返回各条语句影响的行数。

注意事项:

  1. Statement.addBatch(sql) 允许混合放入不同的 DML 语句(如同时包含 INSERT、UPDATE、DELETE),但严禁放入返回结果集的 SELECT 语句,否则在调用 executeBatch() 时会抛出 BatchUpdateException。
  2. 若批处理执行中某一条 SQL 发生语法或约束异常,数据库可能终止后续语句执行,也可能继续执行其余语句(取决于底层驱动和数据库方言)。捕获 BatchUpdateException 时,可通过其 getUpdateCounts() 方法定位失败前已成功执行的语句索引。
java
import java.sql.BatchUpdateException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Arrays;

public class BatchExecutionApiDemo {
  private static final String URL = "jdbc:h2:mem:batch_demo;DB_CLOSE_DELAY=-1";

  public static void main(String[] args) {
    try (Connection conn = DriverManager.getConnection(URL, "sa", "");
       Statement stmt = conn.createStatement()) {

      stmt.execute("CREATE TABLE tb_log (id INT PRIMARY KEY, action VARCHAR(64))");

      // 1. 组装批处理命令队列
      stmt.addBatch("INSERT INTO tb_log VALUES (1, 'USER_LOGIN')");
      stmt.addBatch("INSERT INTO tb_log VALUES (2, 'VIEW_PROFILE')");
      stmt.addBatch("UPDATE tb_log SET action = 'USER_LOGOUT' WHERE id = 1");

      // 2. 执行批量提交
      int[] updateCounts = stmt.executeBatch();
      System.out.println("批处理执行完毕,各语句影响行数: " + Arrays.toString(updateCounts));

      // 3. 再次添加并主动清空队列
      stmt.addBatch("INSERT INTO tb_log VALUES (3, 'TEMP_ACTION')");
      stmt.clearBatch();
      System.out.println("批处理命令队列已清空。");

      // 4. 执行包含海量支持的 executeLargeBatch
      stmt.addBatch("INSERT INTO tb_log VALUES (3, 'PAYMENT_SUCCESS')");
      stmt.addBatch("INSERT INTO tb_log VALUES (4, 'ORDER_CREATED')");
      long[] largeCounts = stmt.executeLargeBatch();
      System.out.println("executeLargeBatch 影响行数: " + Arrays.toString(largeCounts));

    } catch (BatchUpdateException bue) {
      System.err.println("批量执行部分失败,已成功条目计数: " + Arrays.toString(bue.getUpdateCounts()));
      bue.printStackTrace();
    } catch (SQLException e) {
      e.printStackTrace();
    }
  }
}

执行超时与资源管控 ​

  • void setQueryTimeout():(int seconds),设置查询超时时间。设定驱动等待 SQL 语句执行完毕的最大秒数;若超时则底层驱动将抛出 SQLTimeoutException。

  • int getQueryTimeout():(),获取当前查询超时时间。返回当前生效的查询超时秒数(0 表示无限制)。

  • void cancel():(),取消正在执行的语句。供其他并发监控线程调用,请求数据库端终止当前正在耗时执行的 SQL。

  • void setMaxRows():(int max),设置最大读取行数。限制由此 Statement 生成的任意 ResultSet 所能容纳的最大记录行数,超出部分将被数据库驱动直接丢弃。

  • int getMaxRows():(),获取最大读取行数。获取当前配置的最大返回行数限制。

  • void setFetchSize():(int rows),设置网络分批抓取行数。向底层驱动提供提示,指定当结果集需要更多数据时每次跨网络往返抓取的行数。

  • int getFetchSize():(),获取网络抓取行数。获取当前配置的单次抓取行数。

  • void setFetchDirection():(int direction),设置结果集读取方向提示。设定游标预期的移动方向(如 ResultSet.FETCH_FORWARD)。

  • int getFetchDirection():(),获取结果集读取方向。获取当前的抓取方向配置。

  • void close():(),立即关闭语句。显式释放当前 Statement 对象占用的数据库端与 Java 端物理资源。

  • boolean isClosed():(),检测关闭状态。返回当前 Statement 实例是否已经处于关闭状态。

  • void closeOnCompletion():(),配置随结果集消费完毕自动关闭。当由此 Statement 关联的所有 ResultSet 均被消费关闭时,底层自动将本 Statement 关闭。

  • boolean isCloseOnCompletion():(),检测自动关闭特性。返回当前 Statement 是否配置了随结果集消费完毕自动关闭特性。

注意事项:

  1. setQueryTimeout 的底层实现因驱动而异。大多数驱动是通过启动一个独立的后台守护计时线程,在超时触发时向数据库发送异步的中断/Kill 包。因此高频短查询中滥用短超时可能会增加线程上下文切换开销。
  2. setMaxRows 与 SQL 语句中的 LIMIT 不同:setMaxRows 是由驱动层在客户端接收数据时进行截断丢弃,数据库端仍然可能扫描并传输了大量数据;而 LIMIT 则是在数据库引擎物理层面减少扫描。因此不能使用 setMaxRows 完全替代 SQL 物理分页。
java
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class ResourceAndTimeoutApiDemo {
  private static final String URL = "jdbc:h2:mem:timeout_ctrl_demo;DB_CLOSE_DELAY=-1";

  public static void main(String[] args) {
    try (Connection conn = DriverManager.getConnection(URL, "sa", "");
       Statement stmt = conn.createStatement()) {

      // 1. 设置执行超时阈值(3 秒)
      stmt.setQueryTimeout(3);
      System.out.println("配置的 SQL 执行超时时间: " + stmt.getQueryTimeout() + " 秒");

      // 2. 设置最大读取行数限制与网络 FetchSize
      stmt.setMaxRows(2);
      stmt.setFetchSize(50);
      stmt.setFetchDirection(ResultSet.FETCH_FORWARD);
      System.out.println("限制最大行数 (MaxRows): " + stmt.getMaxRows() + ",单次抓取行数 (FetchSize): " + stmt.getFetchSize() + ",抓取方向: " + stmt.getFetchDirection());

      // 3. 配置自动随结果集消费完后关闭 (closeOnCompletion)
      stmt.closeOnCompletion();
      System.out.println("是否启用随结果集自动关闭: " + stmt.isCloseOnCompletion());

      stmt.execute("CREATE TABLE tb_metric (val INT)");
      stmt.execute("INSERT INTO tb_metric VALUES (10), (20), (30), (40)");

      // 验证 MaxRows 拦截效果
      ResultSet rs = stmt.executeQuery("SELECT val FROM tb_metric");
      int count = 0;
      while (rs.next()) {
        count++;
        System.out.println("读取记录值: " + rs.getInt("val"));
      }
      System.out.println("在 4 条数据中实际获取到的行数(受 MaxRows=2 限制): " + count);

      // 关闭当前 ResultSet,触发 Statement 的 closeOnCompletion 机制
      rs.close();
      System.out.println("ResultSet 关闭后,Statement 当前关闭状态: " + stmt.isClosed());

    } catch (SQLException e) {
      e.printStackTrace();
    }
  }
}

自增主键与游标属性 ​

  • ResultSet getGeneratedKeys():(),获取自动生成的主键。获取由于执行当前 Statement 的 INSERT 操作而由数据库自动生成的自增主键结果集。

  • int getResultSetType():(),获取结果集游标类型。返回由此 Statement 生成的结果集类型(如 ResultSet.TYPE_FORWARD_ONLY、ResultSet.TYPE_SCROLL_INSENSITIVE)。

  • int getResultSetConcurrency():(),获取结果集并发模式。返回由此 Statement 生成的结果集的并发修改模式(如 ResultSet.CONCUR_READ_ONLY、ResultSet.CONCUR_UPDATABLE)。

  • int getResultSetHoldability():(),获取结果集可保持性。返回当前事务提交时结果集是否保持打开(如 ResultSet.HOLD_CURSORS_OVER_COMMIT 或 CLOSE_CURSORS_AT_COMMIT)。

  • Connection getConnection():(),获取所属连接对象。返回生成当前 Statement 实例的原始 Connection 物理或逻辑会话对象。

注意事项:

  1. 调用 getGeneratedKeys() 之前,在调用 stmt.execute() 或 stmt.executeUpdate() 时,必须显式传入标志位常量 Statement.RETURN_GENERATED_KEYS,否则在获取时会产生空结果集或直接抛出驱动异常。
java
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class GeneratedKeysAndCursorApiDemo {
  private static final String URL = "jdbc:h2:mem:gen_keys_demo;DB_CLOSE_DELAY=-1";

  public static void main(String[] args) {
    try (Connection conn = DriverManager.getConnection(URL, "sa", "");
       Statement stmt = conn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY)) {

      // 1. 检查游标特性及与 Connection 的关联
      System.out.println("结果集游标类型 (Type): " + stmt.getResultSetType());
      System.out.println("结果集并发模式 (Concurrency): " + stmt.getResultSetConcurrency());
      System.out.println("结果集事务保持性 (Holdability): " + stmt.getResultSetHoldability());
      System.out.println("关联的所属连接是否有效: " + !stmt.getConnection().isClosed());

      // 2. 初始化自增主键表
      stmt.execute("CREATE TABLE tb_customer (cust_id INT AUTO_INCREMENT PRIMARY KEY, cust_name VARCHAR(32))");

      // 3. 执行 INSERT 并显式指定开启自增键返回
      String insertSql = "INSERT INTO tb_customer (cust_name) VALUES ('Enterprise_Client_A')";
      int affected = stmt.executeUpdate(insertSql, Statement.RETURN_GENERATED_KEYS);
      System.out.println("插入操作受影响行数: " + affected);

      // 4. 提取生成的自增主键
      try (ResultSet keysRs = stmt.getGeneratedKeys()) {
        if (keysRs.next()) {
          long generatedId = keysRs.getLong(1);
          System.out.println("成功捕获数据库自增生成的 Primary Key: " + generatedId);
        }
      }

    } catch (SQLException e) {
      e.printStackTrace();
    }
  }
}

API: PreparedStatement ​

基础数据类型参数绑定 ​

  • void setInt():(int parameterIndex, int x),设置整型参数。将指定位置的占位符绑定为 Java int 值。

  • void setLong():(int parameterIndex, long x),设置长整型参数。将指定位置的占位符绑定为 Java long 值。

  • void setDouble():(int parameterIndex, double x),设置双精度浮点参数。将指定位置的占位符绑定为 Java double 值。

  • void setFloat():(int parameterIndex, float x),设置单精度浮点参数。将指定位置的占位符绑定为 Java float 值。

  • void setBoolean():(int parameterIndex, boolean x),设置布尔参数。将指定位置的占位符绑定为 Java boolean 值(在底层转换为 BIT/BOOLEAN 或 1/0)。

  • void setString():(int parameterIndex, String x),设置字符串参数。将指定位置的占位符绑定为 Java String 字符序列。

  • void setBigDecimal():(int parameterIndex, BigDecimal x),设置高精度数值参数。将指定位置的占位符绑定为精度不失真的 BigDecimal 对象(映射到 DECIMAL/NUMERIC)。

  • void setNull():(int parameterIndex, int sqlType),设置 SQL NULL 参数。将指定位置的占位符设置为数据库的 NULL 状态,需指定 java.sql.Types 类型常量。

  • void setNull():(int parameterIndex, int sqlType, String typeName),设置自定义类型的 SQL NULL 参数。专门针对用户自定义类型(UDT)或 REF 类型设置 NULL。

注意事项:

  1. JDBC 中参数索引 parameterIndex 以 1 开始计数,传入 0 或超出 ? 数量的索引会直接抛出 SQLException: Parameter index out of range。
  2. 当包装类对象为 null 时(例如 Integer val = null),若直接调用 pstmt.setInt(1, val) 会触发 Java 自动拆箱引发 NullPointerException;此时必须使用 pstmt.setNull(1, java.sql.Types.INTEGER) 或 pstmt.setObject(1, null)。
java
import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Types;

public class BasicParameterBindingDemo {
  private static final String URL = "jdbc:h2:mem:param_basic;DB_CLOSE_DELAY=-1";

  public static void main(String[] args) {
    try (Connection conn = DriverManager.getConnection(URL, "sa", "")) {
      try (Statement stmt = conn.createStatement()) {
        stmt.execute("CREATE TABLE tb_product (" +
            "id INT PRIMARY KEY, " +
            "name VARCHAR(32), " +
            "price DECIMAL(10,2), " +
            "is_active BOOLEAN, " +
            "discount_code VARCHAR(16)" +
            ")");
      }

      String insertSql = "INSERT INTO tb_product VALUES (?, ?, ?, ?, ?)";

      try (PreparedStatement pstmt = conn.prepareStatement(insertSql)) {
        // 1. 绑定基本数据类型
        pstmt.setInt(1, 1001);
        pstmt.setString(2, "Gaming Headset");
        pstmt.setBigDecimal(3, new BigDecimal("299.99"));
        pstmt.setBoolean(4, true);

        // 2. 绑定 SQL NULL 值
        pstmt.setNull(5, Types.VARCHAR);

        int rows = pstmt.executeUpdate();
        System.out.println("基础数据类型参数绑定并插入成功,影响行数: " + rows);
      }

    } catch (SQLException e) {
      e.printStackTrace();
    }
  }
}

时间日期与复杂流式对象绑定 ​

  • void setDate():(int parameterIndex, java.sql.Date x),设置 SQL DATE 参数。将指定位置的占位符绑定为仅包含年月日的日期值。

  • void setTime():(int parameterIndex, java.sql.Time x),设置 SQL TIME 参数。将指定位置的占位符绑定为仅包含时分秒的时间值。

  • void setTimestamp():(int parameterIndex, java.sql.Timestamp x),设置 SQL TIMESTAMP 参数。将指定位置的占位符绑定为包含纳秒精度的完整时间戳。

  • void setObject():(int parameterIndex, Object x),设置通用对象参数。根据 Java 对象的实际运行时类型自动推导并映射为对应的 SQL 数据类型。

  • void setObject():(int parameterIndex, Object x, int targetSqlType),显式指定 SQL 类型设置对象参数。将 Java 对象显式转换为指定的 java.sql.Types 类型后绑定。

  • void setBlob():(int parameterIndex, Blob x),设置 Blob 对象。向指定占位符绑定二进制大对象实例。

  • void setBlob():(int parameterIndex, InputStream inputStream, long length),以流式方式设置 Blob。通过指定长度的输入流高效传输大型二进制数据,避免内存溢出。

  • void setClob():(int parameterIndex, Clob x),设置 Clob 对象。向指定占位符绑定字符大对象实例。

  • void setClob():(int parameterIndex, Reader reader, long length),以流式方式设置 Clob。通过指定字符长度的 Reader 流式传输大文本数据。

  • void setBytes():(int parameterIndex, byte[] x),设置字节数组。将 byte[] 直接写入 VARBINARY/BLOB 字段。

注意事项:

  1. java.util.Date 不能直接强转为 java.sql.Date。正确转换为:new java.sql.Date(utilDate.getTime());若在 Java 8+ 中使用 LocalDateTime,推荐转换为 Timestamp.valueOf(localDateTime) 或直接使用 setObject(index, localDateTime)(JDBC 4.2+ 规范原生支持)。
  2. 使用输入流 setBlob(index, inputStream, length) 时,底层驱动会在执行时读取流数据,请确保在执行前流对象未被提前关闭或耗尽。
java
import java.io.ByteArrayInputStream;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.Date;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;
import java.time.LocalDate;
import java.time.LocalDateTime;

public class ComplexParameterBindingDemo {
  private static final String URL = "jdbc:h2:mem:param_complex;DB_CLOSE_DELAY=-1";

  public static void main(String[] args) {
    try (Connection conn = DriverManager.getConnection(URL, "sa", "")) {
      try (Statement stmt = conn.createStatement()) {
        stmt.execute("CREATE TABLE tb_attachment (" +
            "id INT PRIMARY KEY, " +
            "created_date DATE, " +
            "updated_time TIMESTAMP, " +
            "content_text CLOB, " +
            "file_bin BLOB" +
            ")");
      }

      String sql = "INSERT INTO tb_attachment VALUES (?, ?, ?, ?, ?)";

      try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
        pstmt.setInt(1, 1);

        // 1. 时间日期类型绑定
        pstmt.setDate(2, Date.valueOf(LocalDate.now()));
        pstmt.setTimestamp(3, Timestamp.valueOf(LocalDateTime.now()));

        // 2. 大文本流式绑定 (CLOB)
        String largeText = "{\"module\": \"CORE\", \"payload\": \"Rich text data block...\"}";
        pstmt.setCharacterStream(4, new StringReader(largeText), largeText.length());

        // 3. 二进制流式绑定 (BLOB)
        byte[] rawBinary = "SAMPLE_BINARY_PAYLOAD".getBytes(StandardCharsets.UTF_8);
        pstmt.setBinaryStream(5, new ByteArrayInputStream(rawBinary), rawBinary.length);

        pstmt.executeUpdate();
        System.out.println("时间与流式大对象参数绑定写入成功。");
      }

    } catch (SQLException e) {
      e.printStackTrace();
    }
  }
}

SQL 执行与批处理 ​

  • ResultSet executeQuery():(),无参执行查询。在当前已完成参数绑定的预编译语句上执行 SELECT 操作并返回结果集。

  • int executeUpdate():(),无参执行更新。执行 DML 变更或 DDL 操作,返回受影响的行数。

  • boolean execute():(),无参通用执行。执行任意类型 SQL。若第一个返回对象为 ResultSet 则返回 true。

  • long executeLargeUpdate():(),大行数更新执行。以 long 形式返回变更影响行数,适用于亿级海量数据操作。

  • void addBatch():(),无参添加批处理参数。将当前已绑定的这组参数压入内部批处理队列,以便批量分发。

  • int[] executeBatch():(),提交批处理参数。将内部积攒的多组参数集一次性发送给数据库,返回每组参数影响的行数数组。

  • long[] executeLargeBatch():(),提交大容量批处理参数。执行批量操作并以 long[] 返回各批次的影响计数。

  • void clearParameters():(),清空已绑定参数。清除当前为占位符设定的所有参数值,释放参数持有对象。

  • void clearBatch():(),清空批处理队列。清空此前通过 addBatch() 积攒的所有参数集合。

注意事项:

  1. 严禁在 PreparedStatement 上调用带 SQL 参数的方法(如 pstmt.executeQuery(sql) 或 pstmt.executeUpdate(sql))。此类方法是从 Statement 继承而来的,在 PreparedStatement 实例上调用时多数驱动会直接抛出 SQLException: Can not issue executeQuery(String) on PreparedStatement。
  2. pstmt.addBatch() 是无参方法(与 stmt.addBatch(sql) 不同)。它仅将当前绑定的参数组打包;执行后若要复用并绑定下一组参数,虽不需要强制调用 clearParameters()(新的 setXxx 会覆盖对应槽位),但在部分严格规范的驱动中显式调用 clearParameters() 能提升安全性与可读性。
java
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Arrays;

public class ExecutionAndBatchApiDemo {
  private static final String URL = "jdbc:h2:mem:pstmt_batch;DB_CLOSE_DELAY=-1";

  public static void main(String[] args) {
    try (Connection conn = DriverManager.getConnection(URL, "sa", "")) {
      try (Statement stmt = conn.createStatement()) {
        stmt.execute("CREATE TABLE tb_order (id INT PRIMARY KEY, order_no VARCHAR(32), amount DOUBLE)");
      }

      String insertSql = "INSERT INTO tb_order VALUES (?, ?, ?)";

      // 1. 高性能参数批处理 (Batch Processing)
      try (PreparedStatement pstmt = conn.prepareStatement(insertSql)) {
        for (int i = 1; i <= 3; i++) {
          pstmt.setInt(1, i);
          pstmt.setString(2, "ORD_20260822_" + i);
          pstmt.setDouble(3, 100.0 * i);
          // 将当前参数快照打包压入批处理队列
          pstmt.addBatch();
        }

        int[] batchResults = pstmt.executeBatch();
        System.out.println("批处理执行完成,每批次影响行数: " + Arrays.toString(batchResults));

        // 清空批处理队列
        pstmt.clearBatch();
      }

      // 2. 标准单查询执行
      String querySql = "SELECT order_no, amount FROM tb_order WHERE amount >= ?";
      try (PreparedStatement queryPstmt = conn.prepareStatement(querySql)) {
        queryPstmt.setDouble(1, 200.0);

        try (ResultSet rs = queryPstmt.executeQuery()) {
          while (rs.next()) {
            System.out.println("查询到订单: " + rs.getString("order_no") + ",金额: " + rs.getDouble("amount"));
          }
        }
      }

    } catch (SQLException e) {
      e.printStackTrace();
    }
  }
}

参数元数据与结果集元数据获取 ​

  • ParameterMetaData getParameterMetaData():(),获取参数元数据。返回描述当前预编译语句中各 ? 占位符数量、预期类型及可空性的元数据对象。

  • ResultSetMetaData getMetaData():(),获取结果集元数据。在未执行查询前,预先获取当前 PreparedStatement 预期返回的 ResultSet 列名、类型与结构信息(若驱动支持)。

注意事项:

  1. getParameterMetaData() 的支持程度高度依赖底层驱动和数据库。某些轻量级驱动在调用 getParameterMetaData() 时可能向服务端发起一次额外的探测往返,或在不支持时直接抛出 SQLFeatureNotSupportedException。
java
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ParameterMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;

public class MetaDataInspectionDemo {
  private static final String URL = "jdbc:h2:mem:pstmt_meta;DB_CLOSE_DELAY=-1";

  public static void main(String[] args) {
    try (Connection conn = DriverManager.getConnection(URL, "sa", "")) {
      try (Statement stmt = conn.createStatement()) {
        stmt.execute("CREATE TABLE tb_employee (emp_id INT PRIMARY KEY, emp_name VARCHAR(32), salary DOUBLE)");
      }

      String sql = "SELECT emp_id, emp_name, salary FROM tb_employee WHERE emp_id = ? AND salary > ?";

      try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
        // 1. 探测参数占位符元数据
        ParameterMetaData pmd = pstmt.getParameterMetaData();
        System.out.println("SQL 占位符总数量: " + pmd.getParameterCount());

        // 2. 预先探测查询结果集元数据(尚未执行 executeQuery)
        ResultSetMetaData rsmd = pstmt.getMetaData();
        if (rsmd != null) {
          System.out.println("预期返回列数: " + rsmd.getColumnCount());
          for (int i = 1; i <= rsmd.getColumnCount(); i++) {
            System.out.println("  列 [" + i + "] 名称: " + rsmd.getColumnName(i) + ",类型: " + rsmd.getColumnTypeName(i));
          }
        }
      }

    } catch (SQLException e) {
      e.printStackTrace();
    }
  }
}

API: CallableStatement ​

OUT 参数注册 ​

  • void registerOutParameter():(int parameterIndex, int sqlType),按索引注册标准 OUT 参数。为指定位置的占位符绑定预期的 java.sql.Types 数据库字段类型。

  • void registerOutParameter():(int parameterIndex, int sqlType, int scale),按索引注册带精度数值 OUT 参数。用于 DECIMAL、NUMERIC 等高精度浮点类型,指定小数点右侧保留位数。

  • void registerOutParameter():(int parameterIndex, int sqlType, String typeName),按索引注册自定义/命名数据类型。用于 REF、STRUCT、DISTINCT 或 Oracle 数组/自定义对象类型。

  • void registerOutParameter():(String parameterName, int sqlType),按名称注册标准 OUT 参数。通过存储过程中的形参名称注册输出类型(JDBC 3.0+)。

  • void registerOutParameter():(String parameterName, int sqlType, int scale),按名称注册带精度数值 OUT 参数。通过形参名称注册并约束小数位数。

  • void registerOutParameter():(String parameterName, int sqlType, String typeName),按名称注册自定义数据类型。通过形参名称注册用户自定义结构体或复合类型。

  • void registerOutParameter():(int parameterIndex, SQLType sqlType),基于标准 SQLType 枚举注册。使用 JDBC 4.2 引入的 java.sql.SQLType 标准枚举替代整型常量。

注意事项:

  1. 所有 OUT 和 INOUT 参数必须在调用 execute() 或 executeQuery() 之前完成注册,否则在执行或读取时会抛出 SQLException: Parameter not registered as OUT parameter。
  2. 占位符索引 parameterIndex 从 1 开始计数。对于 {? = call func(?)} 结构,返回值固定占据索引 1。
  3. 基于参数名称(parameterName)注册和传参依赖底层数据库驱动的元数据支持(如 Oracle、SQL Server 支持良好,部分轻量数据库驱动不支持按名称绑定)。
java
import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Types;

public class RegisterOutParameterApiDemo {
  private static final String URL = "jdbc:h2:mem:reg_out_demo;DB_CLOSE_DELAY=-1";

  public static void main(String[] args) {
    try (Connection conn = DriverManager.getConnection(URL, "sa", "")) {
      // 定义包含 OUT 参数的 Java 过程别名
      try (Statement stmt = conn.createStatement()) {
        stmt.execute("CREATE ALIAS GET_SYSTEM_METRICS AS $$ " +
            "void getMetrics(double[] load, String[] status) { " +
            "    load[0] = 0.758; " +
            "    status[0] = \"HEALTHY\"; " +
            "} $$;");
      }

      String procSql = "{call GET_SYSTEM_METRICS(?, ?)}";

      try (CallableStatement cstmt = conn.prepareCall(procSql)) {
        // 1. 注册带标度精度的数值输出参数 (DECIMAL, 2位小数)
        cstmt.registerOutParameter(1, Types.DECIMAL, 2);

        // 2. 注册标准 VARCHAR 输出参数
        cstmt.registerOutParameter(2, Types.VARCHAR);

        // 3. 执行调用
        cstmt.execute();

        System.out.println("指标输出注册与执行成功 -> 负载: " + cstmt.getBigDecimal(1) + ",状态: " + cstmt.getString(2));
      }

    } catch (SQLException e) {
      e.printStackTrace();
    }
  }
}

OUT 参数读取与 NULL 判定 ​

  • int getInt():(int parameterIndex),按索引读取整型输出。获取指定索引位置返回的整型值。

  • int getInt():(String parameterName),按名称读取整型输出。获取指定形参名称返回的整型值。

  • long getLong():(int parameterIndex),按索引读取长整型输出。获取指定索引位置返回的 long 值。

  • double getDouble():(int parameterIndex),按索引读取双精度浮点输出。获取指定索引位置返回的 double 值。

  • String getString():(int parameterIndex),按索引读取字符串输出。获取指定位置返回的 VARCHAR/CHAR/TEXT 文本。

  • String getString():(String parameterName),按名称读取字符串输出。按参数名称获取字符串结果。

  • BigDecimal getBigDecimal():(int parameterIndex),按索引读取高精度数值。获取金融级无损精度的高精度对象。

  • Date getDate():(int parameterIndex),按索引读取 SQL Date 日期。获取年月日日期。

  • Timestamp getTimestamp():(int parameterIndex),按索引读取 Timestamp 时间戳。获取精确到纳秒的时间戳对象。

  • Object getObject():(int parameterIndex),按索引获取通用对象。将底层输出参数转换为对应的 Java 标准包装类型。

  • <T> T getObject():(int parameterIndex, Class<T> type),按泛型类型安全获取对象。指定预期的 Java 目标类型自动进行转换(JDBC 4.1+)。

  • boolean wasNull():(),检测上一个读取的输出参数是否为 SQL NULL。针对基本数据类型(如 int、double 返回 0 时),用于明确判断数据库底层实际返回的是 0 还是 NULL。

注意事项:

  1. 在调用任何 getXxx() 提取输出参数之前,**必须先调用 execute() 或 executeUpdate()**。在未执行或执行失败时读取参数会抛出未定义状态异常。
  2. 基本数据类型 NULL 陷阱:若数据库返回 NULL,调用 getInt(1) 会默认返回 0 而不是抛出异常。必须紧跟调用 cstmt.wasNull() 判定是否为数据库实际的 SQL NULL。
java
import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Types;

public class GetOutParameterApiDemo {
  private static final String URL = "jdbc:h2:mem:get_out_demo;DB_CLOSE_DELAY=-1";

  public static void main(String[] args) {
    try (Connection conn = DriverManager.getConnection(URL, "sa", "")) {
      try (Statement stmt = conn.createStatement()) {
        stmt.execute("CREATE ALIAS FETCH_OPTIONAL_DATA AS $$ " +
            "void fetchData(Integer[] outId, String[] outName) { " +
            "    outId[0] = null; " +             // 模拟返回 SQL NULL
            "    outName[0] = \"Service_Gateway\"; " +
            "} $$;");
      }

      String procSql = "{call FETCH_OPTIONAL_DATA(?, ?)}";

      try (CallableStatement cstmt = conn.prepareCall(procSql)) {
        cstmt.registerOutParameter(1, Types.INTEGER);
        cstmt.registerOutParameter(2, Types.VARCHAR);

        cstmt.execute();

        // 1. 读取基本类型并验证 wasNull
        int returnedId = cstmt.getInt(1);
        if (cstmt.wasNull()) {
          System.out.println("参数 1 读取到的数值为 0,但底层真实状态为 SQL NULL");
        } else {
          System.out.println("参数 1 真实数值为: " + returnedId);
        }

        // 2. 读取字符串输出
        String serviceName = cstmt.getString(2);
        System.out.println("参数 2 读取字符串: " + serviceName);

        // 3. 使用 JDBC 4.1 泛型类型安全提取
        String safeServiceName = cstmt.getObject(2, String.class);
        System.out.println("泛型安全获取: " + safeServiceName);
      }

    } catch (SQLException e) {
      e.printStackTrace();
    }
  }
}

命名参数设置 ​

  • void setInt():(String parameterName, int x),按形参名称设置整型值。向指定形参名的 IN/INOUT 参数传入 int 数据。

  • void setLong():(String parameterName, long x),按形参名称设置长整型值。向指定形参名传入 long 数据。

  • void setDouble():(String parameterName, double x),按形参名称设置浮点值。向指定形参名传入 double 数据。

  • void setString():(String parameterName, String x),按形参名称设置字符串。向指定形参名传入 String 数据。

  • void setBigDecimal():(String parameterName, BigDecimal x),按形参名称设置高精度数值。向指定形参名传入 BigDecimal 对象。

  • void setObject():(String parameterName, Object x),按形参名称设置通用对象。自动推导 Java 对象的对应 SQL 类型并设值。

  • void setNull():(String parameterName, int sqlType),按形参名称设置 SQL NULL。向指定名称的参数传入显式 NULL。

注意事项:

  1. 索引参数与命名参数禁止混用:在同一个 CallableStatement 会话中,对所有参数要么全部使用位置索引(Index),要么全部使用参数名称(Name),混合使用容易引发底层驱动的参数定位混乱并抛出 SQLException。
  2. 命名参数的名称大小写敏感性取决于底层数据库的元数据实现规则(例如 Oracle 默认通常全大写,MySQL 大小写不敏感)。
java
import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Types;

public class NamedParameterApiDemo {
  private static final String URL = "jdbc:h2:mem:named_param_demo;DB_CLOSE_DELAY=-1";

  public static void main(String[] args) {
    try (Connection conn = DriverManager.getConnection(URL, "sa", "")) {
      try (Statement stmt = conn.createStatement()) {
        stmt.execute("CREATE ALIAS CALCULATE_DISCOUNT AS $$ " +
            "double calc(double originalPrice, double discountRate) { " +
            "    return originalPrice * (1.0 - discountRate); " +
            "} $$;");
      }

      String sql = "{? = call CALCULATE_DISCOUNT(?, ?)}";

      try (CallableStatement cstmt = conn.prepareCall(sql)) {
        // 使用位置索引完成标准注册与设值
        cstmt.registerOutParameter(1, Types.DOUBLE);
        cstmt.setDouble(2, 500.0);
        cstmt.setDouble(3, 0.2);

        cstmt.execute();
        System.out.println("计算后折扣价格: " + cstmt.getDouble(1));
      }

    } catch (SQLException e) {
      e.printStackTrace();
    }
  }
}

复杂数据类型与游标提取 ​

  • Blob getBlob():(int parameterIndex),按索引读取 Blob。获取存储过程输出的二进制大对象。

  • Clob getClob():(int parameterIndex),按索引读取 Clob。获取存储过程输出的超长字符大对象。

  • Array getArray():(int parameterIndex),按索引读取 SQL 数组。获取数据库端返回的 Array 结构(如 PostgreSQL 的数组类型)。

  • URL getURL():(int parameterIndex),按索引读取 DATALINK/URL 对象。获取数据库端存储的 URL 定位符。

  • ResultSet getResultSet():(),获取过程隐式返回的结果集。当存储过程内部包含 SELECT 查询并直接向客户端开放游标时,用于捕获其主结果集。

注意事项:

  1. 游标输出(Ref Cursor):在 Oracle 等数据库中,存储过程常通过 OUT 参数返回游标(OracleTypes.CURSOR)。在注册时需注册为 Types.REF_CURSOR(JDBC 4.2+)或驱动专有常量,在读取时使用 cstmt.getObject(index, ResultSet.class) 或强转 (ResultSet) cstmt.getObject(index)。
  2. 存储过程执行完毕并关闭 CallableStatement 时,由其生成的所有 Blob、Clob、ResultSet 均会被级联释放,若需在业务层传递数据,应尽快将其读出并转为 Java 内存对象。
java
import java.sql.CallableStatement;
import java.sql.Clob;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Types;

public class ComplexTypesOutDemo {
  private static final String URL = "jdbc:h2:mem:complex_out_demo;DB_CLOSE_DELAY=-1";

  public static void main(String[] args) {
    try (Connection conn = DriverManager.getConnection(URL, "sa", "")) {
      try (Statement stmt = conn.createStatement()) {
        stmt.execute("CREATE ALIAS GET_CONFIG_DOCUMENT AS $$ " +
            "void getConfig(String[] outClob) { " +
            "    outClob[0] = \"<configuration><cluster>prod-cluster-01</cluster></configuration>\"; " +
            "} $$;");
      }

      String procSql = "{call GET_CONFIG_DOCUMENT(?)}";

      try (CallableStatement cstmt = conn.prepareCall(procSql)) {
        cstmt.registerOutParameter(1, Types.CLOB);
        cstmt.execute();

        // 提取并消费 CLOB 对象
        Clob clob = cstmt.getClob(1);
        if (clob != null) {
          String clobData = clob.getSubString(1, (int) clob.length());
          System.out.println("从存储过程读取到的 CLOB 报文:\n" + clobData);
        }
      }

    } catch (SQLException e) {
      e.printStackTrace();
    }
  }
}

API: ResultSet ​

游标导航与位置控制 ​

  • boolean next():(),游标向下移动一行。将游标从当前位置向下推移一行。若新当前行有效则返回 true;若已无更多行则返回 false。

  • boolean previous():(),游标向上移动一行。将游标向上回退一行。若移动后落在有效行则返回 true(要求可滚动结果集)。

  • boolean first():(),定位到第一行。将游标直接移动至结果集首行(要求可滚动结果集)。

  • boolean last():(),定位到最后一行。将游标直接移动至结果集末行(要求可滚动结果集)。

  • void beforeFirst():(),定位到首行之前。将游标重置至第一行之前的初始位置(要求可滚动结果集)。

  • void afterLast():(),定位到末行之后。将游标移动至最后一行之后的终态位置(要求可滚动结果集)。

  • boolean absolute():(int row),绝对行号定位。正数表示从首行起算的绝对行号(1 为首行),负数表示从末行向前倒数(-1 为末行)。

  • boolean relative():(int rows),相对行号偏移。正数向前移动指定行数,负数向后回退指定行数。

  • int getRow():(),获取当前绝对行号。返回当前游标所在行的物理序号(以 1 为起点)。若游标处于 Before First、After Last 或空结果集,则返回 0。

  • boolean isBeforeFirst():(),检测是否处于首行之前。判断游标是否位于第一行数据之前的起始区域。

  • boolean isAfterLast():(),检测是否处于末行之后。判断游标是否位于最后一行之后的结束区域。

  • boolean isFirst():(),检测是否处于第一行。判断当前游标是否精确停留在首行。

  • boolean isLast():(),检测是否处于最后一行。判断当前游标是否精确停留在末行。

注意事项:

  1. 在 TYPE_FORWARD_ONLY(默认类型)结果集上调用 previous()、first()、last()、absolute()、relative() 等方法会直接抛出 SQLException: Result set is forward-only。
  2. getRow() 在空结果集或游标移出有效行时恒返回 0,不能依赖其返回值是否大于 0 来替代 next() 的有效性判断。
java
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class CursorNavigationApiDemo {
  private static final String URL = "jdbc:h2:mem:nav_api_demo;DB_CLOSE_DELAY=-1";

  public static void main(String[] args) {
    try (Connection conn = DriverManager.getConnection(URL, "sa", "");
       Statement stmt = conn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY)) {

      stmt.execute("CREATE TABLE tb_rank (score INT)");
      stmt.execute("INSERT INTO tb_rank VALUES (100), (90), (80), (70), (60)");

      try (ResultSet rs = stmt.executeQuery("SELECT score FROM tb_rank ORDER BY score DESC")) {
        // 1. 验证初始状态
        System.out.println("初始状态 isBeforeFirst: " + rs.isBeforeFirst());

        // 2. 移动至首行与末行检测
        rs.first();
        System.out.println("当前行: " + rs.getRow() + ", 分数: " + rs.getInt("score") + ", isFirst: " + rs.isFirst());

        rs.last();
        System.out.println("末行: " + rs.getRow() + ", 分数: " + rs.getInt("score") + ", isLast: " + rs.isLast());

        // 3. 绝对定位与相对定位
        rs.absolute(3); // 直接跳至第 3 行
        System.out.println("absolute(3) -> 行号: " + rs.getRow() + ", 分数: " + rs.getInt("score"));

        rs.relative(-1); // 向上相对回退 1 行 -> 定位到第 2 行
        System.out.println("relative(-1) -> 行号: " + rs.getRow() + ", 分数: " + rs.getInt("score"));

        // 4. 定位至末尾之后
        rs.afterLast();
        System.out.println("afterLast 后 isAfterLast: " + rs.isAfterLast());
      }

    } catch (SQLException e) {
      e.printStackTrace();
    }
  }
}

字段数据读取与类型转换 ​

  • int getInt():(int columnIndex),按列索引读取整型。获取指定列的 Java int 值。

  • int getInt():(String columnLabel),按列别名读取整型。获取指定列标签的 Java int 值。

  • long getLong():(int columnIndex),按列索引读取长整型。获取指定列的 Java long 值。

  • double getDouble():(int columnIndex),按列索引读取双精度浮点。获取指定列的 Java double 值。

  • String getString():(int columnIndex),按列索引读取字符串。获取指定列文本数据。

  • String getString():(String columnLabel),按列别名读取字符串。获取指定别名列的文本数据。

  • BigDecimal getBigDecimal():(int columnIndex),按列索引读取高精度数值。获取金融级无损精度的 BigDecimal 对象。

  • boolean getBoolean():(int columnIndex),按列索引读取布尔值。获取布尔字段或将 1/0 转换为 boolean。

  • Date getDate():(int columnIndex),按列索引读取 SQL 日期。获取精确到年月日的 java.sql.Date 对象。

  • Timestamp getTimestamp():(int columnIndex),按列索引读取时间戳。获取精确到纳秒的 java.sql.Timestamp 对象。

  • Object getObject():(int columnIndex),按列索引读取通用对象。自动映射为底层 JDBC 规范对应的 Java 包装类对象。

  • <T> T getObject():(int columnIndex, Class<T> type),按泛型类型安全读取对象。直接转换为指定的 Java 类型(JDBC 4.1+,如 LocalDate.class)。

  • int findColumn():(String columnLabel),查找列标签对应的列索引。返回指定列标签在当前结果集中的 1-based 物理索引。

  • boolean wasNull():(),检测上一次读取的字段是否为 SQL NULL。针对基本数据类型(如 int、double 读取到 0 时),明确区分其值是 0 还是数据库中的 NULL。

注意事项:

  1. 列索引从 1 开始:JDBC 列索引 columnIndex 以 1 为基准,传入 0 会直接抛出 SQLException: Column index out of range。
  2. **columnLabel 优于 columnName**:当 SQL 包含别名(如 SELECT u_name AS username)时,getString("username")使用的是columnLabel。根据 JDBC 规范推荐,应始终优先传入别名。
  3. 基本类型与 wasNull() 陷阱:当数据库字段为 NULL 时,调用 getInt() 会默认返回 0 而非报错。必须在读取后立即调用 rs.wasNull() 判定是否真实为 SQL NULL。
java
import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.Date;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;
import java.time.LocalDate;
import java.time.LocalDateTime;

public class DataExtractionApiDemo {
  private static final String URL = "jdbc:h2:mem:extract_api_demo;DB_CLOSE_DELAY=-1";

  public static void main(String[] args) {
    try (Connection conn = DriverManager.getConnection(URL, "sa", "")) {
      initSchema(conn);

      String querySql = "SELECT id, emp_name AS name, salary, age, is_manager, hire_date, last_login FROM tb_emp";

      try (Statement stmt = conn.createStatement();
         ResultSet rs = stmt.executeQuery(querySql)) {

        while (rs.next()) {
          // 1. 通过 1-based 索引与列别名读取数据
          long id = rs.getLong(1);
          String name = rs.getString("name");
          BigDecimal salary = rs.getBigDecimal("salary");
          boolean isManager = rs.getBoolean("is_manager");
          Date hireDate = rs.getDate("hire_date");
          Timestamp lastLogin = rs.getTimestamp("last_login");

          // 2. 基本类型 NULL 检测 (age 列可能为 null)
          int age = rs.getInt("age");
          Integer boxedAge = rs.wasNull() ? null : age;

          // 3. JDBC 4.1 泛型类型安全提取 Java 8 时间类型
          LocalDate localHireDate = rs.getObject("hire_date", LocalDate.class);

          // 4. 查询列别名对应的列索引
          int salaryColIndex = rs.findColumn("salary");

          System.out.printf("ID: %d | 姓名: %s | 薪资: %s (列索引: %d) | 年龄: %s | 管理员: %b | 入职: %s | 泛型入职: %s | 登录: %s%n",
              id, name, salary, salaryColIndex, boxedAge, isManager, hireDate, localHireDate, lastLogin);
        }
      }

    } catch (SQLException e) {
      e.printStackTrace();
    }
  }

  private static void initSchema(Connection conn) throws SQLException {
    try (Statement stmt = conn.createStatement()) {
      stmt.execute("CREATE TABLE tb_emp (" +
          "id BIGINT PRIMARY KEY, " +
          "emp_name VARCHAR(32), " +
          "salary DECIMAL(10,2), " +
          "age INT, " +
          "is_manager BOOLEAN, " +
          "hire_date DATE, " +
          "last_login TIMESTAMP" +
          ")");
    }
    try (PreparedStatement pstmt = conn.prepareStatement("INSERT INTO tb_emp VALUES (?, ?, ?, ?, ?, ?, ?)")) {
      pstmt.setLong(1, 1001L);
      pstmt.setString(2, "Alex");
      pstmt.setBigDecimal(3, new BigDecimal("18500.50"));
      pstmt.setNull(4, java.sql.Types.INTEGER); // 注入 NULL
      pstmt.setBoolean(5, true);
      pstmt.setDate(6, Date.valueOf(LocalDate.now()));
      pstmt.setTimestamp(7, Timestamp.valueOf(LocalDateTime.now()));
      pstmt.executeUpdate();
    }
  }
}

结果集原地修改与行插入 ​

  • void updateString():(int columnIndex, String x),按索引原地更新字符串字段。将当前行指定列在内存缓冲区的值更新为指定字符串。

  • void updateDouble():(int columnIndex, double x),按索引原地更新浮点字段。更新当前行指定列的 double 值。

  • void updateInt():(int columnIndex, int x),按索引原地更新整型字段。更新当前行指定列的 int 值。

  • void updateNull():(int columnIndex),按索引原地更新字段为 NULL。将当前行指定列置为 SQL NULL。

  • void updateRow():(),提交当前行原地修改。将在此行上调用的所有 updateXxx() 变更同步持久化回写至底层数据库。

  • void deleteRow():(),删除当前行。从结果集和底层物理数据库中同时删除当前游标所在的记录。

  • void moveToInsertRow():(),移动至专属插入缓冲区行。将游标移动到一个新建的暂存行缓冲区,用于构建待插入的新记录。

  • void insertRow():(),执行新行物理插入。将暂存行缓冲区中设置的数据作为新记录物理插入到底层数据库。

  • void moveToCurrentRow():(),从插入缓冲区切回当前行。在完成 insertRow() 之后,将游标还原回插入操作之前所停留在的原始有效行。

  • void cancelRowUpdates():(),撤销当前行未提交的修改。放弃当前行尚未调用 updateRow() 的所有更新。

  • void refreshRow():(),刷新当前行数据。用数据库底层的最新实时数据重新填充当前行。

注意事项:

  1. 原地修改(updateRow、insertRow、deleteRow)必须在创建语句时声明 ResultSet.CONCUR_UPDATABLE。
  2. 查询 SQL 必须包含主键列,且通常不能包含 JOIN、UNION、GROUP BY 或聚合函数,否则驱动无法唯一定位待修改或删除的物理记录,会抛出 SQLException。
java
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class UpdatableResultSetApiDemo {
  private static final String URL = "jdbc:h2:mem:updatable_demo;DB_CLOSE_DELAY=-1";

  public static void main(String[] args) {
    try (Connection conn = DriverManager.getConnection(URL, "sa", "")) {
      initTable(conn);

      // 必须使用 CONCUR_UPDATABLE 模式
      try (Statement stmt = conn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE);
         ResultSet rs = stmt.executeQuery("SELECT id, prod_name, price FROM tb_inventory")) {

        // 1. 原地修改第一行数据
        if (rs.next()) {
          System.out.println("修改前价格: " + rs.getDouble("price"));
          rs.updateDouble("price", 8999.00);
          rs.updateString("prod_name", "Gaming Laptop Pro");
          rs.updateRow(); // 提交物理回写
          System.out.println("已通过 updateRow() 完成物理行持久化更新。");
        }

        // 2. 插入一条新记录
        rs.moveToInsertRow(); // 切换至独立插入暂存缓冲区
        rs.updateInt(1, 201);
        rs.updateString(2, "Mechanical Keyboard RGB");
        rs.updateDouble(3, 499.00);
        rs.insertRow(); // 物理写入
        rs.moveToCurrentRow(); // 游标复位回原始行

        System.out.println("已通过 insertRow() 完成新记录追加插入。");

        // 3. 删除指定行 (定位到第 2 行执行删除)
        if (rs.absolute(2)) {
          System.out.println("即将删除记录: " + rs.getString("prod_name"));
          rs.deleteRow();
          System.out.println("已通过 deleteRow() 完成物理删除。");
        }
      }

    } catch (SQLException e) {
      e.printStackTrace();
    }
  }

  private static void initTable(Connection conn) throws SQLException {
    try (Statement stmt = conn.createStatement()) {
      stmt.execute("CREATE TABLE tb_inventory (id INT PRIMARY KEY, prod_name VARCHAR(64), price DECIMAL(10,2))");
      stmt.execute("INSERT INTO tb_inventory VALUES (101, 'Standard Laptop', 6500.00), (102, 'Old Monitor', 800.00)");
    }
  }
}

流式数据与大对象提取 ​

  • Blob getBlob():(int columnIndex),按索引读取 Blob。获取二进制大对象引用。

  • Blob getBlob():(String columnLabel),按列标签读取 Blob。获取二进制大对象引用。

  • Clob getClob():(int columnIndex),按索引读取 Clob。获取字符大文本大对象引用。

  • InputStream getBinaryStream():(int columnIndex),按索引获取二进制输入流。以 I/O 流方式直接消费超长二进制字节,避免整块载入内存。

  • Reader getCharacterStream():(int columnIndex),按索引获取字符 Reader 流。以 Reader 管道分块流式读取长文本。

  • Array getArray():(int columnIndex),按索引获取 SQL 数组。获取数据库端存储的 Array 结构(如 PostgreSQL 的数组类型)。

注意事项:

  1. getBinaryStream() 或 getCharacterStream() 获取的流对象必须在游标移向下一行(调用 next())之前完整消费完毕。许多 JDBC 驱动在游标推移时会自动释放或关闭上一行的数据流。
  2. Blob 与 Clob 句柄在所属事务或 ResultSet 关闭后可能失效,需要深拷贝或转存为 Java 内存对象。
java
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.Reader;
import java.sql.Array;
import java.sql.Blob;
import java.sql.Clob;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class StreamAndLobApiDemo {
  private static final String URL = "jdbc:h2:mem:lob_api_demo;DB_CLOSE_DELAY=-1";

  public static void main(String[] args) {
    try (Connection conn = DriverManager.getConnection(URL, "sa", "")) {
      initLobData(conn);

      String query = "SELECT doc_clob, data_blob, tags_array FROM tb_docs WHERE id = 1";

      try (Statement stmt = conn.createStatement();
         ResultSet rs = stmt.executeQuery(query)) {

        if (rs.next()) {
          // 1. 基于 Reader 流式消费 CLOB
          try (Reader charReader = rs.getCharacterStream("doc_clob");
             BufferedReader bufferedReader = new BufferedReader(charReader)) {
            System.out.println("CLOB 流式读取第一行: " + bufferedReader.readLine());
          }

          // 2. 基于 InputStream 流式消费 BLOB
          try (InputStream binStream = rs.getBinaryStream("data_blob")) {
            byte[] buffer = new byte[8];
            int bytesRead = binStream.read(buffer);
            System.out.println("BLOB 流式读取字节数: " + bytesRead + ", 首字节: " + buffer[0]);
          }

          // 3. 读取 SQL Array
          Array tagArray = rs.getArray("tags_array");
          if (tagArray != null) {
            Object[] javaArray = (Object[]) tagArray.getArray();
            System.out.println("SQL Array 元素个数: " + javaArray.length + ", 第一个元素: " + javaArray[0]);
          }
        }
      }

    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  private static void initLobData(Connection conn) throws SQLException {
    try (Statement stmt = conn.createStatement()) {
      stmt.execute("CREATE TABLE tb_docs (id INT PRIMARY KEY, doc_clob CLOB, data_blob BLOB, tags_array ARRAY)");
    }
    try (PreparedStatement pstmt = conn.prepareStatement("INSERT INTO tb_docs VALUES (1, ?, ?, ?)")) {
      Clob clob = conn.createClob();
      clob.setString(1, "{\"config\": \"production_v1\", \"max_connections\": 500}");
      pstmt.setClob(1, clob);

      Blob blob = conn.createBlob();
      blob.setBytes(1, new byte[]{0x12, 0x34, 0x56, 0x78});
      pstmt.setBlob(2, blob);

      Array array = conn.createArrayOf("VARCHAR", new Object[]{"Core", "DB", "Infra"});
      pstmt.setArray(3, array);

      pstmt.executeUpdate();
    }
  }
}

元数据探测与状态配置 ​

  • ResultSetMetaData getMetaData():(),获取结果集元数据。返回描述结果集列数、列名、列类型及精度的元数据对象。

  • Statement getStatement():(),获取所属语句对象。返回生成当前 ResultSet 的 Statement 或 PreparedStatement 实例。

  • void setFetchSize():(int rows),设置分批网络抓取行数。向底层驱动提供单次网络往返抓取行数提示。

  • int getFetchSize():(),获取分批网络抓取行数。获取当前配置的抓取行数。

  • void setFetchDirection():(int direction),设置结果集读取方向提示。设定游标预期的推移方向(如 ResultSet.FETCH_FORWARD)。

  • int getFetchDirection():(),获取结果集读取方向。获取当前的抓取方向配置。

  • int getType():(),获取游标类型。返回当前 ResultSet 的滚动特性常量值。

  • int getConcurrency():(),获取并发模式。返回当前 ResultSet 的并发可更新特性常量值。

  • int getHoldability():(),获取事务提交保持性。返回当前 ResultSet 事务提交时的游标保留规则。

  • void close():(),关闭结果集。立即释放底层数据库游标、客户端内存及关联的网络资源。

  • boolean isClosed():(),检测关闭状态。判断当前 ResultSet 是否已被关闭。

注意事项:

  1. getMetaData() 具备强大的反射能力,但频繁对海量小查询调用 getMetaData() 可能会在部分驱动中带来额外的字典解析开销。
  2. 当生成此 ResultSet 的 Statement 被关闭、重新执行另一条 SQL 或所属 Connection 关闭时,当前 ResultSet 将会被驱动隐式自动关闭。
java
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;

public class MetaAndStateApiDemo {
  private static final String URL = "jdbc:h2:mem:meta_api_demo;DB_CLOSE_DELAY=-1";

  public static void main(String[] args) {
    try (Connection conn = DriverManager.getConnection(URL, "sa", "");
       Statement stmt = conn.createStatement()) {

      stmt.execute("CREATE TABLE tb_config (k VARCHAR(32) PRIMARY KEY, v VARCHAR(64))");
      stmt.execute("INSERT INTO tb_config VALUES ('max_retry', '3'), ('timeout_ms', '5000')");

      ResultSet rs = stmt.executeQuery("SELECT k, v FROM tb_config");

      // 1. 配置与读取状态属性
      rs.setFetchSize(50);
      rs.setFetchDirection(ResultSet.FETCH_FORWARD);
      System.out.println("FetchSize: " + rs.getFetchSize() + " | FetchDirection: " + rs.getFetchDirection());
      System.out.println("Type: " + rs.getType() + " | Concurrency: " + rs.getConcurrency() + " | Holdability: " + rs.getHoldability());

      // 2. 利用 ResultSetMetaData 反射解析结果集列信息
      ResultSetMetaData rsmd = rs.getMetaData();
      int columnCount = rsmd.getColumnCount();
      System.out.println("查询返回列总数: " + columnCount);

      for (int i = 1; i <= columnCount; i++) {
        System.out.printf("  列 [%d] 标签: %-12s | 原列名: %-12s | SQL类型: %-10s | 类名: %s%n",
            i,
            rsmd.getColumnLabel(i),
            rsmd.getColumnName(i),
            rsmd.getColumnTypeName(i),
            rsmd.getColumnClassName(i));
      }

      // 3. 验证显式关闭与状态判定
      rs.close();
      System.out.println("显式调用 close() 后 isClosed: " + rs.isClosed());

    } catch (SQLException e) {
      e.printStackTrace();
    }
  }
}