
JDBC是一套由Java语言编写的类和接口,位于java.sql和javax.sql包中,它为多种关系数据库提供了统一的访问方式。数据库厂商根据JDBC规范实现对应的驱动程序,开发者只需按标准API编码,即可屏蔽底层差异。了解原生开发流程,对后续学习连接池、分布式事务等高级特性也大有裨益。下面我们从核心组件开始,逐步拆解完整的开发步骤。
JDBC核心接口与执行流程
JDBC定义了四个最关键的核心接口:Driver、Connection、Statement和ResultSet。其中Driver接口由数据库厂商实现,负责与具体的数据库通信;Connection代表一个数据库连接会话;Statement用于将SQL语句发送到数据库执行;ResultSet则封装了查询返回的结果集。此外,辅助类DriverManager负责管理已注册的驱动,并根据URL选择合适的驱动来建立连接。
在实际操作中,开发人员接触最多的是Connection、Statement和ResultSet。为了预防SQL注入,更推荐使用PreparedStatement(继承自Statement),它能对SQL语句进行预编译并支持参数化查询。整个工作流为:加载驱动 → 获取连接 → 创建执行语句 → 执行SQL并处理结果 → 释放资源。这五个步骤缺一不可,尤其是资源释放环节,若处理不当很容易造成数据库连接泄露,进而拖垮整个应用。
下面是一段完整的原生JDBC查询示例,它遵循了上述流程,并通过try-with-resources自动关闭资源:
public List<User> queryUsers(String keyword) {
String url = "jdbc:mysql://localhost:3306/test?useSSL=false";
String user = "root";
String password = "123456";
String sql = "SELECT id, name, email FROM users WHERE name LIKE ?";
List<User> users = new ArrayList<>();
try (Connection conn = DriverManager.getConnection(url, user, password);
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, "%" + keyword + "%");
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
User u = new User();
u.setId(rs.getInt("id"));
u.setName(rs.getString("name"));
u.setEmail(rs.getString("email"));
users.add(u);
}
}
} catch (SQLException e) {
e.printStackTrace();
}
return users;
}
这段代码从连接建立到结果封装一气呵成,展示了最常用的PreparedStatement模式。接下来我们详细拆解其中的每一步。
原生JDBC开发步骤详解
第一步:加载与注册驱动
在JDBC 4.0之前,开发者必须显式调用Class.forName()来加载驱动类,例如Class.forName("com.mysql.cj.jdbc.Driver")。这会触发驱动类中的静态初始化代码,将自身注册到DriverManager中。从JDBC 4.0开始(随Java 6发布),引入了服务提供者机制(SPI),只要驱动的JAR包中包含了META-INF/services/java.sql.Driver文件,DriverManager就能自动发现并注册驱动,因此显式加载不再是必须的。但为了兼容旧环境或某些非标准驱动,仍建议保留该调用。
驱动注册本质上就是将驱动实例添加到DriverManager内部的registeredDrivers列表中。当调用getConnection()时,DriverManager会遍历该列表,依次询问每个驱动是否能够处理传入的URL,第一个返回成功连接的驱动即被使用。这一机制让程序无需硬编码数据库类型,更换数据库时只需替换驱动JAR和URL即可。
第二步:建立数据库连接
通过DriverManager.getConnection(url, user, password)即可获得一个Connection对象。URL的格式为jdbc:子协议:子名称,例如jdbc:mysql://localhost:3306/dbname。参数可以携带各种配置,如useSSL=false、serverTimezone=Asia/Shanghai等,不同数据库的参数差异较大。
获取到的连接是稀缺资源,每次物理连接的开销都很大,因此生产环境中通常结合连接池(如HikariCP、Druid)来复用连接,但在原生JDBC开发中,为了演示清晰,我们直接创建。务必在操作完成后立即关闭连接,否则会出现连接泄露。Java 7引入的try-with-resources语法能够确保Connection在被使用后自动调用close(),极大简化了资源管理。
第三步:创建Statement或PreparedStatement对象
Statement用于执行静态SQL语句,通常没有参数。但绝大部分场景下,建议使用PreparedStatement,它具备三大优势:一是防止SQL注入,因为参数值会被预编译和转义;二是对于重复执行的SQL语句,预编译能提高性能;三是代码可读性更好。创建方式为conn.prepareStatement(sql),SQL中使用问号占位,随后通过setXxx(index, value)方法设置参数,索引从1开始。
如果需要在插入数据后返回自增主键,可以在创建PreparedStatement时指定Statement.RETURN_GENERATED_KEYS标志,然后通过ps.getGeneratedKeys()获取主键值。
String insertSQL = "INSERT INTO users(name, email) VALUES(?, ?)";
try (Connection conn = DriverManager.getConnection(url, user, password);
PreparedStatement ps = conn.prepareStatement(insertSQL, Statement.RETURN_GENERATED_KEYS)) {
ps.setString(1, "张三");
ps.setString(2, "zhang@ippipp.com");
int affectedRows = ps.executeUpdate();
if (affectedRows > 0) {
try (ResultSet rs = ps.getGeneratedKeys()) {
if (rs.next()) {
long generatedId = rs.getLong(1);
System.out.println("插入成功,主键ID:" + generatedId);
}
}
}
}
第四步:执行SQL语句
Statement接口提供了三种执行方法:executeQuery()用于SELECT查询,返回ResultSet;executeUpdate()用于INSERT、UPDATE、DELETE等DML语句,返回受影响的行数(int);execute()可用于执行任何SQL,返回布尔值表示是否返回ResultSet,然后通过getResultSet()获取结果。日常开发中最常用的是前两种。
执行查询时,必须正确处理ResultSet。它以游标方式指向结果行,初始时游标位于第一行之前,调用next()方法可将游标移动到下一行,并返回布尔值表示是否到达数据集末尾。典型的遍历模式为while(rs.next()),每次循环通过列名或索引取值。注意,列索引也是从1开始,但因为列顺序可能因表结构变化而改变,推荐使用列名获取值,如rs.getString("name")。
第五步:释放资源
资源释放的顺序与创建顺序相反,即先关闭ResultSet,再关闭Statement,最后关闭Connection。每个close操作都应独立进行,避免前面的close抛出异常导致后续资源无法释放。在Java 7之前,需要在finally块中手工关闭,代码十分冗长;采用try-with-resources后,只要资源实现了AutoCloseable接口,便会按照声明的相反顺序自动关闭,代码更加安全和简洁。
需要特别注意的是,ResultSet和Statement的关闭并不会自动关闭底层数据库连接;若使用连接池,Connection的关闭是返还给池中,而非物理断开。如果忘记关闭Statement或ResultSet,可能会影响连接池中连接的回收,逐渐耗尽资源。因此,养成良好的资源管理习惯至关重要。
事务控制与异常处理
默认情况下,JDBC连接处于自动提交模式,每执行一条SQL语句都会立即提交到数据库。为了保持多个操作的原子性,必须手动开启事务。通过conn.setAutoCommit(false)关闭自动提交,此时所有SQL都在同一个事务中,直到调用conn.commit()或conn.rollback()。需要注意的是,事务结束时应当恢复自动提交状态,以避免影响连接复用时的事务行为。
异常处理同样是原生JDBC编程中的重点。所有数据库操作都可能抛出SQLException,其中包含错误码、SQL状态等信息。捕获异常后应执行回滚,并妥善记录日志。以下是一个完整的事务管理示例:
public void transfer(int fromId, int toId, double amount) {
String url = "jdbc:mysql://localhost:3306/test";
String user = "root";
String password = "123456";
Connection conn = null;
try {
conn = DriverManager.getConnection(url, user, password);
conn.setAutoCommit(false);
// 扣款
try (PreparedStatement ps1 = conn.prepareStatement("UPDATE account SET balance = balance - ? WHERE id = ?")) {
ps1.setDouble(1, amount);
ps1.setInt(2, fromId);
ps1.executeUpdate();
}
// 加款
try (PreparedStatement ps2 = conn.prepareStatement("UPDATE account SET balance = balance + ? WHERE id = ?")) {
ps2.setDouble(1, amount);
ps2.setInt(2, toId);
ps2.executeUpdate();
}
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();
}
}
}
}
使用try-with-resources时,由于资源在代码块结束时关闭,而commit/rollback需要在关闭前执行,因此连接不能直接放在try的括号中,必须像上面那样单独管理。另一种做法是使用事务管理工具或AOP进行统一控制,但原生JDBC中的这些基础写法有助于理解事务边界。
通过以上步骤,我们已经完整掌握了原生JDBC的开发流程。虽然在实际项目中很少直接手写这些模板代码(更常用Spring JDBC或ORM框架),但只有理解底层机制,才能在出现性能问题或复杂查询时做出更精准的决策。