欢迎光临
我们一直在努力

JDBC 技术的使用

一、JDBC架构与驱动详解

1. JDBC架构组成

JDBC API分为两层:

· Application层:开发者编写的Java程序,调用JDBC接口。
· Driver Manager层:DriverManager负责加载并选择合适的数据库驱动。
· Driver层:数据库厂商实现的驱动,负责与数据库通信(通常基于TCP/IP)。

2. 驱动类型深入

类型 名称 工作方式 典型场景 现状
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) {}
}
```

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

```java
try (Connection conn = DriverManager.getConnection(…);
     PreparedStatement pstmt = conn.prepareStatement(sql);
     ResultSet rs = pstmt.executeQuery()) {
    // 使用资源
} // 自动关闭
```

3. 封装工具类 + 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,也会被当作普通字符串值,不会改变语义。

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

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

隔离级别 脏读 不可重复读 幻读 性能
READ_UNCOMMITTED 是 是 是 最好
READ_COMMITTED (Oracle默认) 否 是 是 好
REPEATABLE_READ (MySQL默认) 否 否 是(InnoDB通过间隙锁避免) 中等
SERIALIZABLE 否 否 否 最差

· 脏读:读到未提交的数据。
· 不可重复读:同一事务内两次查询结果不同(因为其他事务更新了数据)。
· 幻读:同一事务内两次范围查询,结果行数不同(其他事务插入或删除了记录)。

设置事务隔离级别

```java
conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
```

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

3. 批处理深度优化

两种批处理方式

方式一: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();
```

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

为什么需要连接池?

· 创建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();
```

5. 调用存储过程(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();
```

6. 处理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分段写入,避免内存溢出。

7. 元数据的实际应用

动态生成简单的查询工具

```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"));
    }
}
```

8. 超时设置

层级 设置方法 作用
驱动连接超时 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使用。

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

· 小批量(每批几百条):自动提交模式(默认)每条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();
```

10. 处理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行
   ```
6. 流式读取:对于超大结果集,可设置useCursorFetch=true和defaultFetchSize,但需结合数据库特性。
7. 尽量使用索引:这是数据库层面的优化,但JDBC执行的SQL也要注意。
8. 关闭自动提交:在事务中合并多个DML操作。
9. 避免在循环中执行SQL:改用批处理或JOIN查询。
10. 正确关闭资源:防止连接泄露。

五、与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, params[i]);
        }
    }
    
    @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);

赞(0)
未经允许不得转载:171主机测评 » JDBC 技术的使用
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址