Statement 与 CRUD
本文你会学到:
executeQuery、executeUpdate、execute 三种执行方法的区别
- 通过 Statement 完成基本的查询、插入、删除操作
- SQL 注入风险的成因和利用方式
- 如何通过
RETURN_GENERATED_KEYS 获取自增主键
⚙️ 三种执行方法
执行 SQL 用哪个方法?——三种方式对比
| 方法 |
返回值 |
适用场景 |
executeQuery(sql) |
ResultSet |
仅用于 SELECT 查询 |
executeUpdate(sql) |
int(受影响行数) |
INSERT / UPDATE / DELETE / DDL |
execute(sql) |
boolean(true 表示有 ResultSet) |
不确定 SQL 类型时的通用方法 |
初始化测试数据
| 建表并插入初始数据(@BeforeEach) |
|---|
| /**
* 每个测试前:建表并插入初始数据
*/
@BeforeEach
void setUp() throws SQLException {
conn = DriverManager.getConnection(URL);
try (Statement stmt = conn.createStatement()) {
// 创建 users 表
stmt.execute("CREATE TABLE IF NOT EXISTS users ("
+ "id INT, "
+ "name VARCHAR(50), "
+ "age INT)");
// 插入 3 条初始数据
stmt.executeUpdate("INSERT INTO users VALUES (1, '张三', 25)");
stmt.executeUpdate("INSERT INTO users VALUES (2, '李四', 30)");
stmt.executeUpdate("INSERT INTO users VALUES (3, '王五', 28)");
}
}
|
📝 CRUD 操作实践
如何查询数据?——executeQuery
| executeQuery 查询所有用户 |
|---|
| /**
* 演示 executeQuery 查询所有用户
*/
@Test
void testExecuteQuery() throws SQLException {
try (Statement stmt = conn.createStatement();
// executeQuery 专用于 SELECT 语句,返回 ResultSet
ResultSet rs = stmt.executeQuery("SELECT * FROM users ORDER BY id")) {
int count = 0;
while (rs.next()) {
int id = rs.getInt("id");
String name = rs.getString("name");
int age = rs.getInt("age");
System.out.printf("用户: id=%d, name=%s, age=%d%n", id, name, age);
count++;
}
assertEquals(3, count, "应查询到 3 条用户记录");
}
}
|
如何插入数据?——executeUpdate
| executeUpdate 执行 INSERT |
|---|
| /**
* 演示 executeUpdate 插入一条记录
*/
@Test
void testExecuteUpdateInsert() throws SQLException {
try (Statement stmt = conn.createStatement()) {
// executeUpdate 用于 INSERT/UPDATE/DELETE,返回受影响的行数
int rowsAffected = stmt.executeUpdate(
"INSERT INTO users VALUES (4, '赵六', 35)");
assertEquals(1, rowsAffected, "插入操作应影响 1 行");
System.out.println("插入影响行数: " + rowsAffected);
// 验证插入成功
ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM users");
rs.next();
assertEquals(4, rs.getInt(1), "插入后应有 4 条记录");
}
}
|
如何删除数据?——executeUpdate
| executeUpdate 执行 DELETE |
|---|
| /**
* 演示 executeUpdate 删除记录
*/
@Test
void testExecuteUpdateDelete() throws SQLException {
try (Statement stmt = conn.createStatement()) {
// 删除 age > 27 的用户(李四30岁、王五28岁,共 2 条)
int rowsAffected = stmt.executeUpdate(
"DELETE FROM users WHERE age > 27");
assertEquals(2, rowsAffected, "删除操作应影响 2 行");
System.out.println("删除影响行数: " + rowsAffected);
// 验证只剩 1 条记录
ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM users");
rs.next();
assertEquals(1, rs.getInt(1), "删除后应只剩 1 条记录");
}
}
|
不确定 SQL 类型怎么办?——execute 通用方法
| execute() 处理 SELECT 和 UPDATE 两种情况 |
|---|
| /**
* 演示 execute() 方法,处理返回 ResultSet 或更新计数两种情况
*/
@Test
void testExecuteGeneric() throws SQLException {
try (Statement stmt = conn.createStatement()) {
// execute() 可以执行任意 SQL,返回 boolean:
// true -> 结果是 ResultSet(用 getResultSet() 获取)
// false -> 结果是更新计数(用 getUpdateCount() 获取)
// 情况1:执行 SELECT,返回 true
boolean isResultSet = stmt.execute("SELECT * FROM users");
assertTrue(isResultSet, "SELECT 语句应返回 true(有 ResultSet)");
try (ResultSet rs = stmt.getResultSet()) {
int count = 0;
while (rs.next()) count++;
assertEquals(3, count, "应查询到 3 条记录");
System.out.println("execute SELECT 返回 ResultSet,记录数: " + count);
}
// 情况2:执行 UPDATE,返回 false
boolean isResultSet2 = stmt.execute("UPDATE users SET age = age + 1 WHERE id = 1");
assertFalse(isResultSet2, "UPDATE 语句应返回 false(无 ResultSet)");
int updateCount = stmt.getUpdateCount();
assertEquals(1, updateCount, "更新操作应影响 1 行");
System.out.println("execute UPDATE 返回更新计数: " + updateCount);
}
}
|
🛡️ 安全与高级特性
SQL 注入风险演示
SQL 注入危险
使用 Statement 拼接用户输入会导致严重的安全漏洞。攻击者可通过精心构造的输入绕过认证、读取全表数据,甚至删除数据库。
| SQL 注入风险演示——直接拼接用户输入 |
|---|
| /**
* 演示 SQL 注入风险:用字符串拼接构造恶意查询
* 这就是为什么需要 PreparedStatement 的原因!
*/
@Test
void testSqlInjectionDemo() throws SQLException {
try (Statement stmt = conn.createStatement()) {
// 模拟用户输入(恶意输入)
String maliciousInput = "' OR '1'='1";
// ⚠️ 危险:直接拼接用户输入到 SQL 中
String sql = "SELECT * FROM users WHERE name = '" + maliciousInput + "'";
System.out.println("拼接后的 SQL: " + sql);
// 实际执行的 SQL: SELECT * FROM users WHERE name = '' OR '1'='1'
// 条件 '1'='1' 恒为 true,导致查出所有记录
ResultSet rs = stmt.executeQuery(sql);
int count = 0;
while (rs.next()) {
count++;
}
// 注入成功:本应查不到数据,却查到了所有 3 条记录
assertEquals(3, count, "SQL 注入导致查到了所有记录(应该 0 条,实际 3 条)");
System.out.println("⚠️ SQL 注入成功!查到了 " + count + " 条记录(本不应查到任何数据)");
// 这就是为什么必须使用 PreparedStatement 来防止 SQL 注入
}
}
|
插入后怎么拿到自增 ID?
INSERT 语句执行后,经常需要立即获取新记录的自增主键 id(例如插入订单后需要 orderId 用于后续关联)。JDBC 通过 Statement.RETURN_GENERATED_KEYS 标志和 getGeneratedKeys() 方法实现此功能。
| executeUpdate 传入 RETURN_GENERATED_KEYS,插入后读取自增主键 |
|---|
| /**
* 演示 Statement.RETURN_GENERATED_KEYS:插入后获取自动生成的主键值
* 适用场景:INSERT 后需要立刻拿到新记录的自增 id,再用于关联操作
*/
@Test
void testGetGeneratedKeys() throws SQLException {
// 建一张带自增主键的表用于演示
try (Statement stmt = conn.createStatement()) {
stmt.execute("CREATE TABLE IF NOT EXISTS orders ("
+ "id INT AUTO_INCREMENT PRIMARY KEY, "
+ "product VARCHAR(100), "
+ "amount DOUBLE)");
}
// 关键:executeUpdate 第二个参数传 Statement.RETURN_GENERATED_KEYS
// 告诉驱动:执行完 INSERT 后请保留生成的主键,供 getGeneratedKeys() 读取
String sql = "INSERT INTO orders (product, amount) VALUES ('Java Book', 99.9)";
try (Statement stmt = conn.createStatement()) {
try {
int rowsAffected = stmt.executeUpdate(sql, Statement.RETURN_GENERATED_KEYS);
assertEquals(1, rowsAffected, "应插入 1 条记录");
// 通过 getGeneratedKeys() 获取自动生成的主键
try (ResultSet generatedKeys = stmt.getGeneratedKeys()) {
assertTrue(generatedKeys.next(), "应有生成的主键");
long generatedId = generatedKeys.getLong(1);
assertTrue(generatedId > 0, "生成的主键 id 应大于 0");
System.out.println("新插入记录的自增主键 id: " + generatedId);
}
} finally {
// 使用 finally 确保断言失败时也能清理临时表
stmt.execute("DROP TABLE IF EXISTS orders");
}
}
}
|
PreparedStatement 同样支持
PreparedStatement 也支持获取自增主键,在 prepareStatement(sql, Statement.RETURN_GENERATED_KEYS) 时传入标志即可,用法完全一致。