为什么要用PreparedStatement?
PreparedStatement是预编译的SQL语句对象,它不仅能提高SQL执行效率,更重要的是能防止SQL注入攻击,保护数据库安全。
SQL注入攻击比喻
想象你是一家餐厅的员工点餐系统。顾客输入"宫保鸡丁"你就直接做这道菜。但恶意顾客输入"宫保鸡丁; DELETE ALL ORDERS",如果系统直接执行,后果不堪设想!PreparedStatement就像一个严格的点餐系统——它只接受"菜品名称",不接受任何其他"命令"。
PreparedStatement vs Statement对比
| 特性 | Statement | PreparedStatement |
|---|---|---|
| SQL注入 | 不安全,容易被攻击 | 安全,自动转义 |
| 性能 | 每次执行都编译 | 预编译,重复执行高效 |
| 参数绑定 | 字符串拼接 | 占位符? + setXxx() |
| 可读性 | 差,SQL和代码混杂 | 好,SQL和参数分离 |
| 使用场景 | 不推荐 | 推荐使用 |
重要:永远不要用字符串拼接SQL!
// 危险!SQL注入风险
String sql = "SELECT * FROM users WHERE name = '" + username + "'";
// 如果username = "'; DROP TABLE users; --",整个表都没了!
// 安全!使用PreparedStatement
String sql = "SELECT * FROM users WHERE name = ?";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setString(1, username); // 自动转义
PreparedStatement完整使用流程
JDBC查询步骤:
1. 编写SQL(使用?作为占位符)
String sql = "SELECT * FROM users WHERE age > ? AND status = ?";
2. 预编译SQL(创建PreparedStatement)
PreparedStatement ps = conn.prepareStatement(sql);
3. 绑定参数(按顺序设置?的值)
ps.setInt(1, 18); // 第一个?的值
ps.setString(2, "active"); // 第二个?的值
4. 执行查询
ResultSet rs = ps.executeQuery();
5. 处理结果
while (rs.next()) {
String name = rs.getString("name");
// ...
}
6. 关闭资源
rs.close();
ps.close();
完整查询示例
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
public class JDBCQueryDemo {
public static void main(String[] args) {
String url = "jdbc:mysql://localhost:3306/mydb";
String user = "root";
String password = "123456";
String sql = "SELECT id, name, email, age, created_at " +
"FROM users WHERE age >= ? AND status = ? " +
"ORDER BY created_at DESC LIMIT ?";
try (Connection conn = DriverManager.getConnection(url, user, password);
PreparedStatement ps = conn.prepareStatement(sql)) {
// 设置参数
ps.setInt(1, 18); // 年龄 >= 18
ps.setString(2, "active"); // 状态是active
ps.setInt(3, 10); // 最多返回10条
// 执行查询
try (ResultSet rs = ps.executeQuery()) {
List users = new ArrayList<>();
while (rs.next()) {
User user = new User();
user.setId(rs.getLong("id"));
user.setName(rs.getString("name"));
user.setEmail(rs.getString("email"));
user.setAge(rs.getInt("age"));
user.setCreatedAt(rs.getTimestamp("created_at"));
users.add(user);
}
System.out.println("查询到 " + users.size() + " 条用户记录");
users.forEach(System.out::println);
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
// User类
class User {
private Long id;
private String name;
private String email;
private int age;
private Timestamp createdAt;
// getters, setters, toString省略
}
ResultSet处理详解
ResultSet基本操作
// 游标移动(默认只在第一行之前)
rs.next(); // 移动到下一行,返回是否有数据
rs.previous(); // 移动到上一行(需要支持滚动)
rs.first(); // 移动到第一行
rs.last(); // 移动到最后一行
rs.absolute(5); // 移动到第5行
// 获取数据
while (rs.next()) {
// 按列名获取(推荐,代码更清晰)
String name = rs.getString("name");
int age = rs.getInt("age");
// 按列索引获取(从1开始)
String name2 = rs.getString(1);
int age2 = rs.getInt(2);
}
各种数据类型的获取
while (rs.next()) {
// 基本类型
int id = rs.getInt("id");
long big = rs.getLong("id");
double price = rs.getDouble("price");
boolean active = rs.getBoolean("status");
// 字符串
String name = rs.getString("name");
// 日期时间(推荐使用Java 8+的LocalDateTime)
java.sql.Date sqlDate = rs.getDate("birth_date"); // java.sql.Date
java.sql.Time sqlTime = rs.getTime("create_time"); // java.sql.Time
java.sql.Timestamp timestamp = rs.getTimestamp("create_time"); // 日期+时间
// Java 8+ 推荐
LocalDate localDate = rs.getObject("birth_date", LocalDate.class);
LocalDateTime localDateTime = rs.getObject("create_time", LocalDateTime.class);
// 大数据
String content = rs.getString("description"); // 短文本
InputStream is = rs.getBinaryStream("file_data"); // 二进制流
}
参数设置方法一览
| 方法 | 适用类型 | 示例 |
|---|---|---|
| setInt(int paramIndex, int value) | int, Integer | ps.setInt(1, 25) |
| setLong(int paramIndex, long value) | long, Long | ps.setLong(1, 100L) |
| setDouble(int paramIndex, double value) | double, Double | ps.setDouble(1, 99.99) |
| setString(int paramIndex, String value) | String | ps.setString(1, "北京") |
| setBoolean(int paramIndex, boolean value) | boolean | ps.setBoolean(1, true) |
| setDate(int paramIndex, Date value) | java.sql.Date | ps.setDate(1, new Date()) |
| setNull(int paramIndex, int sqlType) | NULL值 | ps.setNull(1, Types.VARCHAR) |
| setObject(int paramIndex, Object value) | 通用 | ps.setObject(1, obj) |
常见的SQL查询模式
1. 查询单条记录
public User findById(Long id) {
String sql = "SELECT * FROM users WHERE id = ?";
try (Connection conn = getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setLong(1, id);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
return mapToUser(rs);
}
}
}
return null; // 未找到
}
private User mapToUser(ResultSet rs) throws SQLException {
User user = new User();
user.setId(rs.getLong("id"));
user.setName(rs.getString("name"));
user.setEmail(rs.getString("email"));
user.setAge(rs.getInt("age"));
return user;
}
2. 模糊查询
public List searchByName(String keyword) {
String sql = "SELECT * FROM users WHERE name LIKE ?";
try (Connection conn = getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, "%" + keyword + "%"); // LIKE需要自己加%
List results = new ArrayList<>();
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
results.add(mapToUser(rs));
}
}
return results;
}
}
3. 批量查询(IN条件)
public List findByIds(List ids) {
if (ids == null || ids.isEmpty()) {
return new ArrayList<>();
}
// 构建 ?,,? 形式的占位符
String placeholders = ids.stream()
.map(id -> "?")
.collect(Collectors.joining(","));
String sql = "SELECT * FROM users WHERE id IN (" + placeholders + ")";
try (Connection conn = getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
// 设置每个参数
for (int i = 0; i < ids.size(); i++) {
ps.setLong(i + 1, ids.get(i));
}
List results = new ArrayList<>();
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
results.add(mapToUser(rs));
}
}
return results;
}
}
注意事项
ResultSet需要关闭!
ResultSet会在Statement关闭时自动关闭,但最好显式关闭以避免资源泄漏。使用try-with-resources。
索引从1开始!
PreparedStatement的参数索引从1开始,不是0。
小结
- PreparedStatement防止SQL注入,是推荐做法
- 使用?占位符代替字符串拼接
- 通过setXxx()方法按顺序绑定参数
- ResultSet通过next()遍历,getXxx()获取数据
- 使用try-with-resources确保资源正确关闭