预处理语句
什么是预处理语句
预处理语句(Prepared Statements)是数据库操作的一种重要技术,它将SQL语句的编译和执行分开进行。预处理语句先发送SQL模板到数据库服务器进行编译,然后再发送具体的参数值进行执行。
预处理语句的优势
- 防止SQL注入:参数值不会被当作SQL代码执行
- 提高性能:相同模板的语句只需编译一次,可多次执行
- 数据类型安全:自动处理数据类型转换
- 可读性好:SQL语句更清晰,参数明确
预处理语句的工作原理
第1步:准备SQL模板
↓
第2步:数据库编译SQL模板
↓
第3步:绑定参数值
↓
第4步:执行SQL语句
两种参数绑定方式
1. 位置参数(问号占位符)
<?php
// SQL模板中使用?作为占位符
$sql = "SELECT * FROM users WHERE username = ? AND age > ?";
try {
// 准备语句
$stmt = $pdo->prepare($sql);
// 执行时按顺序传递参数
$stmt->execute(['zhangsan', 18]);
// 获取结果
$user = $stmt->fetch();
} catch (PDOException $e) {
echo "查询失败: " . $e->getMessage();
}
?>
2. 命名参数(冒号占位符)
<?php
// SQL模板中使用:开头的命名参数
$sql = "SELECT * FROM users
WHERE username = :username AND age > :min_age
ORDER BY created_at DESC
LIMIT :limit";
try {
// 准备语句
$stmt = $pdo->prepare($sql);
// 执行时使用关联数组传递参数
$params = [
'username' => 'zhangsan',
'min_age' => 18,
'limit' => 10
];
$stmt->execute($params);
// 获取结果
$users = $stmt->fetchAll();
} catch (PDOException $e) {
echo "查询失败: " . $e->getMessage();
}
?>
参数绑定方法详解
1. execute()方法绑定(常用)
<?php
// 方法一:通过execute()的参数直接传递
$sql = "INSERT INTO users (username, email, age) VALUES (?, ?, ?)";
$stmt = $pdo->prepare($sql);
$stmt->execute(['user1', 'user1@example.com', 25]);
// 方法二:命名参数使用关联数组
$sql = "INSERT INTO users (username, email, age)
VALUES (:username, :email, :age)";
$stmt = $pdo->prepare($sql);
$stmt->execute([
'username' => 'user2',
'email' => 'user2@example.com',
'age' => 30
]);
?>
2. bindValue()方法绑定
<?php
$sql = "INSERT INTO users (username, email, age) VALUES (?, ?, ?)";
$stmt = $pdo->prepare($sql);
// 逐个绑定值(1-based索引)
$stmt->bindValue(1, 'user3');
$stmt->bindValue(2, 'user3@example.com');
$stmt->bindValue(3, 28);
// 命名参数
$sql = "INSERT INTO users (username, email, age)
VALUES (:username, :email, :age)";
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':username', 'user4');
$stmt->bindValue(':email', 'user4@example.com');
$stmt->bindValue(':age', 32);
$stmt->execute();
?>
3. bindParam()方法绑定
<?php
$sql = "INSERT INTO users (username, email, age) VALUES (?, ?, ?)";
$stmt = $pdo->prepare($sql);
// bindParam绑定变量引用
$username = 'user5';
$email = 'user5@example.com';
$age = 22;
$stmt->bindParam(1, $username);
$stmt->bindParam(2, $email);
$stmt->bindParam(3, $age);
// 可以修改变量值,多次执行
$username = 'user6';
$email = 'user6@example.com';
$age = 27;
$stmt->execute(); // 插入user6
$username = 'user7';
$email = 'user7@example.com';
$age = 35;
$stmt->execute(); // 插入user7
// bindParam对命名参数
$sql = "SELECT * FROM users WHERE age > :min_age";
$stmt = $pdo->prepare($sql);
$minAge = 18;
$stmt->bindParam(':min_age', $minAge);
$minAge = 25;
$stmt->execute(); // 查询年龄>25的用户
$minAge = 30;
$stmt->execute(); // 查询年龄>30的用户
?>
数据类型处理
显式指定数据类型
<?php
$sql = "INSERT INTO products (name, price, quantity, created_at)
VALUES (:name, :price, :quantity, :created_at)";
$stmt = $pdo->prepare($sql);
// 绑定参数并指定数据类型
$stmt->bindValue(':name', 'iPhone 15', PDO::PARAM_STR);
$stmt->bindValue(':price', 999.99, PDO::PARAM_STR); // DECIMAL作为字符串
$stmt->bindValue(':quantity', 100, PDO::PARAM_INT);
$stmt->bindValue(':created_at', date('Y-m-d H:i:s'), PDO::PARAM_STR);
$stmt->execute();
// PDO参数常量
/*
PDO::PARAM_BOOL 布尔类型
PDO::PARAM_NULL NULL值
PDO::PARAM_INT 整数
PDO::PARAM_STR 字符串
PDO::PARAM_LOB 大对象(LOB)
*/
?>
NULL值处理
<?php
// 更新用户信息,可能包含NULL值
function updateUser($pdo, $userId, $data) {
$sql = "UPDATE users SET
email = :email,
phone = :phone,
updated_at = NOW()
WHERE id = :id";
$stmt = $pdo->prepare($sql);
// 绑定参数,处理NULL值
if ($data['email'] === null) {
$stmt->bindValue(':email', null, PDO::PARAM_NULL);
} else {
$stmt->bindValue(':email', $data['email'], PDO::PARAM_STR);
}
if ($data['phone'] === null) {
$stmt->bindValue(':phone', null, PDO::PARAM_NULL);
} else {
$stmt->bindValue(':phone', $data['phone'], PDO::PARAM_STR);
}
$stmt->bindValue(':id', $userId, PDO::PARAM_INT);
return $stmt->execute();
}
// 使用示例
updateUser($pdo, 1, [
'email' => 'new@example.com',
'phone' => null // 将phone设为NULL
]);
?>
IN语句的特殊处理
使用问号占位符
<?php
// 查询指定ID的用户
function getUsersByIds($pdo, $ids) {
// 创建占位符字符串
$placeholders = str_repeat('?,', count($ids) - 1) . '?';
$sql = "SELECT * FROM users WHERE id IN ($placeholders)";
$stmt = $pdo->prepare($sql);
$stmt->execute($ids);
return $stmt->fetchAll();
}
// 使用示例
$userIds = [1, 3, 5, 7, 9];
$users = getUsersByIds($pdo, $userIds);
?>
使用命名参数
<?php
function getUsersByStatus($pdo, $statuses) {
// 为每个状态创建命名参数
$params = [];
$placeholders = [];
foreach ($statuses as $i => $status) {
$param = ":status_$i";
$params[$param] = $status;
$placeholders[] = $param;
}
$sql = "SELECT * FROM users WHERE status IN (" .
implode(',', $placeholders) . ")";
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll();
}
// 使用示例
$statuses = ['active', 'pending'];
$users = getUsersByStatus($pdo, $statuses);
?>
复杂查询示例
动态条件查询
<?php
class UserQuery {
private $pdo;
private $conditions = [];
private $params = [];
public function __construct($pdo) {
$this->pdo = $pdo;
}
public function whereUsername($username) {
$this->conditions[] = "username = :username";
$this->params[':username'] = $username;
return $this;
}
public function whereAgeGt($age) {
$this->conditions[] = "age > :min_age";
$this->params[':min_age'] = $age;
return $this;
}
public function whereStatus($status) {
$this->conditions[] = "status = :status";
$this->params[':status'] = $status;
return $this;
}
public function orderBy($field, $direction = 'ASC') {
$this->orderBy = "ORDER BY $field $direction";
return $this;
}
public function limit($limit, $offset = 0) {
$this->limit = "LIMIT $limit OFFSET $offset";
return $this;
}
public function execute() {
$sql = "SELECT * FROM users";
if (!empty($this->conditions)) {
$sql .= " WHERE " . implode(' AND ', $this->conditions);
}
if (isset($this->orderBy)) {
$sql .= " " . $this->orderBy;
}
if (isset($this->limit)) {
$sql .= " " . $this->limit;
}
$stmt = $this->pdo->prepare($sql);
$stmt->execute($this->params);
return $stmt->fetchAll();
}
}
// 使用示例
$query = new UserQuery($pdo);
$users = $query->whereUsername('zhangsan')
->whereAgeGt(18)
->whereStatus('active')
->orderBy('created_at', 'DESC')
->limit(10)
->execute();
?>
批量操作优化
批量插入
<?php
// 方法一:使用事务批量插入
function batchInsertUsers($pdo, $users) {
$sql = "INSERT INTO users (username, email, age, status)
VALUES (?, ?, ?, ?)";
$stmt = $pdo->prepare($sql);
$pdo->beginTransaction();
try {
foreach ($users as $user) {
$stmt->execute([
$user['username'],
$user['email'],
$user['age'],
$user['status']
]);
}
$pdo->commit();
return true;
} catch (Exception $e) {
$pdo->rollback();
return false;
}
}
// 方法二:使用批量INSERT语法
function batchInsertUsersOptimized($pdo, $users) {
$values = [];
$params = [];
foreach ($users as $i => $user) {
$values[] = "(?, ?, ?, ?)";
array_push($params,
$user['username'],
$user['email'],
$user['age'],
$user['status']
);
}
$sql = "INSERT INTO users (username, email, age, status)
VALUES " . implode(',', $values);
$stmt = $pdo->prepare($sql);
return $stmt->execute($params);
}
// 使用示例
$newUsers = [
['username' => 'user1', 'email' => 'user1@test.com', 'age' => 25, 'status' => 'active'],
['username' => 'user2', 'email' => 'user2@test.com', 'age' => 30, 'status' => 'active'],
['username' => 'user3', 'email' => 'user3@test.com', 'age' => 28, 'status' => 'pending']
];
batchInsertUsers($pdo, $newUsers);
?>
调试预处理语句
获取调试信息
<?php
$sql = "SELECT * FROM users WHERE username = :username AND age > :min_age";
$stmt = $pdo->prepare($sql);
// 获取原始SQL
echo "SQL模板: " . $stmt->queryString . "\n";
// 执行查询
$stmt->execute(['username' => 'test', 'min_age' => 18]);
// 获取行数
echo "影响行数: " . $stmt->rowCount() . "\n";
// 获取列数
echo "列数: " . $stmt->columnCount() . "\n";
// 模拟SQL输出(用于调试)
function debugQuery($sql, $params) {
foreach ($params as $key => $value) {
if (is_string($value)) {
$value = "'" . addslashes($value) . "'";
}
$sql = str_replace($key, $value, $sql);
}
return $sql;
}
echo "执行的SQL: " . debugQuery($sql, ['username' => 'test', 'min_age' => 18]);
?>
安全最佳实践
1. 始终使用预处理语句
<?php
// ❌ 错误:直接拼接SQL(不安全)
function unsafeSearch($pdo, $keyword) {
$sql = "SELECT * FROM products WHERE name LIKE '%$keyword%'";
return $pdo->query($sql)->fetchAll();
}
// ✅ 正确:使用预处理语句(安全)
function safeSearch($pdo, $keyword) {
$sql = "SELECT * FROM products WHERE name LIKE :keyword";
$stmt = $pdo->prepare($sql);
$stmt->execute(['keyword' => "%$keyword%"]);
return $stmt->fetchAll();
}
?>
2. 验证输入数据
<?php
function insertUser($pdo, $userData) {
// 验证必填字段
$required = ['username', 'email', 'age'];
foreach ($required as $field) {
if (!isset($userData[$field]) || empty($userData[$field])) {
throw new Exception("字段 $field 是必填的");
}
}
// 验证数据类型
if (!filter_var($userData['email'], FILTER_VALIDATE_EMAIL)) {
throw new Exception("邮箱格式不正确");
}
if (!is_int($userData['age']) || $userData['age'] < 0) {
throw new Exception("年龄必须是正整数");
}
$sql = "INSERT INTO users (username, email, age)
VALUES (:username, :email, :age)";
$stmt = $pdo->prepare($sql);
return $stmt->execute([
'username' => $userData['username'],
'email' => $userData['email'],
'age' => $userData['age']
]);
}
?>
性能优化建议
1. 重用预处理语句
<?php
// 预编译一次,多次执行
function updateUserStatus($pdo, $userIds, $newStatus) {
$sql = "UPDATE users SET status = :status WHERE id = :id";
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':status', $newStatus, PDO::PARAM_STR);
foreach ($userIds as $userId) {
$stmt->bindValue(':id', $userId, PDO::PARAM_INT);
$stmt->execute();
}
return $stmt->rowCount();
}
?>
2. 使用合适的数据类型
<?php
// ✅ 使用正确的数据类型
$stmt->bindValue(':count', $count, PDO::PARAM_INT);
$stmt->bindValue(':price', $price, PDO::PARAM_STR); // DECIMAL
$stmt->bindValue(':active', $active, PDO::PARAM_BOOL);
// ❌ 避免类型转换错误
$stmt->bindValue(':count', (string)$count); // 不推荐
?>
通过本节的学习,你应该掌握了预处理语句的各种用法和安全编程技巧。下一节我们将学习事务处理,进一步掌握数据库的高级操作。