Skip to content

PDO 预处理语句

概述

预处理语句(Prepared Statements)是 PDO 的核心安全特性,通过参数绑定防止 SQL 注入。prepare() 创建预处理语句,bindParam()/bindValue() 绑定参数,execute() 执行语句。PDO 支持命名占位符(:name)和问号占位符(?)两种参数风格。

适用场景

  • 所有 SQL 查询(安全必需)
  • 重复执行的 SQL(性能优化)
  • 用户输入的参数化查询
  • 批量数据操作

基础概念

占位符类型

类型语法示例
命名占位符:nameWHERE id = :id
问号占位符?WHERE id = ?

bindParam vs bindValue

函数引用绑定类型执行时求值
bindParam()是(引用)可指定 PDO::PARAM_*
bindValue()否(值)可指定 PDO::PARAM_*

性能优势

预处理语句在多次执行相同 SQL 时(不同参数)具有性能优势,因为 SQL 只需编译一次。

语法与代码示例

命名占位符

php
<?php

declare(strict_types=1);

$pdo = new PDO('mysql:host=localhost;dbname=test', 'root', '');

// prepare + bindValue + execute
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id AND status = :status');
$stmt->bindValue(':id', 1, PDO::PARAM_INT);
$stmt->bindValue(':status', 'active', PDO::PARAM_STR);
$stmt->execute();
$user = $stmt->fetch();

// execute 直接传参数数组(推荐简洁写法)
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id AND status = :status');
$stmt->execute(['id' => 1, 'status' => 'active']);
$user = $stmt->fetch();

问号占位符

php
<?php

// 问号占位符(按索引绑定)
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = ? AND status = ?');
$stmt->bindValue(1, 1, PDO::PARAM_INT);
$stmt->bindValue(2, 'active', PDO::PARAM_STR);
$stmt->execute();

// execute 数组(索引从 1 开始)
$stmt->execute([1, 'active']);
$user = $stmt->fetch();

bindParam 引用绑定

php
<?php

// bindParam 绑定变量引用
// 在 execute() 时才读取变量值
$stmt = $pdo->prepare('INSERT INTO logs (message, level) VALUES (:msg, :level)');

$message = 'Test message';
$level = 'INFO';

$stmt->bindParam(':msg', $message, PDO::PARAM_STR);
$stmt->bindParam(':level', $level, PDO::PARAM_STR);

// 修改变量值后执行
$message = 'Modified message';
$level = 'WARNING';
$stmt->execute(); // 使用修改后的值

批量执行

php
<?php

// 批量插入
$stmt = $pdo->prepare('INSERT INTO users (name, email, age) VALUES (:name, :email, :age)');

$users = [
    ['name' => 'Alice', 'email' => 'alice@example.com', 'age' => 28],
    ['name' => 'Bob', 'email' => 'bob@example.com', 'age' => 32],
    ['name' => 'Charlie', 'email' => 'charlie@example.com', 'age' => 25],
];

$pdo->beginTransaction();
try {
    foreach ($users as $user) {
        $stmt->execute([
            'name' => $user['name'],
            'email' => $user['email'],
            'age' => $user['age'],
        ]);
    }
    $pdo->commit();
} catch (PDOException $e) {
    $pdo->rollBack();
    throw $e;
}

IN 查询处理

php
<?php

// PDO 不直接支持 IN 占位符,需要动态构建
$ids = [1, 2, 3, 4, 5];

// 动态生成占位符
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$stmt = $pdo->prepare("SELECT * FROM users WHERE id IN ({$placeholders})");
$stmt->execute(array_values($ids));
$users = $stmt->fetchAll();

// 命名占位符版本
$placeholders = implode(',', array_map(fn($i) => ':id' . $i, array_keys($ids)));
$stmt = $pdo->prepare("SELECT * FROM users WHERE id IN ({$placeholders})");
$params = array_combine(
    array_map(fn($i) => ':id' . $i, array_keys($ids)),
    $ids
);
$stmt->execute($params);

实战示例

通用查询构建器

php
<?php

declare(strict_types=1);

class QueryBuilder
{
    private PDO $pdo;
    private string $table;
    private array $where = [];
    private array $params = [];
    private ?int $limit = null;
    private ?int $offset = null;
    private array $orderBy = [];

    public function __construct(PDO $pdo, string $table)
    {
        $this->pdo = $pdo;
        $this->table = $table;
    }

    public function where(string $column, mixed $value, string $operator = '='): self
    {
        $param = ':w_' . count($this->where);
        $this->where[] = "{$column} {$operator} {$param}";
        $this->params[$param] = $value;
        return $this;
    }

    public function limit(int $limit): self
    {
        $this->limit = $limit;
        return $this;
    }

    public function offset(int $offset): self
    {
        $this->offset = $offset;
        return $this;
    }

    public function orderBy(string $column, string $direction = 'ASC'): self
    {
        $this->orderBy[] = "{$column} {$direction}";
        return $this;
    }

    public function get(): array
    {
        $sql = "SELECT * FROM {$this->table}";
        if (!empty($this->where)) {
            $sql .= ' WHERE ' . implode(' AND ', $this->where);
        }
        if (!empty($this->orderBy)) {
            $sql .= ' ORDER BY ' . implode(', ', $this->orderBy);
        }
        if ($this->limit !== null) {
            $sql .= " LIMIT {$this->limit}";
        }
        if ($this->offset !== null) {
            $sql .= " OFFSET {$this->offset}";
        }

        $stmt = $this->pdo->prepare($sql);
        $stmt->execute($this->params);
        return $stmt->fetchAll();
    }

    public function first(): ?array
    {
        $this->limit(1);
        $results = $this->get();
        return $results[0] ?? null;
    }
}

// 使用
$users = (new QueryBuilder($pdo, 'users'))
    ->where('status', 'active')
    ->where('age', 18, '>')
    ->orderBy('created_at', 'DESC')
    ->limit(10)
    ->get();

注意事项

占位符只能用于值

php
<?php

// 占位符不能用于表名、列名、SQL 关键字
// 不好
$stmt = $pdo->prepare('SELECT * FROM :table WHERE :column = :value');

// 好:表名和列名直接拼接到 SQL(需要白名单验证)
$allowedTables = ['users', 'orders', 'products'];
$table = $_GET['table'];
if (!in_array($table, $allowedTables, true)) {
    throw new InvalidArgumentException('Invalid table');
}
$stmt = $pdo->prepare("SELECT * FROM {$table} WHERE id = :id");

LIKE 查询中的占位符

php
<?php

// LIKE 通配符需要在值中包含
$search = '%john%';
$stmt = $pdo->prepare("SELECT * FROM users WHERE name LIKE :search");
$stmt->execute(['search' => $search]);

最佳实践

1. 优先使用 execute() 传参

php
<?php

// 推荐:简洁
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id');
$stmt->execute(['id' => $userId]);

// 不必要:繁琐
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id');
$stmt->bindValue(':id', $userId, PDO::PARAM_INT);
$stmt->execute();

2. 指定参数类型

php
<?php

// 对于整型、布尔型参数指定类型
$stmt = $pdo->prepare('SELECT * FROM users WHERE age > :age AND active = :active');
$stmt->bindValue(':age', 18, PDO::PARAM_INT);
$stmt->bindValue(':active', true, PDO::PARAM_BOOL);
$stmt->execute();

进阶用法

调试与测试技巧

php
<?php
declare(strict_types=1);

// 单元测试辅助函数
function createTestResource(): mixed
{
    return match (true) {
        default => new stdClass(),
    };
}

// 调试输出函数
function debugOutput(mixed , string  = ''): void
{
     =  ? ": " : '';
     .= print_r(, true);
    fwrite(STDERR,  . "\n");
}

// 性能基准测试
function benchmark(callable , int  = 1000): float
{
     = hrtime(true);
    for ($i = 0; $i < $iterations; $i++) {
        $fn();
    }
    return (hrtime(true) - $start) / 1e9;
}

日志记录实践

php
<?php
declare(strict_types=1);

/**
 * 简易日志记录器
 */
class SimpleLogger
{
    private string $logFile;
    private string $level = 'INFO';

    public function __construct(string $logFile)
    {
        $this->logFile = $logFile;
    }

    public function info(string $message, array $context = []): void
    {
        $this->log('INFO', $message, $context);
    }

    public function warning(string $message, array $context = []): void
    {
        $this->log('WARNING', $message, $context);
    }

    public function error(string $message, array $context = []): void
    {
        $this->log('ERROR', $message, $context);
    }

    private function log(string $level, string $message, array $context): void
    {
        $timestamp = date('Y-m-d H:i:s');
        $contextStr = $context ? ' ' . json_encode($context, JSON_UNESCAPED_UNICODE) : '';
        $line = "[{$timestamp}] [{$level}] {$message}{$contextStr}\n";
        file_put_contents($this->logFile, $line, FILE_APPEND | LOCK_EX);
    }
}

配置与环境检测

php
<?php
declare(strict_types=1);

// 环境检测工具
class EnvironmentChecker
{
    public static function checkRequirements(array $requirements): array
    {
        $results = [];
        foreach ($requirements as $name => $check) {
            $results[$name] = is_callable($check) ? $check() : false;
        }
        return $results;
    }

    public static function getSystemInfo(): array
    {
        return [
            'php_version' => PHP_VERSION,
            'os' => PHP_OS,
            'sapi' => PHP_SAPI,
            'memory_limit' => ini_get('memory_limit'),
            'max_execution_time' => ini_get('max_execution_time'),
            'loaded_extensions' => get_loaded_extensions(),
        ];
    }
}

常见问题排查

问题可能原因解决方案
连接超时网络问题/配置错误检查配置,增加超时时间
权限不足文件/目录权限使用 chmod/chown 修正
性能下降索引缺失/数据量大添加索引,优化查询
数据不一致并发冲突/事务残留使用锁机制和事务
内存溢出大数据集/未释放资源增大内存限制,分批处理

故障排除步骤

  1. 检查错误日志和异常信息
  2. 确认配置和环境是否正确
  3. 使用调试工具逐步排查
  4. 参考官方文档查找已知问题

版本兼容性说明

功能最低版本说明
基础功能PHP 8.1本文档基准版本
只读属性PHP 8.1public readonly 修饰符
枚举类型PHP 8.1enum 类型和 match 表达式
FiberPHP 8.1协程/轻量级并发
命名参数PHP 8.0foo(arg_name: value)
联合类型PHP 8.0`int
Null 安全运算符PHP 8.0$obj?->method()
析构器 promotionPHP 8.0__construct(public $x)
php
<?php
declare(strict_types=1);

// 版本兼容性检测
function ensureVersion(string $minVersion): void
{
    if (version_compare(PHP_VERSION, $minVersion, '<')) {
        throw new RuntimeException(
            sprintf('需要 PHP %s+, 当前版本: %s', $minVersion, PHP_VERSION)
        );
    }
}

ensureVersion('8.1.0');

参考链接