PHP的PDO(PHP Data Objects)扩展从5.1版本起内置,提供了一套轻量且一致的数据访问抽象层。无论后端是MySQL、PostgreSQL还是SQLite,开发者调用的都是同一组方法,不必因数据库切换而重写全部SQL逻辑。PDO最核心的价值在于预处理语句,它将SQL模板与参数分开传输,数据库先编译模板,再绑定数值,从而天然隔绝了绝大多数SQL注入途径。

一、建立PDO连接
使用PDO的第一步是实例化PDO类,传入数据源名称(DSN)、用户名与密码。DSN包含了数据库类型、主机、库名以及连接选项。推荐在构造时通过属性数组设定错误模式为异常,并指定字符集,避免乱码与静默失败。
下面的示例展示了一个最基础的MySQL连接写法。我们把ERRMODE设成EXCEPTION,这样执行出错时会抛出PDOException,方便用try-catch统一处理;同时设定UTF8编码,保证中文数据正常存取。
<?php
$dsn = 'mysql:host=127.0.0.1;dbname=test_db;charset=utf8';
$user = 'root';
$pass = 'secret';
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
];
try {
$pdo = new PDO($dsn, $user, $pass, $options);
echo '连接成功';
} catch (PDOException $e) {
echo '连接失败: ' . $e->getMessage();
}
?>
二、使用预处理执行查询
PDO的预处理分为两种占位符:问号占位与命名占位。问号占位按位置绑定,命名占位按key绑定,后者在参数多时可读性更好。预处理对象由prepare方法返回,执行时调用execute并传入参数数组。
以下代码演示用命名占位符查询用户表。我们先写好带:email的SQL,再把实际值放进关联数组传给execute。fetch方法默认按之前设定的FETCH_ASSOC返回纯数组,没有多余的数字索引。
<?php $sql = 'SELECT id, name FROM users WHERE email = :email LIMIT 1'; $stmt = $pdo->prepare($sql); $stmt->execute([':email' => 'admin@ipipp.com']); $row = $stmt->fetch(); print_r($row); ?>
若使用问号占位,写法变为位置数组,注意索引从0开始。两种写法在性能上无本质差异,团队内部统一风格即可。
<?php $sql = 'SELECT id, name FROM users WHERE id = ?'; $stmt = $pdo->prepare($sql); $stmt->execute([10]); $row = $stmt->fetch(); print_r($row); ?>
三、插入、更新与删除操作
增删改同样依赖prepare与execute,只是SQL关键字不同。因为PDOException会在出错时抛出,我们可以用单行受影响行数判断操作是否命中记录。rowCount方法返回受影响的行数,对SELECT它在某些数据库上不一定可靠,但对写操作是准确的。
下面的例子演示插入一条留言。我们把内容、作者作为参数绑定,执行后通过rowCount确认是否写入成功,并借助lastInsertId拿到自增主键,便于后续关联操作。
<?php
$sql = 'INSERT INTO messages (content, author) VALUES (:content, :author)';
$stmt = $pdo->prepare($sql);
$stmt->execute([
':content' => '今天天气不错',
':author' => '小明'
]);
if ($stmt->rowCount() > 0) {
$newId = $pdo->lastInsertId();
echo '插入成功,新ID为' . $newId;
}
?>
更新与删除的写法完全一致,只是把INSERT换成UPDATE或DELETE。务必在WHERE中继续使用占位符,切勿直接拼接变量,否则就失去了PDO的防护意义。
<?php $sql = 'UPDATE messages SET content = :content WHERE id = :id'; $stmt = $pdo->prepare($sql); $stmt->execute([':content' => '已编辑', ':id' => 5]); echo '更新了' . $stmt->rowCount() . '行'; $sql = 'DELETE FROM messages WHERE id = :id'; $stmt = $pdo->prepare($sql); $stmt->execute([':id' => 5]); echo '删除了' . $stmt->rowCount() . '行'; ?>
四、事务处理与异常捕获
当一组写操作必须同时成功或同时失败时,就要用事务。PDO通过beginTransaction、commit和rollBack控制。发生任意一步异常,catch块里调用rollBack即可回滚,保证数据一致。
示例里先开启事务,连续插入订单与订单项,若中途出错则整体撤销。注意PDO的MYSQL驱动默认是隐式提交,只有显式beginTransaction才会关闭自动提交。
<?php
try {
$pdo->beginTransaction();
$pdo->prepare('INSERT INTO orders (user_id) VALUES (?)')->execute([3]);
$orderId = $pdo->lastInsertId();
$pdo->prepare('INSERT INTO order_items (order_id, sku) VALUES (?, ?)')->execute([$orderId, 'A100']);
$pdo->commit();
echo '下单完成';
} catch (PDOException $e) {
$pdo->rollBack();
echo '下单失败,已回滚:' . $e->getMessage();
}
?>
五、常见误区与注意点
一个常见错误是在循环中反复prepare同一条SQL。正确做法是在循环外prepare一次,循环内只execute不同参数,这样数据库只需编译一次,效率明显提升。另一个误区是以为用了PDO就绝对安全,若仍用字符串拼接SQL再交给query执行,注入风险依旧存在。
此外,PDO的模拟预处理(ATTR_EMULATE_PREPARES)在旧版MySQL下默认开启,它其实是在客户端拼SQL。为了真正利用服务端预处理,可显式关闭该属性。但对多数应用而言,保持默认并坚持参数绑定已足够安全。
<?php $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); ?>
| 对比项 | PDO | mysqli |
|---|---|---|
| 数据库支持 | 多种 | 仅MySQL |
| 占位符风格 | 问号与命名 | 仅问号 |
| 事务接口 | 统一方法 | 过程与对象混合 |
掌握上述连接、查询、写操作、事务与避坑要点后,你就能在PHP项目中用PDO稳当地完成绝大多数数据库任务。它不会替你设计表结构,但能让数据访问层更干净、更安全。