JDBC 技术的使用

一、JDBC架构与驱动详解

  1. JDBC架构组成

JDBC API分为两层:

· Application层:开发者编写的Java程序,调用JDBC接口。

· Driver Manager层:DriverManager负责加载并选择合适的数据库驱动。

· Driver层:数据库厂商实现的驱动,负责与数据库通信(通常基于TCP/IP)。

  1. 驱动类型深入

类型 名称 工作方式 典型场景 现状

1 JDBC-ODBC桥 通过ODBC驱动访问数据库,需配置ODBC数据源 老旧系统 已废弃(Java 8移除)

2 本地API驱动 使用厂商客户端库(如OCI),转为本地方法调用 依赖特定平台 较少使用

3 网络纯Java驱动 中间服务器转换为数据库协议 防火墙穿透 很少见

4 纯Java直连驱动 直接使用数据库自有协议(如MySQL的Binlog协议) 几乎所有生产环境 最主流

常见Type 4驱动JAR坐标(Maven):

· MySQL:mysql:mysql-connector-java:8.0.33

· PostgreSQL:org.postgresql:postgresql:42.6.0

· Oracle:com.oracle.database.jdbc:ojdbc8:21.9.0.0

二、详细操作步骤(附原理说明)

步骤0:添加驱动依赖

将驱动JAR放入classpath(传统Web应用放在WEB-INF/lib;Maven项目添加依赖)。

步骤1:注册驱动

· JDBC 3.0以前:必须用Class.forName("驱动类名"),触发静态代码块中的DriverManager.registerDriver()。

· JDBC 4.0+(Java 6+):驱动JAR的META-INF/services/java.sql.Driver文件指定了驱动类名,DriverManager在初始化时会通过ServiceLoader自动加载。因此大多数情况可省略。

注意:某些老旧驱动或类加载器隔离环境(如OSGi)仍需显式注册。

步骤2:获取连接

```java

String url = "jdbc:mysql://host:port/database?参数1=值1&参数2=值2";

Connection conn = DriverManager.getConnection(url, user, password);

```

JDBC URL格式解析(以MySQL为例)

· 协议:jdbc:mysql:

· 主机/端口://localhost:3306/

· 数据库名:mydb

· 常用参数:

· useSSL=false(开发环境) / true(生产应配置证书)

· serverTimezone=UTC(避免时间区错误)

· rewriteBatchedStatements=true(优化批处理性能)

· allowMultiQueries=true(允许一次发送多条SQL,分号分隔)

· connectTimeout=30000(连接超时毫秒)

· socketTimeout=60000(读写超时)

DriverManager 工作原理

· 内部维护一个CopyOnWriteArrayList<DriverInfo>,所有已注册驱动。

· getConnection遍历驱动列表,调用driver.connect(url, info),第一个返回非空Connection即成功。

步骤3:创建Statement对象

接口 创建方式 适用场景 风险/优势

Statement conn.createStatement() 静态SQL,无参数 有SQL注入风险,效率低

PreparedStatement conn.prepareStatement(sql) 带参数的SQL,重复执行 防注入,预编译,推荐

CallableStatement conn.prepareCall("{call proc(?,?)}") 调用存储过程 可获取输出参数

PreparedStatement 预编译真相

· 当调用prepareStatement时,驱动发送SQL模板(带占位符?)到数据库,数据库进行解析、验证、优化、编译,生成执行计划。

· 后续多次调用时,只需发送参数值,数据库直接使用已编译的计划执行,大幅提升性能。

· 注意:某些驱动(如MySQL老版本)默认不开启预编译,需要URL参数useServerPrepStmts=true。

步骤4:执行SQL

执行方法表

方法 返回值 适用于

executeQuery() ResultSet SELECT

executeUpdate() int(影响行数) INSERT/UPDATE/DELETE,以及DDL(如CREATE)

execute() boolean(true表示有结果集) 不确定的SQL,或存储过程返回多个结果集

处理多个结果集(如存储过程)

```java

boolean isResultSet = stmt.execute(sql);

while (true) {

if (isResultSet) {

ResultSet rs = stmt.getResultSet();

// 处理rs

rs.close();

} else {

int updateCount = stmt.getUpdateCount();

if (updateCount == -1) break; // 没有更多结果

// 处理updateCount

}

isResultSet = stmt.getMoreResults(); // 移动到下一个结果集

}

```

步骤5:处理ResultSet

ResultSet核心方法

· 移动游标:next()、previous()、absolute(int)、relative(int)、first()、last()(需创建可滚动结果集)。

· 获取数据:getXxx(int columnIndex)、getXxx(String columnLabel),支持类型转换(如getInt读取VARCHAR数字列)。

· 获取元数据:ResultSetMetaData md = rs.getMetaData();,可获取列数、列名、列类型等。

可滚动、可更新结果集

创建Statement时指定:

```java

Statement stmt = conn.createStatement(

ResultSet.TYPE_SCROLL_INSENSITIVE, // 或 TYPE_SCROLL_SENSITIVE

ResultSet.CONCUR_UPDATABLE // 允许更新

);

ResultSet rs = stmt.executeQuery("SELECT id, name FROM user");

rs.absolute(5); // 移动到第5行

rs.updateString("name", "newName");

rs.updateRow(); // 更新数据库

```

注意:可更新结果集要求查询包含主键列,且不涉及多表连接。实际生产用得不多,推荐直接用UPDATE语句。

步骤6:关闭资源

正确关闭的三种写法

  1. finally块(Java 6及以前)

```java

Connection conn = null;

PreparedStatement pstmt = null;

ResultSet rs = null;

try {

conn = ...;

pstmt = conn.prepareStatement(sql);

rs = pstmt.executeQuery();

} catch (SQLException e) {

// 处理

} finally {

if (rs != null) try { rs.close(); } catch (SQLException e) {}

if (pstmt != null) try { pstmt.close(); } catch (SQLException e) {}

if (conn != null) try { conn.close(); } catch (SQLException e) {}

}

```

  1. try-with-resources(Java 7+,推荐)

```java

try (Connection conn = DriverManager.getConnection(...);

PreparedStatement pstmt = conn.prepareStatement(sql);

ResultSet rs = pstmt.executeQuery()) {

// 使用资源

} // 自动关闭

```

  1. 封装工具类 + ThreadLocal(不推荐,易出错)

三、深入知识点扩展

  1. SQL注入原理与PreparedStatement防护

注入示例:

String sql = "SELECT * FROM user WHERE name = '" + userName + "'";

如果输入' OR '1'='1,最终SQL变成... WHERE name = '' OR '1'='1',永远为真。

PreparedStatement防护机制:

· 参数占位符?发送到数据库时,驱动会转义参数中的特殊字符(如引号、反斜杠)。

· 数据库将参数当作字面值处理,而不是SQL语法的一部分。

· 即使是字符串参数包含OR '1'='1,也会被当作普通字符串值,不会改变语义。

  1. 事务隔离级别与并发问题

4种隔离级别(从低到高)

隔离级别 脏读 不可重复读 幻读 性能

READ_UNCOMMITTED 是 是 是 最好

READ_COMMITTED (Oracle默认) 否 是 是 好

REPEATABLE_READ (MySQL默认) 否 否 是(InnoDB通过间隙锁避免) 中等

SERIALIZABLE 否 否 否 最差

· 脏读:读到未提交的数据。

· 不可重复读:同一事务内两次查询结果不同(因为其他事务更新了数据)。

· 幻读:同一事务内两次范围查询,结果行数不同(其他事务插入或删除了记录)。

设置事务隔离级别

```java

conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);

```

注意:必须在事务开始前(setAutoCommit(true)时或开启事务前)设置。

  1. 批处理深度优化

两种批处理方式

方式一:Statement批处理

```java

Statement stmt = conn.createStatement();

stmt.addBatch("INSERT INTO t VALUES(1)");

stmt.addBatch("UPDATE t SET x=2 WHERE id=1");

int\[\] results = stmt.executeBatch();

```

方式二:PreparedStatement批处理(更常用)

```java

PreparedStatement pstmt = conn.prepareStatement("INSERT INTO log(msg) VALUES(?)");

for (int i = 0; i < 100000; i++) {

pstmt.setString(1, "log " + i);

pstmt.addBatch();

if (i % 1000 == 0) { // 每1000条提交一次,避免内存溢出

pstmt.executeBatch();

pstmt.clearBatch();

}

}

pstmt.executeBatch(); // 剩余

```

MySQL批处理性能关键参数

· rewriteBatchedStatements=true:将多条INSERT重写为一条INSERT INTO ... VALUES (...), (...), ...,性能可提升数十倍。

· useServerPrepStmts=false(默认):客户端预编译,批处理效果差;设为true可启用服务端预编译,但批处理时建议rewriteBatchedStatements为主。

与事务结合

```java

conn.setAutoCommit(false);

// 批处理执行...

conn.commit();

```

  1. 连接池原理与主流实现

为什么需要连接池?

· 创建Connection需要网络握手、认证、分配资源,耗时可达100ms+。

· 高并发下频繁创建关闭连接会严重降低吞吐量。

· 连接池维护一定数量的连接,复用它们,大幅减少延迟。

主流连接池对比

连接池 特点 性能 活跃度

HikariCP 极快,字节码精简,默认Spring Boot 2.x 最高 非常活跃

Druid 阿里开源,支持监控、SQL防火墙、日志 高 活跃

Tomcat JDBC Pool 兼容Tomcat,性能也不错 较高 维护中

C3P0 老牌,稳定但性能较差 低 已过时

HikariCP完整配置示例

```java

HikariConfig config = new HikariConfig();

config.setJdbcUrl("jdbc:mysql://localhost:3306/test");

config.setUsername("root");

config.setPassword("pass");

config.setMaximumPoolSize(10); // 最大连接数

config.setMinimumIdle(5); // 最小空闲连接

config.setConnectionTimeout(30000); // 获取连接超时(ms)

config.setIdleTimeout(600000); // 空闲超时(ms)

config.setMaxLifetime(1800000); // 连接最大生命周期

config.setPoolName("MyHikariPool");

// 可选:连接测试

config.setConnectionTestQuery("SELECT 1");

HikariDataSource dataSource = new HikariDataSource(config);

// 使用时

try (Connection conn = dataSource.getConnection()) {

// ...

}

// 应用关闭时释放池

dataSource.close();

```

  1. 调用存储过程(CallableStatement)

MySQL存储过程示例:

```sql

CREATE PROCEDURE getUser(IN userId INT, OUT userName VARCHAR(50))

BEGIN

SELECT name INTO userName FROM user WHERE id = userId;

END;

```

Java调用:

```java

CallableStatement cstmt = conn.prepareCall("{call getUser(?, ?)}");

cstmt.setInt(1, 100); // 设置输入参数

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

boolean hadResults = cstmt.execute(); // 或 cstmt.executeUpdate()

String name = cstmt.getString(2); // 获取输出参数

// 如果有结果集,用cstmt.getResultSet()获取

cstmt.close();

```

  1. 处理BLOB/CLOB大数据

写入BLOB:

```java

String sql = "INSERT INTO files (name, content) VALUES (?, ?)";

PreparedStatement pstmt = conn.prepareStatement(sql);

pstmt.setString(1, "photo.jpg");

try (InputStream in = new FileInputStream("photo.jpg")) {

pstmt.setBinaryStream(2, in, (int)new File("photo.jpg").length());

pstmt.executeUpdate();

}

```

读取BLOB:

```java

ResultSet rs = stmt.executeQuery("SELECT content FROM files WHERE id=1");

if (rs.next()) {

Blob blob = rs.getBlob("content");

InputStream in = blob.getBinaryStream();

// 写入文件或处理

blob.free(); // 释放BLOB占用的数据库资源

}

```

注意:对于非常大的数据(>10MB),建议使用setBinaryStream分段写入,避免内存溢出。

  1. 元数据的实际应用

动态生成简单的查询工具

```java

public void printTableData(String tableName) throws SQLException {

try (Statement stmt = conn.createStatement();

ResultSet rs = stmt.executeQuery("SELECT * FROM " + tableName)) {

ResultSetMetaData md = rs.getMetaData();

int columnCount = md.getColumnCount();

// 打印表头

for (int i = 1; i <= columnCount; i++) {

System.out.print(md.getColumnName(i) + "\t");

}

System.out.println();

// 打印数据

while (rs.next()) {

for (int i = 1; i <= columnCount; i++) {

System.out.print(rs.getString(i) + "\t");

}

System.out.println();

}

}

}

```

获取数据库所有表

```java

DatabaseMetaData meta = conn.getMetaData();

try (ResultSet rs = meta.getTables(null, null, "%", new String\[\]{"TABLE"})) {

while (rs.next()) {

System.out.println(rs.getString("TABLE_NAME"));

}

}

```

  1. 超时设置

层级 设置方法 作用

驱动连接超时 URL参数connectTimeout=30000 建立TCP连接的超时

驱动socket超时 socketTimeout=60000 执行SQL期间,从socket读取数据的超时

JDBC Statement超时 stmt.setQueryTimeout(10) 执行SQL的最大秒数(秒),超时抛出SQLTimeoutException

事务超时 使用TransactionManager或Spring @Transactional(timeout=10) 整个事务的最大执行时间

注意:setQueryTimeout对某些驱动有bug(如MySQL老版本),推荐结合socketTimeout使用。

  1. 批量操作与事务的权衡

· 小批量(每批几百条):自动提交模式(默认)每条SQL单独提交,性能差,推荐手动提交。

· 大批量(数万条):手动提交并在适当位置commit(),防止事务日志过大。

```java

conn.setAutoCommit(false);

int batchSize = 1000;

for (int i = 0; i < 100000; i++) {

pstmt.setString(1, "value" + i);

pstmt.addBatch();

if (i % batchSize == 0) {

pstmt.executeBatch();

conn.commit();

pstmt.clearBatch();

}

}

pstmt.executeBatch();

conn.commit();

```

  1. 处理JDBC异常

SQLException的子类(JDBC 4.0+)

· SQLNonTransientException:重试不会成功的错误(如SQL语法错误、表不存在)。

· SQLTransientException:临时错误,重试可能成功(如网络超时、死锁)。

· SQLRecoverableException(Oracle特有):连接失效但可通过重建连接恢复。

获取详细错误信息

```java

catch (SQLException e) {

System.err.println("SQLState: " + e.getSQLState());

System.err.println("ErrorCode: " + e.getErrorCode()); // 厂商特定代码

System.err.println("Message: " + e.getMessage());

for (Throwable t : e) { // SQLException支持迭代链

System.err.println("Cause: " + t);

}

}

四、性能调优最佳实践

  1. 使用连接池:避免频繁创建关闭物理连接。

  2. 使用PreparedStatement:预编译和防注入。

  3. 合理设置批处理大小:通常500~2000条/批。

  4. 只查询需要的列:不要SELECT *,减少网络传输和内存。

  5. 使用fetchSize:对于大结果集,设置合理的fetchSize避免一次性加载所有行。

```java

pstmt.setFetchSize(1000); // 每次从数据库抓取1000行

```

  1. 流式读取:对于超大结果集,可设置useCursorFetch=true和defaultFetchSize,但需结合数据库特性。

  2. 尽量使用索引:这是数据库层面的优化,但JDBC执行的SQL也要注意。

  3. 关闭自动提交:在事务中合并多个DML操作。

  4. 避免在循环中执行SQL:改用批处理或JOIN查询。

  5. 正确关闭资源:防止连接泄露。

五、与ORM框架对比及适用场景

特性 原生JDBC MyBatis Hibernate/JPA

控制力 完全控制SQL SQL写XML/注解 自动生成SQL

开发效率 低,需手动映射 中等,半自动 高,对象化操作

学习曲线 较平缓 中等 陡峭(需理解持久化上下文)

性能 最高(无反射开销) 高 中等(有缓存和代理开销)

适用场景 高性能需求、复杂SQL、批量处理 大多数企业应用 快速开发、简单CRUD

建议:

· 学习阶段、性能敏感模块(如批处理、报表)、需要精细控制SQL的场景,使用原生JDBC。

· 一般业务开发,使用MyBatis或JPA,它们底层都依赖JDBC。

六、常见错误及排查

错误现象 可能原因 解决方法

ClassNotFoundException 驱动JAR未在classpath 添加Maven依赖或手动放JAR

No suitable driver found URL格式错误,或驱动未注册 检查URL,JDBC 4.0前需Class.forName

Access denied for user 用户名或密码错误 验证数据库权限

Communications link failure 网络不通、防火墙、数据库未启动 ping数据库端口,检查bind-address

Data truncation 插入数据长度超过字段定义 检查字段长度或启用STRICT_TRANS_TABLES

Lock wait timeout exceeded 事务未提交导致锁等待 检查是否未commit或rollback

ResultSet closed 在Statement/Connection关闭后访问ResultSet 确保ResultSet在使用完毕前不被关闭

Prepared statement already closed 复用已关闭的PreparedStatement 使用连接池时,Statement随Connection归还而关闭,需重新获取

七、JDBC 4.x 新特性

· 自动加载驱动(已提及)。

· try-with-resources支持(Java 7)。

· RowSet:可离线的结果集,支持JavaBeans属性。

· DataSource接口:标准连接池工厂,JNDI绑定。

· QueryTimeout 和 setNetworkTimeout 细粒度超时控制。

· JDBC 4.3(Java 9+):增加了sharding相关支持,以及largeUpdateCount。

八、实战:一个完整的CRUD工具类(简化版)

```java

public class JdbcUtils {

private static HikariDataSource dataSource;

static {

HikariConfig config = new HikariConfig();

config.setJdbcUrl("jdbc:mysql://localhost:3306/db");

config.setUsername("root");

config.setPassword("pass");

config.addDataSourceProperty("cachePrepStmts", "true");

config.addDataSourceProperty("prepStmtCacheSize", "250");

config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");

dataSource = new HikariDataSource(config);

}

public static Connection getConnection() throws SQLException {

return dataSource.getConnection();

}

public static int update(String sql, Object... params) {

try (Connection conn = getConnection();

PreparedStatement pstmt = conn.prepareStatement(sql)) {

setParameters(pstmt, params);

return pstmt.executeUpdate();

} catch (SQLException e) {

throw new RuntimeException(e);

}

}

public static <T> List<T> query(String sql, RowMapper<T> mapper, Object... params) {

List<T> list = new ArrayList<>();

try (Connection conn = getConnection();

PreparedStatement pstmt = conn.prepareStatement(sql)) {

setParameters(pstmt, params);

try (ResultSet rs = pstmt.executeQuery()) {

while (rs.next()) {

list.add(mapper.mapRow(rs));

}

}

} catch (SQLException e) {

throw new RuntimeException(e);

}

return list;

}

private static void setParameters(PreparedStatement pstmt, Object\[\] params) throws SQLException {

for (int i = 0; i < params.length; i++) {

pstmt.setObject(i + 1, paramsi);

}

}

@FunctionalInterface

public interface RowMapper<T> {

T mapRow(ResultSet rs) throws SQLException;

}

}

```

使用示例:

```java

String sql = "SELECT id, name FROM user WHERE age > ?";

List<User> users = JdbcUtils.query(sql, rs -> {

User u = new User();

u.setId(rs.getInt("id"));

u.setName(rs.getString("name"));

return u;

}, 18);

相关推荐
Byte不洛1 小时前
哈希表原理 + 冲突解决 + C++实现
数据结构·c++·算法·哈希算法·散列表
Dillon Dong4 小时前
【风电控制】TI TMS320F28379D 双CPU架构解析与任务分布设计
嵌入式硬件·算法·变流器·风电控制
ps酷教程9 小时前
Jackson 解决没有无参构造函数的反序列化问题
java
NiceCloud喜云9 小时前
Opus 4.8 的 Effort Control 怎么选:Low 到 Max 五档策略
android·java·大数据·前端·c++·python·spring
小羊在睡觉9 小时前
力扣84. 柱状图中最大的矩形
后端·算法·leetcode·golang·go
3DVisionary10 小时前
蓝光三维扫描:医疗制造的精度焦虑怎么解
人工智能·算法·制造·蓝光三维扫描·医疗制造·三维检测·义齿检测
好评笔记10 小时前
机器学习面试八股——常用损失函数
人工智能·深度学习·算法·机器学习·校招
weixin_4684668510 小时前
全局与局部注意力机制新手实战指南
人工智能·python·深度学习·算法·自然语言处理·transformer·注意力机制
_日拱一卒10 小时前
LeetCode:994腐烂的橘子
java·数据结构·算法·leetcode·深度优先