Java JDBC教程 day01
学习思路:
- 从宏观到微观:先理解JDBC是什么,再学习核心组件,然后掌握具体操作步骤。
- 理论与实践结合:每个知识点都配有清晰的解释和可直接运行的代码示例,方便跟着敲写。
- 问题导向:针对学习中常见的难点和企业开发中的重点(如SQL注入、事务、性能)进行深入讲解。
- 结构化记忆:通过清晰的章节和要点总结,帮助我们构建知识体系,便于记忆。
第一部分:JDBC基础认知 (打好地基)
在学习任何技术之前,首先要明白它是什么(What),为什么要有它(Why)。这有助于我们从根本上理解它的设计思想。JDBC的本质是一个标准,一个规范。
核心概念:
-
JDBC是什么?
- 全称:Java Database Connectivity (Java数据库连接)。
- 本质:是Java官方定义的一套API规范(一组接口),用于让Java应用程序与各种不同的数据库进行交互。
- 重要比喻:想象一下,现在有一个需求,需要给不同国家的人(各种数据库,如MySQL, Oracle, SQL Server)打电话。JDBC就像是提供了一个标准的电话机(Java API),我们只需要学会用这个电话机就行。具体要打给哪个国家的人,就需要安装对应国家的电话线路(数据库驱动)。
-
为什么需要JDBC?
- 统一接口,解耦合:如果没有JDBC,Java代码就需要针对每一种数据库(MySQL, Oracle…)写一套完全不同的连接和操作代码。这会导致代码难以维护和移植。JDBC提供了一套统一的接口,你的Java代码只需要面向JDBC接口编程,而不需要关心底层用的是什么数据库。
- 厂商实现:Java定义了标准,具体的数据库厂商(如MySQL公司)则根据这个标准提供具体的实现类,这个实现类被称为数据库驱动(Driver)。
总结: JDBC是标准,驱动是实现。我们写代码是调用JDBC标准接口,实际运行的是具体数据库的驱动。
第二部分:JDBC核心接口与类 (认识工具)
要使用JDBC,就必须了解它的核心“零件”。这部分是整个JDBC知识体系的基石,我会列出最重要的几个组件,并说明它们各自的职责。
核心组件清单:
-
java.sql.DriverManager
:驱动管理器- 作用:管理数据库驱动。最核心的功能是根据指定的数据库URL,从已注册的驱动中找到合适的驱动,并建立一个到数据库的连接。
- 核心方法:
static Connection getConnection(String url, String user, String password)
-
java.sql.Connection
:数据库连接- 作用:代表一个与数据库的物理连接会话。所有与数据库的交互都是通过
Connection
对象完成的。 - 重要比喻:这是你和数据库之间建立的“通话线路”,后续所有的数据请求和响应都通过这条线路。
- 核心方法:
Statement createStatement()
: 创建一个用于执行静态SQL语句的Statement
对象。PreparedStatement prepareStatement(String sql)
: 创建一个用于执行预编译SQL语句的PreparedStatement
对象(更常用,更安全)。void close()
: 关闭连接,释放资源。void setAutoCommit(boolean autoCommit)
: 设置是否自动提交事务。void commit()
: 提交事务。void rollback()
: 回滚事务。
- 作用:代表一个与数据库的物理连接会话。所有与数据库的交互都是通过
-
java.sql.Statement
:SQL执行器 (基础版)- 作用:用于执行静态的、不带参数的SQL语句,并将结果返回。
- 缺点:存在SQL注入风险,性能也相对较差。在实际开发中很少直接使用。
- 核心方法:
ResultSet executeQuery(String sql)
: 执行查询语句 (SELECT)。int executeUpdate(String sql)
: 执行增、删、改语句 (INSERT, DELETE, UPDATE),返回受影响的行数。boolean execute(String sql)
: 可以执行任何SQL语句。
-
java.sql.PreparedStatement
:SQL执行器 (预编译版)- 作用:
Statement
的子接口,用于执行预编译的、可带参数的SQL语句。 - 优点:
- 防止SQL注入:通过占位符
?
来传递参数,将SQL指令和数据分离,从根本上杜绝了SQL注入的风险。 - 性能更高:SQL语句会预先编译并缓存在数据库中,当多次执行相同结构的SQL时,效率更高。
- 防止SQL注入:通过占位符
- 核心方法:
void setXxx(int parameterIndex, Xxx value)
: 为SQL语句中的占位符?
设置参数。例如setString()
,setInt()
。ResultSet executeQuery()
: 执行查询。int executeUpdate()
: 执行增删改。
- 作用:
-
java.sql.ResultSet
:结果集- 作用:封装了执行查询操作(
executeQuery
)后从数据库返回的数据。 - 重要比喻:可以看作一个指向数据库查询结果表格的行指针。初始时,指针在第一行之前。
- 核心方法:
boolean next()
: 将指针向下移动一行。如果下一行有数据,则返回true
,否则返回false
。这是遍历结果集的标准方式。Xxx getXxx(int columnIndex)
: 根据列的索引(从1开始)获取数据。Xxx getXxx(String columnLabel)
: 根据列的名称获取数据(推荐使用)。void close()
: 关闭结果集,释放资源。
- 作用:封装了执行查询操作(
第三部分:JDBC经典操作六步曲 (动手实践)
这是最重要的实践环节。我将把JDBC的所有操作流程化、标准化,形成一个固定的“套路”。
准备工作:
-
确保我们的Java项目中已经添加了对应数据库的JDBC驱动JAR包(例如,
mysql-connector-java-8.x.x.jar
)。 -
准备好一个数据库,并创建一张测试表,例如:
CREATE TABLE `user` ( `id` int NOT NULL AUTO_INCREMENT, `username` varchar(50) DEFAULT NULL, `password` varchar(50) DEFAULT NULL, PRIMARY KEY (`id`) );
经典六步(以MySQL为例,使用PreparedStatement
):
// 这是一个完整的、可以直接运行的JDBC示例
// 请务必亲手敲一遍,体会每一步的作用
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class JdbcQuickStart {
// 数据库连接信息 (实际开发中应放在配置文件中)
private static final String DB_URL = "jdbc:mysql://localhost:3306/your_database?useSSL=false&serverTimezone=UTC";
private static final String USER = "root";
private static final String PASS = "your_password";
public static void main(String[] args) {
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
// ==================== 步骤 1: 加载数据库驱动 ====================
// 在JDBC 4.0之后,这步可以省略,因为DriverManager可以自动加载classpath中的驱动。
// 但为了理解流程,我们保留它。Class.forName("com.mysql.cj.jdbc.Driver");
System.out.println("步骤1: 驱动加载成功(可省略)");
// ==================== 步骤 2: 获取数据库连接 ====================
System.out.println("步骤2: 正在连接到数据库...");
conn = DriverManager.getConnection(DB_URL, USER, PASS);
System.out.println("数据库连接成功!");
// ==================== 步骤 3: 创建PreparedStatement对象 ====================
// 准备一个带占位符的SQL语句
String sql = "SELECT id, username, password FROM user WHERE id = ?";
System.out.println("步骤3: 准备创建PreparedStatement...");
pstmt = conn.prepareStatement(sql);
// 为占位符设置参数
pstmt.setInt(1, 1); // 查询ID为1的用户
System.out.println("PreparedStatement创建成功!");
// ==================== 步骤 4: 执行SQL语句 ====================
System.out.println("步骤4: 正在执行查询...");
rs = pstmt.executeQuery(); // 执行查询,返回ResultSet
System.out.println("查询执行成功!");
// ==================== 步骤 5: 处理结果集ResultSet ====================
System.out.println("步骤5: 正在处理结果集...");
// 使用while循环和rs.next()遍历结果
while (rs.next()) {
// 通过列名获取数据,推荐!
int id = rs.getInt("id");
String username = rs.getString("username");
String password = rs.getString("password");
System.out.println("--------------------");
System.out.println("查询到用户ID: " + id);
System.out.println("用户名: " + username);
System.out.println("密码: " + password);
System.out.println("--------------------");
}
} catch (SQLException e) {
System.err.println("数据库操作出错!");
e.printStackTrace();
} finally {
// ==================== 步骤 6: 关闭资源 ====================
// 必须在finally块中关闭资源,确保无论是否发生异常都会执行。
// 关闭顺序:先开后关,后开先关 (ResultSet -> PreparedStatement -> Connection)
System.out.println("步骤6: 正在关闭资源...");
try {
if (rs != null) {
rs.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
try {
if (pstmt != null) {
pstmt.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
try {
if (conn != null) {
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
System.out.println("资源已全部关闭。");
}
}
}
第四部分:进阶知识点与最佳实践 (提升能力)
掌握了基础操作后,要成为一个合格的开发者,还必须了解如何写出更安全、更高效、更健壮的代码。这部分内容是面试和实际工作的重点。
1. SQL注入漏洞与PreparedStatement
-
什么是SQL注入?
当应用程序将用户输入的内容直接拼接到SQL语句中时,恶意用户可以输入一段SQL代码来篡改原始SQL的执行逻辑。 -
危险的
Statement
示例:String username = "admin"; String password = "' or '1'='1"; // 恶意输入 String sql = "SELECT * FROM user WHERE username = '" + username + "' AND password = '" + password + "'"; // 拼接后的SQL: SELECT * FROM user WHERE username = 'admin' AND password = '' or '1'='1' // 这个SQL恒为真,导致无需密码即可登录。
-
安全的
PreparedStatement
方案:String sql = "SELECT * FROM user WHERE username = ? AND password = ?"; PreparedStatement pstmt = conn.prepareStatement(sql); pstmt.setString(1, "admin"); pstmt.setString(2, "' or '1'='1"); // 恶意输入 // 数据库会将 ' or '1'='1' 作为一个完整的字符串参数处理,而不是SQL指令。
-
结论:永远优先使用
PreparedStatement
,杜绝使用Statement
进行字符串拼接!
2. 资源释放的优雅方式:try-with-resources
-
传统
finally
块的痛点:代码冗长,需要层层嵌套try-catch
来关闭资源,容易遗漏。 -
Java 7+ 的
try-with-resources
:
编译器会自动为我们生成finally
块并调用资源的close()
方法。代码更简洁、更安全。 -
示例:
public static void betterQuery() { String sql = "SELECT id, username FROM user WHERE id = ?"; // 将需要自动关闭的资源写在try()的括号内 try (Connection conn = DriverManager.getConnection(DB_URL, USER, PASS); PreparedStatement pstmt = conn.prepareStatement(sql)) { pstmt.setInt(1, 1); // ResultSet也应该放在try()中,因为它也需要关闭 try (ResultSet rs = pstmt.executeQuery()) { while (rs.next()) { System.out.println("ID: " + rs.getInt("id") + ", Name: " + rs.getString("username")); } } } catch (SQLException e) { e.printStackTrace(); } // 无需手动编写finally和close() }
-
结论:只要你的JDK版本 >= 7,就应该始终使用
try-with-resources
来管理JDBC资源。
3. 数据库事务 (Transaction)
-
什么是事务?
一组必须要么全部成功,要么全部失败的数据库操作单元。典型的例子是银行转账。 -
事务的四大特性(ACID):
- 原子性 (Atomicity):事务是最小的执行单位,不可分割。
- 一致性 (Consistency):事务执行前后,数据库从一个一致性状态转移到另一个一致性状态。
- 隔离性 (Isolation):多个并发事务之间互不干扰。
- 持久性 (Durability):事务一旦提交,其结果就是永久性的。
-
JDBC中的事务控制:
conn.setAutoCommit(false);
// 开启事务- 执行多个
executeUpdate()
操作… conn.commit();
// 如果所有操作成功,提交事务- 在
catch
块中:conn.rollback();
// 如果任何一步发生异常,回滚事务
-
示例:模拟转账
public static void transferMoney() { Connection conn = null; try { conn = DriverManager.getConnection(DB_URL, USER, PASS); // 1. 开启事务 conn.setAutoCommit(false); // A账户减100 String sql1 = "UPDATE account SET balance = balance - 100 WHERE id = 1"; try(PreparedStatement pstmt1 = conn.prepareStatement(sql1)) { pstmt1.executeUpdate(); } // 模拟发生异常 // if (true) throw new RuntimeException("系统故障!"); // B账户加100 String sql2 = "UPDATE account SET balance = balance + 100 WHERE id = 2"; try(PreparedStatement pstmt2 = conn.prepareStatement(sql2)) { pstmt2.executeUpdate(); } // 3. 提交事务 conn.commit(); System.out.println("转账成功!"); } catch (Exception e) { System.err.println("转账失败,执行回滚!"); // 4. 回滚事务 try { if (conn != null) { conn.rollback(); } } catch (SQLException ex) { ex.printStackTrace(); } e.printStackTrace(); } finally { // 关闭连接等资源 if (conn != null) try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }
4. 批处理 (Batch Processing)
-
场景:当我们需要一次性执行大量结构相同但参数不同的SQL语句时(如批量插入1000条用户数据)。
-
优点:减少网络通信次数,大幅提升性能。
-
步骤:
- 创建
PreparedStatement
。 - 循环调用
pstmt.setXxx()
设置参数。 - 调用
pstmt.addBatch()
将当前参数的SQL添加到批处理中。 - 循环结束后,调用
pstmt.executeBatch()
执行整个批处理。
- 创建
-
示例:
// ... 获取Connection ... String sql = "INSERT INTO user (username, password) VALUES (?, ?)"; try (PreparedStatement pstmt = conn.prepareStatement(sql)) { for (int i = 0; i < 100; i++) { pstmt.setString(1, "user_" + i); pstmt.setString(2, "pass_" + i); pstmt.addBatch(); // 添加到批处理 } int[] results = pstmt.executeBatch(); // 执行批处理 System.out.println("批处理执行完毕,影响行数:" + results.length); } // ... catch and finally ...
5. 连接池 (Connection Pool)
- 为什么需要连接池?
- 数据库连接的创建和销毁是非常耗费系统资源的昂贵操作。
- 在Web应用中,用户请求频繁,如果每次请求都新建一个连接,服务器会很快不堪重负。
- 工作原理:
- 应用启动时,预先创建一定数量的数据库连接,并放入一个“池子”中。
- 当需要连接时,不是新建,而是从池中“借用”一个。
- 使用完毕后,不是关闭,而是“归还”到池中,供其他线程复用。
- 常用连接池技术:
- HikariCP (SpringBoot 2.x 默认,强烈推荐,性能最好)
- Druid (阿里巴巴出品,功能强大,监控完善)
- C3P0, DBCP (较老的连接池技术)
- 注意:在JavaWeb开发中,我们通常不会自己去实现连接池,而是直接使用这些成熟的第三方库。我们只需要学会如何配置它们即可。