JDBC:Java 连接数据库的官方通道
网页上的数据哪来的?存在 MySQL 这类数据库里。JDBC(Java Database Connectivity)是 Java 定的一套"连数据库"的标准接口——不管 MySQL、PostgreSQL、Oracle,只要装了对应驱动,Java 代码写法几乎一样。下面五步走是 JDBC 的固定流程,背下来。
JDBC 第一步:加 MySQL 驱动依赖(Maven)
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.0.33</version> <!-- 以 Maven 中央仓库最新稳定版为准 -->
</dependency>
装完驱动,三件套就齐了:JDK 写代码、Tomcat 跑代码、MySQL 存数据。下面正式开始读代码。
JDBC 五步走:注册驱动 → 连接 → 语句 → 结果集 → 关闭
import java.sql.*;
public class JdbcDemo {
public static void main(String[] args) {
// try-with-resources 自动关 Connection / PreparedStatement / ResultSet
String url = "jdbc:mysql://localhost:3306/testdb?useSSL=false&characterEncoding=utf8";
String sql = "SELECT id, name, score FROM students WHERE name = ?";
// 用 PreparedStatement:? 是占位符,参数用 setXxx 绑定,防 SQL 注入
try (Connection conn = DriverManager.getConnection(url, "root", "你的密码");
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, "小明"); // 第一个 ? 填"小明"
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) { // 一行行往下走
int id = rs.getInt("id");
String name = rs.getString("name");
double score = rs.getDouble("score");
System.out.println(id + " " + name + " " + score);
}
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
论为什么必须用 PreparedStatement
Statement 是把 SQL 字符串拼好直接执行。用户在登录框输入 ' OR '1'='1,拼出来的 SQL 就变成 SELECT * FROM users WHERE name='' OR '1'='1'——恒真,直接登录成功,这就是著名的 SQL 注入。
PreparedStatement 先把 SQL 模板发给数据库预编译,参数用 ? 占位、单独传,数据库自动转义——你输入的引号永远只是"一个字符",不可能改变 SQL 结构。写 JDBC,永远用 PreparedStatement,永远不拼字符串 SQL。
事务:要么全成,要么全不成
转账:A 扣 100,B 加 100。如果 A 扣完、B 还没加,数据库崩了——钱凭空消失。事务(Transaction)就是把这两步打包,要么都成功,失败就一起回滚。
JDBC 事务模板
Connection conn = /* 拿到连接 */;
try {
conn.setAutoCommit(false); // 关掉自动提交,手动控制事务
// ... 执行 A 扣款、B 加款两条 SQL ...
conn.commit(); // 都成功,一起提交
} catch (SQLException e) {
conn.rollback(); // 任何一步出错,整体回滚,假装啥都没发生
throw e;
} finally {
conn.setAutoCommit(true);
}
三种语句对象,选哪个
| 语句对象 | 用途 |
|---|---|
| Statement | 静态 SQL,无参。已被 PreparedStatement 基本淘汰,别用。 |
| PreparedStatement | 预编译 + ? 占位符,防注入、可复用,日常首选。 |
| CallableStatement | 调用数据库存储过程,项目里不常见,知道有它即可。 |
批量插入:别一条一条插,1000 条就慢给你看
批量导入 1 万条数据,循环 1 万次 executeUpdate,每次都和数据库往返一次,能跑到天亮。批处理(addBatch/executeBatch)把一批 SQL 打包一次性发过去,速度差几十倍。
JDBC 批处理模板
try (Connection conn = ds.getConnection();
PreparedStatement ps = conn.prepareStatement(
"INSERT INTO students(id, name) VALUES(?, ?)")) {
conn.setAutoCommit(false); // 批处理配合事务,更快更安全
for (int i = 0; i < 10000; i++) {
ps.setInt(1, i);
ps.setString(2, "学生" + i);
ps.addBatch(); // 攒进批次,不立刻发
if (i % 1000 == 0) {
ps.executeBatch(); // 每攒 1000 条发一次
}
}
ps.executeBatch(); // 把剩下的尾巴发完
conn.commit();
} catch (SQLException e) {
// 出错 conn.rollback(),见上一节
}
| 方法 | 用在哪 |
|---|---|
executeQuery() | SELECT 查询,返回 ResultSet。 |
executeUpdate() | INSERT / UPDATE / DELETE,返回影响行数。 |
executeBatch() | 批量执行一组更新,返回每条影响行数的数组。 |
连接、语句、结果集:谁先关
JDBC 三个资源是"套娃":Connection 开 PreparedStatement,再开 ResultSet。正确关闭顺序是从里到外:先关 ResultSet,再关 Statement,最后关 Connection。但你根本不用记——try-with-resources 按声明的反序自动关,顺序永远正确。手写 finally 的年代已经过去了,别给自己加戏。
完整 DAO 层案例:建表 SQL + HikariCP + PreparedStatement + 事务 + CRUD
-- 第一步:MySQL 里建表
CREATE TABLE students (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL,
score INT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
// 第二步:StudentDao.java —— 完整 DAO
import java.sql.*;
import java.util.*;
import com.zaxxer.hikari.HikariDataSource;
public class StudentDao {
private final HikariDataSource ds;
public StudentDao(HikariDataSource ds) { this.ds = ds; }
// 查全部
public List<Map<String, Object>> findAll() throws SQLException {
String sql = "SELECT id, name, score FROM students ORDER BY id";
try (Connection conn = ds.getConnection();
PreparedStatement ps = conn.prepareStatement(sql);
ResultSet rs = ps.executeQuery()) {
List<Map<String, Object>> list = new ArrayList<>();
while (rs.next()) {
Map<String, Object> row = new HashMap<>();
row.put("id", rs.getInt("id"));
row.put("name", rs.getString("name"));
row.put("score", rs.getInt("score"));
list.add(row);
}
return list;
}
}
// 插入(带返回自增主键)
public int insert(String name, int score) throws SQLException {
String sql = "INSERT INTO students(name, score) VALUES(?, ?)";
try (Connection conn = ds.getConnection();
PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
ps.setString(1, name);
ps.setInt(2, score);
ps.executeUpdate();
// 拿回自增 id
try (ResultSet keys = ps.getGeneratedKeys()) {
return keys.next() ? keys.getInt(1) : -1;
}
}
}
// 事务:同时改两个学生的分数,要么都成要么都回滚
public void transferScore(int fromId, int toId, int delta) throws SQLException {
Connection conn = ds.getConnection();
try {
conn.setAutoCommit(false); // 手动事务
try (PreparedStatement ps1 = conn.prepareStatement(
"UPDATE students SET score = score - ? WHERE id = ?")) {
ps1.setInt(1, delta); ps1.setInt(2, fromId);
ps1.executeUpdate();
}
try (PreparedStatement ps2 = conn.prepareStatement(
"UPDATE students SET score = score + ? WHERE id = ?")) {
ps2.setInt(1, delta); ps2.setInt(2, toId);
ps2.executeUpdate();
}
conn.commit(); // 都成功才提交
} catch (SQLException e) {
conn.rollback(); // 出错全回滚
throw e;
} finally {
conn.setAutoCommit(true);
conn.close(); // close 是还回池子
}
}
}
// 调用示例:
// dao.insert("小明", 92); → 返回 1(自增 id)
// dao.findAll(); → [{id=1, name=小明, score=92}]
// dao.transferScore(1, 2, 10); → 1 号扣 10 分,2 号加 10 分,要么都成要么都不成
JDBC 里 Connection、Statement、ResultSet 都是昂贵资源,用完不关,连接池很快被占满,整个网站卡死。养成习惯:一律 try-with-resources,让编译器替你关。手写 finally close 十个里有九个漏一半。
1.(概念题)为什么必须用 PreparedStatement 而不是 Statement?
查看答案
答案:防 SQL 注入。Statement 拼字符串,用户输入 ' OR '1'='1 能把 SQL 变恒真。PreparedStatement 用 ? 占位符,参数单独传,数据库当数据处理,注入不了。
2.(概念题)JDBC 事务怎么控制?默认是什么?
查看答案
答案:默认自动提交(每条 SQL 立刻 commit)。要手动事务就 conn.setAutoCommit(false),都成功调 commit(),出错调 rollback()。
3.(选择题)JDBC 事务隔离级别有哪几个?默认是?
查看答案
答案:读未提交、读已提交、可重复读、串行化。MySQL InnoDB 默认可重复读(REPEATABLE READ),Oracle 默认读已提交。
4.(概念题)ResultSet、Statement、Connection 谁先关?
查看答案
答案:从里到外:ResultSet → Statement → Connection。但用 try-with-resources 自动按声明反序关,顺序永远正确,不用记。
5.(排错题)插入中文到数据库变成问号?
查看答案
答案:JDBC URL 没带 characterEncoding=utf8,或库表不是 utf8mb4。三处对齐:JDBC URL、库、表。