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

本文基于 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 的核心差异是通用性和 MySQL 专用能力,不是“谁绝对更安全”。PDO 预处理和 MySQLi 预处理都能把数据与 SQL 结构分离;真正的数据库连接安全还取决于字符集、白名单、凭据管理、最小权限、事务和错误处理。先按项目边界选 API,再用同一套工程标准验证实现。
延伸学习
- PHP 连接 MySQL 的两种方法 可复习基础连接;
- PDO 驱动缺失排查 适合处理扩展问题。
- 【体系课】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 确认。

免费 AI IDE



