PHP连接MySQL应选PDO还是MySQLi?安全写法与选型对比

编程狮 2026-09-21 12:01:12 浏览数 (41)
反馈

PHP 只连接 MySQL 时,PDO 与 MySQLi 都能安全工作;需要统一数据库接口、命名占位符或未来可能换数据库时优先 PDO,明确只用 MySQL 且依赖其专属能力时选 MySQLi。无论选哪一个,都要设置 utf8mb4、开启异常报告、使用预处理语句,并把连接凭据放在代码之外。

PDO 还是 MySQLi按项目边界选择

本文基于 PHP 官方 PDO 与 MySQLi 手册,使用同一个“按邮箱查询用户”的任务比较两套 API。你会看到连接、参数绑定、事务、动态字段白名单和错误处理的完整写法,而不是只比较函数名。最后还会给出一张按项目条件选择的清单。

一、先看结论:PDO 与 MySQLi 怎么选

对比项 PDO MySQLi
支持的数据库 多种数据库统一接口 仅 MySQL
占位符 命名 :email 和问号 ? 仅问号 ?
接口风格 面向对象 面向对象 + 过程式
预处理支持 支持 支持
事务支持 支持 支持
驱动要求 需 pdo_mysql 需 mysqli + mysqlnd(部分方法)
适合场景 未来可能换库、公共库 明确只用 MySQL、依赖专属功能
学习曲线 略高,但通用 直接,但只限 MySQL

一句话:需要通用性和命名占位符选 PDO,明确只用 MySQL 且依赖专属能力选 MySQLi;安全标准两者相同。

二、PDO 适合统一接口和命名占位符

若对 PHP 类、异常或环境变量还不熟悉,可先补 PHP 教程。正式代码应从配置层读取连接信息,示例中的库名和邮箱仅用于演示;账号也应只授予目标库所需的最小权限。

连接层之外,表、索引和事务知识决定查询能否正确工作。先运行最小 PHP 页面并确认扩展、环境变量与异常输出都符合预期,再从数据库客户端核对账号权限和目标库是否存在;完成这两步后,再用 MySQL 教程 核对数据库侧配置,不要把慢查询或权限错误归因于 PHP API 选型。

<?php
// 创建 PDO 连接:DSN 指定主机、库名和字符集
$pdo = new PDO(
    'mysql:host=127.0.0.1;dbname=w3cschool;charset=utf8mb4',
    getenv('DB_USER'),   // 从环境变量读取用户名
    getenv('DB_PASS'),   // 从环境变量读取密码
    [
        // 开启异常模式,让错误以异常形式抛出
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
    ]
);

// 准备带命名占位符的 SQL 模板
$stmt = $pdo->prepare('SELECT id, name FROM users WHERE email = :email');

// 执行并绑定参数,参数值不会被当作 SQL 代码
$stmt->execute(['email' => 'student@example.com']);

// 以关联数组形式输出结果
var_export($stmt->fetch(PDO::FETCH_ASSOC));

PDO 提供一致接口,但仍需安装 pdo_mysql 驱动。预期结果(未连接本机数据库)是一条关联数组或 false。账号密码从环境变量读取,不应提交到仓库。

PDO 预处理把 SQL 模板与参数分开,但表名、列名和排序方向不能直接用普通占位符替换,必须从允许列表中选择。PHP 速查手册 可用于核对异常和数组 API。

对整数分页参数,绑定时还要明确 PDO::PARAM_INT,避免把数字当成带引号的字符串传入:

// 整数参数显式绑定为 PARAM_INT
$stmt = $pdo->prepare('SELECT id FROM users LIMIT :limit OFFSET :offset');
$stmt->bindValue(':limit', 20, PDO::PARAM_INT);
$stmt->bindValue(':offset', 0, PDO::PARAM_INT);
$stmt->execute();

是否启用模拟预处理取决于驱动和兼容要求;团队应固定设置并测试,而不是在不同环境使用不同默认值。

PDO 配置项 推荐值 作用
PDO::ATTR_ERRMODE ERRMODE_EXCEPTION 错误抛异常,避免静默失败
PDO::ATTR_EMULATE_PREPARES false 使用真实预处理,需确认驱动支持
PDO::ATTR_DEFAULT_FETCH_MODE FETCH_ASSOC 默认返回关联数组
DSN charset utf8mb4 避免连接字符集不一致

三、MySQLi 适合明确绑定 MySQL 的项目

<?php
// 开启严格错误报告,让错误抛异常
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);

// 创建 MySQLi 连接
$db = new mysqli(
    '127.0.0.1',
    getenv('DB_USER'),
    getenv('DB_PASS'),
    'w3cschool'
);

// 设置连接字符集
$db->set_charset('utf8mb4');

// 准备带问号占位符的 SQL 模板
$stmt = $db->prepare('SELECT id, name FROM users WHERE email = ?');

// 待绑定参数
$email = 'student@example.com';

// 's' 表示字符串类型
$stmt->bind_param('s', $email);

// 执行查询
$stmt->execute();

// 获取结果并输出关联数组
var_export($stmt->get_result()->fetch_assoc());

MySQLi 同时有面向对象和过程式接口,团队应选一种风格。不要把用户输入拼进 SQL;MySQLi 预处理不仅处理参数转义,也让 SQL 结构与数据边界清楚。

bind_param('s', $email) 中的 s 表示字符串,整数、浮点数和二进制数据应使用对应类型,类型写错可能让查询发生隐式转换。

类型字符 含义 对应 PHP 类型
s 字符串 string
i 整数 int
d 浮点数 float
b 二进制 string(BLOB)

get_result() 依赖 mysqlnd 驱动;某些部署环境没有它时,需要改用 bind_result()fetch()。这不是 PDO 与 MySQLi 的安全差异,而是运行环境差异,部署前应通过 php -i 或最小连接脚本确认。

四、按项目约束做选择

条件 建议 原因
未来可能切换数据库 PDO 统一接口,减少上层分支
只使用 MySQL 专属功能 MySQLi 可直接使用 MySQL 特性
喜欢命名占位符 PDO 参数名即业务含义
旧项目已统一 MySQLi 保持一致并补安全配置 减少迁移成本
公共库需支持多数据库 PDO 驱动可替换
团队只熟悉 MySQLi MySQLi + 严格规范 熟悉度比流行度重要

如果团队没有切换数据库的计划,现有封装、测试工具和成员熟悉度通常比“网上谁更流行”更重要。相反,如果公共库必须支持多种数据库,PDO 的统一接口能减少上层分支,但 SQL 方言仍然不会自动统一。

驱动缺失会出现 could not find driver 等错误;凭据错误、端口不通和权限不足则属于连接层。一次只排查一层:

# 查看已启用的 PHP 扩展
php -m

# 确认 pdo_mysql 是否在列表中
php -m | grep pdo_mysql

先用 php -m 看扩展,再检查 TCP、账号和数据库授权。普通语法可以用本地 PHP 解释器练习,在线环境通常不能访问你的数据库,也不应上传真实凭据。

五、事务和错误处理不能省

多条写操作要用事务,并在异常时回滚。以“创建订单并扣库存”为例,两条语句必须一起成功;只给其中一条预处理,并不能保证业务一致性。

<?php
// 开启事务
$pdo->beginTransaction();

try {
    // 扣库存:条件中要求库存充足
    $stock = $pdo->prepare(
        'UPDATE products SET stock = stock - :qty WHERE id = :id AND stock >= :qty'
    );
    $stock->execute(['qty' => 1, 'id' => 42]);

    // 受影响行数不为 1,说明库存不足或商品不存在
    if ($stock->rowCount() !== 1) {
        throw new RuntimeException('库存不足或商品不存在');
    }

    // 创建订单
    $pdo->prepare('INSERT INTO orders(product_id, qty) VALUES(:id, :qty)')
        ->execute(['id' => 42, 'qty' => 1]);

    // 两条语句都成功,提交事务
    $pdo->commit();
} catch (Throwable $error) {
    // 发生异常时回滚,避免数据不一致
    if ($pdo->inTransaction()) {
        $pdo->rollBack();
    }
    throw $error;
}

MySQLi 也提供 begin_transaction()commit()rollback(),事务能力不是 PDO 独有。

事务步骤 PDO MySQLi
开始 beginTransaction() begin_transaction()
提交 commit() commit()
回滚 rollBack() rollback()
判断是否在事务中 inTransaction() 需自行记录状态

不要捕获异常后只输出“失败”;应记录错误类型、请求 ID 和不含敏感值的上下文,同时向用户返回不暴露密码、SQL 和服务器路径的消息。连接对象不要作为全局变量到处传递,集中封装更容易替换测试数据库。

六、动态查询要对白名单负责

参数占位符只能代表数据值,不能代表列名。下面的排序字段即使来自查询参数,也必须映射到固定集合:

<?php
// 允许的排序字段白名单:用户输入映射到真实列名
$allowed = [
    'created' => 'created_at',
    'name'    => 'name',
];

// 从查询参数取排序键,默认 created
$sortKey = $_GET['sort'] ?? 'created';

// 只允许白名单中的键,否则用默认值
$sortColumn = $allowed[$sortKey] ?? $allowed['created'];

// 拼接的是白名单结果,不是原始用户输入
$sql = "SELECT id, name FROM users ORDER BY {$sortColumn} DESC LIMIT 20";

这里拼接的是开发者控制的白名单结果,而不是原始用户输入。动态表名、升降序和筛选运算符同样处理。

SQL 部分 能否参数化 正确处理方式
WHERE email = :email 预处理绑定
ORDER BY :column 不能 白名单映射
LIMIT :limit 能(需指定类型) PARAM_INT
表名 不能 白名单映射
ASC / DESC 不能 白名单映射

数据库连接安全不是“使用了 prepare”这一项打勾就结束,还包括最小权限账号、TLS、凭据轮换、超时和错误信息脱敏。

验证时准备正常邮箱、不存在邮箱、包含引号的邮箱和超长输入。正确结果应分别是单条记录、false、被当作普通数据处理和在业务校验层拒绝;任何输入都不应改变 SQL 结构。

测试输入 预期结果
正常邮箱 返回单条记录
不存在邮箱 返回 false
含引号邮箱 被当作普通数据,不改变 SQL
超长输入 在业务校验层拒绝
排序参数 ?sort=xxx 使用默认排序,不报错

七、部署环境差异与检查清单

连接池与长驻进程还要额外验证断线重连和事务状态。传统 PHP 请求结束后连接通常随请求释放,但 Swoole、RoadRunner 或队列 worker 会长期持有对象;数据库重启、空闲超时后,旧连接可能已经失效。重连前必须确认没有未提交事务,不能捕获所有错误后盲目重放写操作。

部署清单应包含:

检查项 说明 命令/方法
PHP 版本 确认与开发环境一致 php -v
启用扩展 pdo_mysql 或 mysqli php -m
驱动版本 与数据库版本匹配 php -i
数据库版本 确认兼容性 SELECT VERSION()
字符集 连接与表一致 SHOW VARIABLES LIKE 'character_set%'
SQL mode 严格模式差异 SELECT @@sql_mode

开发机使用 mysqlnd、生产机没有时,MySQLi 的取结果方式可能不同;测试库宽松、生产库严格时,插入行为也会不同。把这些环境事实记录下来,比“PDO 更通用”或“MySQLi 更快”这类空泛结论更有排错价值。

性能选择只能由相同 SQL、相同索引、相同连接配置下的测量支持。通常先优化查询和索引,再评估驱动差异;不要为未经测量的微小差异放弃命名占位符、现有封装或团队可维护性。

PDO 与 MySQLi 的选型对照

总结

PDO 与 MySQLi 的核心差异是通用性和 MySQL 专用能力,不是“谁绝对更安全”。PDO 预处理和 MySQLi 预处理都能把数据与 SQL 结构分离;真正的数据库连接安全还取决于字符集、白名单、凭据管理、最小权限、事务和错误处理。先按项目边界选 API,再用同一套工程标准验证实现。

延伸学习

  1. PHP 连接 MySQL 的两种方法 可复习基础连接;
  2. PDO 驱动缺失排查 适合处理扩展问题。
  3. 【体系课】PHP 零基础入门到就业

常见问题

Q:PDO 会自动防止 SQL 注入吗?

A:不会自动保证。必须使用 prepare 与参数绑定,动态表名和排序字段仍要白名单校验。

Q:连接成功后还要设置字符集吗?

A:要。使用 DSN 或 set_charset 明确 utf8mb4,避免连接默认值与表字符集不一致。

Q:可以为了省事把密码写在 PHP 文件里吗?

A:不建议。使用环境变量或密钥管理服务,并限制数据库账号权限。

Q:MySQLi 的 get_result() 一定可用吗?

A:不一定。它依赖 mysqlnd 驱动。若部署环境没有该驱动,应改用 bind_result()fetch(),或安装 mysqlnd。部署前用 php -i 确认。

0 人点赞