JDBC从入门到面试:全面掌握Java数据库连接技术
JDBC从入门到面试:全面掌握Java数据库连接技术
引言
Java数据库连接(JDBC)是Java语言中用来规范客户端程序如何访问数据库的应用程序接口(API),它提供了跨数据库的统一访问方式。无论是初学者还是准备面试的开发者,深入理解JDBC都是Java后端开发者的必备技能。本文将带你从JDBC基础概念开始,逐步深入到高级特性和常见面试问题。
一、JDBC基础概念
1.1 什么是JDBC?
JDBC(Java Database Connectivity)是Java提供的一套用于执行SQL语句的Java API,它由一组用Java语言编写的类和接口组成。通过JDBC,开发者可以使用纯Java代码连接各种关系型数据库,并执行数据库操作。
1.2 为什么需要JDBC?
在JDBC出现之前,Java程序访问不同数据库需要使用各自特定的API,这导致了几个问题:
- 代码与特定数据库绑定,移植困难
- 需要学习不同数据库的API
- 维护成本高
JDBC通过提供统一的接口解决了这些问题,实现了"编写一次,随处运行"的数据库访问能力。
1.3 JDBC架构
JDBC架构分为四层:
- 应用程序层:调用JDBC API的Java程序
- JDBC API层:java.sql和javax.sql包中的接口和类
- 驱动程序管理层:DriverManager类
- 数据库驱动程序层:数据库厂商提供的具体实现
二、JDBC核心组件
2.1 DriverManager
DriverManager是JDBC架构中的基本服务,用于管理数据库驱动程序。它负责加载驱动程序并根据连接请求匹配适当的驱动程序。
// 加载驱动(JDBC 4.0以后可自动加载)
Class.forName("com.mysql.cj.jdbc.Driver");// 建立连接
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "username", "password");
2.2 Connection
Connection对象代表与数据库的连接,是JDBC操作的起点。通过Connection可以创建Statement、PreparedStatement和CallableStatement对象。
// 获取连接
Connection connection = DriverManager.getConnection(url, user, password);// 设置事务属性
connection.setAutoCommit(false); // 关闭自动提交
connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); // 设置隔离级别
2.3 Statement
Statement用于执行静态SQL语句并返回结果。适用于执行不带参数的SQL语句。
Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM users");
2.4 PreparedStatement
PreparedStatement是Statement的子接口,用于执行预编译的SQL语句,可以有效防止SQL注入攻击,提高执行效率。
String sql = "INSERT INTO users (name, email) VALUES (?, ?)";
PreparedStatement pstmt = connection.prepareStatement(sql);
pstmt.setString(1, "John Doe");
pstmt.setString(2, "john@example.com");
pstmt.executeUpdate();
2.5 CallableStatement
CallableStatement用于执行数据库存储过程,是PreparedStatement的子接口。
CallableStatement cstmt = connection.prepareCall("{call get_user_details(?, ?)}");
cstmt.setInt(1, userId);
cstmt.registerOutParameter(2, Types.VARCHAR);
cstmt.execute();
String userName = cstmt.getString(2);
2.6 ResultSet
ResultSet表示数据库结果集的数据表,通常通过执行查询数据库的语句生成。它提供了多种方法来获取和操作数据。
ResultSet rs = stmt.executeQuery("SELECT * FROM users");
while (rs.next()) {int id = rs.getInt("id");String name = rs.getString("name");String email = rs.getString("email");System.out.println("ID: " + id + ", Name: " + name + ", Email: " + email);
}
三、JDBC编程步骤
3.1 基本步骤
- 加载并注册数据库驱动
- 建立数据库连接
- 创建Statement对象
- 执行SQL语句
- 处理结果集
- 关闭连接释放资源
3.2 完整示例
import java.sql.*;public class JdbcExample {public static void main(String[] args) {Connection conn = null;Statement stmt = null;ResultSet rs = null;try {// 1. 加载驱动Class.forName("com.mysql.cj.jdbc.Driver");// 2. 建立连接conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "username", "password");// 3. 创建Statementstmt = conn.createStatement();// 4. 执行查询rs = stmt.executeQuery("SELECT id, name, email FROM users");// 5. 处理结果while (rs.next()) {int id = rs.getInt("id");String name = rs.getString("name");String email = rs.getString("email");System.out.println("ID: " + id + ", Name: " + name + ", Email: " + email);}} catch (ClassNotFoundException e) {System.out.println("找不到数据库驱动: " + e.getMessage());} catch (SQLException e) {System.out.println("数据库操作错误: " + e.getMessage());} finally {// 6. 关闭资源try {if (rs != null) rs.close();if (stmt != null) stmt.close();if (conn != null) conn.close();} catch (SQLException e) {System.out.println("关闭资源时发生错误: " + e.getMessage());}}}
}
3.3 使用try-with-resources简化代码
Java 7引入了try-with-resources语句,可以自动关闭资源,使代码更简洁。
try (Connection conn = DriverManager.getConnection(url, user, password);Statement stmt = conn.createStatement();ResultSet rs = stmt.executeQuery("SELECT * FROM users")) {while (rs.next()) {// 处理结果}
} catch (SQLException e) {e.printStackTrace();
}
四、事务处理
4.1 什么是事务?
事务是数据库操作的基本单位,是一系列要么全部成功要么全部失败的操作。事务具有ACID特性:
- 原子性(Atomicity):事务中的所有操作要么全部完成,要么全部不完成
- 一致性(Consistency):事务执行前后数据库状态保持一致
- 隔离性(Isolation):并发事务之间相互隔离
- 持久性(Durability):事务提交后对数据库的修改是永久的
4.2 JDBC事务管理
Connection conn = null;
try {conn = DriverManager.getConnection(url, user, password);// 关闭自动提交conn.setAutoCommit(false);// 执行多个SQL操作Statement stmt1 = conn.createStatement();stmt1.executeUpdate("UPDATE account SET balance = balance - 100 WHERE id = 1");Statement stmt2 = conn.createStatement();stmt2.executeUpdate("UPDATE account SET balance = balance + 100 WHERE id = 2");// 提交事务conn.commit();} catch (SQLException e) {// 回滚事务if (conn != null) {try {conn.rollback();} catch (SQLException ex) {ex.printStackTrace();}}e.printStackTrace();
} finally {// 恢复自动提交并关闭连接if (conn != null) {try {conn.setAutoCommit(true);conn.close();} catch (SQLException e) {e.printStackTrace();}}
}
4.3 保存点(Savepoint)
保存点允许在事务中设置中间点,可以回滚到某个保存点而不是整个事务。
Savepoint savepoint = null;
try {conn.setAutoCommit(false);// 执行一些操作stmt.executeUpdate("INSERT INTO table1 VALUES (...)");// 设置保存点savepoint = conn.setSavepoint("SAVEPOINT_1");// 执行更多操作stmt.executeUpdate("INSERT INTO table2 VALUES (...)");conn.commit();
} catch (SQLException e) {if (savepoint != null) {// 回滚到保存点conn.rollback(savepoint);conn.commit(); // 提交部分事务} else {conn.rollback(); // 回滚整个事务}
}
五、数据库连接池
5.1 为什么需要连接池?
传统的JDBC连接方式存在以下问题:
- 每次操作都需要建立和关闭连接,开销大
- 无法控制连接数量,可能导致系统资源耗尽
- 连接缺乏管理,容易造成连接泄漏
连接池通过预先创建并管理一定数量的数据库连接,解决了这些问题。
5.2 常见连接池
- Apache DBCP:Apache基金会提供的连接池
- C3P0:老牌连接池,稳定但性能一般
- Druid:阿里巴巴开源的高性能连接池,功能全面
- HikariCP:目前性能最好的连接池,Spring Boot 2.x默认连接池
5.3 HikariCP配置示例
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");
config.setUsername("username");
config.setPassword("password");
config.setMaximumPoolSize(20);
config.setMinimumIdle(5);
config.setConnectionTimeout(30000);
config.setIdleTimeout(600000);
config.setMaxLifetime(1800000);HikariDataSource ds = new HikariDataSource(config);// 获取连接
try (Connection conn = ds.getConnection();Statement stmt = conn.createStatement();ResultSet rs = stmt.executeQuery("SELECT * FROM users")) {// 处理结果
}
5.4 连接池关键参数
- maximumPoolSize:连接池最大连接数
- minimumIdle:连接池最小空闲连接数
- connectionTimeout:获取连接的超时时间
- idleTimeout:连接空闲超时时间
- maxLifetime:连接最大存活时间
六、JDBC进阶特性
6.1 批量处理
批量处理可以显著提高大量数据操作的性能。
try (Connection conn = DriverManager.getConnection(url, user, password);PreparedStatement pstmt = conn.prepareStatement("INSERT INTO users (name, email) VALUES (?, ?)")) {conn.setAutoCommit(false);for (int i = 1; i <= 1000; i++) {pstmt.setString(1, "User" + i);pstmt.setString(2, "user" + i + "@example.com");pstmt.addBatch(); // 添加到批处理if (i % 100 == 0) {pstmt.executeBatch(); // 每100条执行一次conn.commit(); // 提交事务}}pstmt.executeBatch(); // 执行剩余的conn.commit();} catch (SQLException e) {e.printStackTrace();
}
6.2 结果集元数据
ResultSetMetaData可以获取结果集的结构信息,如列数、列名、列类型等。
ResultSet rs = stmt.executeQuery("SELECT * FROM users");
ResultSetMetaData rsmd = rs.getMetaData();int columnCount = rsmd.getColumnCount();
for (int i = 1; i <= columnCount; i++) {String columnName = rsmd.getColumnName(i);String columnType = rsmd.getColumnTypeName(i);int columnSize = rsmd.getColumnDisplaySize(i);System.out.println("Column " + i + ": " + columnName + " (" + columnType + ", " + columnSize + ")");
}
6.3 数据库元数据
DatabaseMetaData可以获取数据库的整体信息。
DatabaseMetaData dbmd = conn.getMetaData();System.out.println("Database Product: " + dbmd.getDatabaseProductName());
System.out.println("Database Version: " + dbmd.getDatabaseProductVersion());
System.out.println("Driver Name: " + dbmd.getDriverName());
System.out.println("Driver Version: " + dbmd.getDriverVersion());// 获取所有表
ResultSet tables = dbmd.getTables(null, null, "%", new String[]{"TABLE"});
while (tables.next()) {String tableName = tables.getString("TABLE_NAME");System.out.println("Table: " + tableName);
}
6.4 可滚动和可更新的结果集
JDBC支持可滚动和可更新的结果集,但需要数据库驱动支持。
// 创建可滚动、可更新的Statement
Statement stmt = conn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE
);ResultSet rs = stmt.executeQuery("SELECT * FROM users");// 滚动到第一行
rs.first();// 更新当前行
rs.updateString("name", "New Name");
rs.updateRow();// 插入新行
rs.moveToInsertRow();
rs.updateString("name", "Inserted Name");
rs.updateString("email", "inserted@example.com");
rs.insertRow();
七、JDBC与SQL注入
7.1 什么是SQL注入?
SQL注入是一种代码注入技术,攻击者通过在输入中插入恶意SQL代码,从而执行非预期的数据库操作。
7.2 如何防止SQL注入?
- 使用PreparedStatement:最重要的防御手段
- 输入验证:对所有用户输入进行严格验证
- 最小权限原则:数据库用户只授予必要权限
- 使用ORM框架:如Hibernate、MyBatis等
// 易受SQL注入的写法
String query = "SELECT * FROM users WHERE name = '" + userName + "'";
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(query);// 安全的写法
String query = "SELECT * FROM users WHERE name = ?";
PreparedStatement pstmt = conn.prepareStatement(query);
pstmt.setString(1, userName);
ResultSet rs = pstmt.executeQuery();
八、JDBC常见面试题
8.1 基础问题
-
什么是JDBC?
JDBC是Java数据库连接API,提供了一套用于执行SQL语句的Java接口,使Java程序能够与各种关系型数据库进行交互。 -
JDBC驱动有哪些类型?
- Type 1: JDBC-ODBC桥接驱动
- Type 2: 本地API驱动
- Type 3: 网络协议驱动
- Type 4: 纯Java驱动(最常用)
-
Statement和PreparedStatement的区别?
- Statement用于执行静态SQL,每次执行都需要编译
- PreparedStatement用于执行预编译SQL,性能更好,可防止SQL注入
-
execute、executeQuery和executeUpdate的区别?
- execute: 可执行任何SQL,返回boolean表示是否有ResultSet
- executeQuery: 执行查询,返回ResultSet
- executeUpdate: 执行DML语句,返回受影响的行数
8.2 进阶问题
-
什么是数据库连接池?为什么使用它?
数据库连接池是管理数据库连接的缓存池,通过复用连接减少创建和关闭连接的开销,提高性能。 -
JDBC如何处理事务?
通过Connection的setAutoCommit(false)关闭自动提交,使用commit()提交事务,rollback()回滚事务。 -
ResultSet.TYPE_SCROLL_INSENSITIVE和ResultSet.TYPE_SCROLL_SENSITIVE的区别?
- INSENSITIVE: 结果集不反映底层数据的更改
- SENSITIVE: 结果集反映底层数据的更改
-
JDBC中的批处理是什么?
批处理允许将多个SQL语句分组为一个单元一次性提交到数据库,减少网络通信开销,提高性能。
8.3 实战问题
-
如何优化JDBC性能?
- 使用连接池
- 使用PreparedStatement
- 使用批处理
- 合理设置fetch size
- 及时关闭资源
-
如何处理JDBC中的大数据类型?
使用setBinaryStream()、setCharacterStream()等方法处理BLOB和CLOB类型数据。 -
如何实现JDBC的分页查询?
不同数据库语法不同,MySQL使用LIMIT,Oracle使用ROWNUM,也可以通过ResultSet的absolute()方法实现。
// MySQL分页
String sql = "SELECT * FROM users LIMIT ?, ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, (pageNo - 1) * pageSize); // 起始位置
pstmt.setInt(2, pageSize); // 每页大小
九、总结
JDBC作为Java访问数据库的标准API,是每个Java开发者必须掌握的基础技能。本文从JDBC的基本概念开始,详细介绍了核心组件、编程步骤、事务处理、连接池等关键知识点,并涵盖了常见的面试问题。
随着技术的发展,虽然现在更多使用ORM框架如MyBatis、Hibernate等,但这些框架底层仍然基于JDBC。深入理解JDBC原理和最佳实践,不仅有助于编写高效的数据库访问代码,也是解决复杂数据库问题的关键。
希望本文能帮助你全面掌握JDBC,无论是日常开发还是面试准备,都能游刃有余。