PDO 数据库操作

PDO(PHP Data Objects)是 PHP 推荐的数据库抽象层,支持多种数据库,代码更简洁安全。PDO 通过统一的 API 屏蔽了不同数据库之间的差异,使得切换数据库只需修改连接字符串(DSN),业务代码无需改动。这种设计在需要支持多种数据库或未来可能迁移数据库的项目中尤为重要。

为什么用 PDO?

特性MySQLiPDO
支持数据库仅 MySQL12+ 种数据库
预处理语句支持支持
命名参数不支持支持
异常处理手动自动(可配置)
切换数据库需重写只改 DSN
PDO 的命名参数(:username)比 MySQLi 的问号占位符(?)更具可读性,尤其在参数较多时,命名参数让 SQL 语句的意图一目了然,也不容易因参数顺序错误而引发 bug。

连接数据库

PDO 连接通过 DSN(Data Source Name)字符串指定数据库类型、主机、数据库名和字符集。推荐在创建连接时通过选项数组配置错误模式和默认获取模式。

PHP 实例
$dsn  = 'mysql:host=localhost;dbname=myapp;charset=utf8mb4';
$user = 'root';
$pass = 'your_password';

try { $pdo = new PDO($dsn, $user, $pass, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // 异常模式 PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // 默认关联数组 PDO::ATTR_EMULATE_PREPARES => false, // 使用真实预处理 ]); } catch (PDOException $e) { die("连接失败:" . $e->getMessage()); } ?> ``

PDO::ERRMODE_EXCEPTION 让 PDO 在出错时自动抛出 PDOException,无需手动检查每个操作的返回值。PDO::ATTR_EMULATE_PREPARES => false 强制使用数据库原生的预处理语句,安全性更高,也能让数据库对查询计划进行缓存优化。

增删改查

插入

PDO 支持两种占位符风格:问号占位符(?)和命名占位符(:name)。命名占位符在参数较多时更清晰,推荐优先使用。

`php <?php // 问号占位符 $stmt = $pdo->prepare("INSERT INTO users (username, email) VALUES (?, ?)"); $stmt->execute(["张三", "zhangsan@example.com"]); echo "新 ID:" . $pdo->lastInsertId();

// 命名占位符(更清晰) $stmt = $pdo->prepare("INSERT INTO users (username, email) VALUES (:username, :email)"); $stmt->execute([ ':username' => '李四', ':email' => 'lisi@example.com', ]); ?> `

lastInsertId() 返回最后插入行的自增 ID,注意这是 PDO 对象的方法,而不是 PDOStatement 的方法。使用命名占位符时,execute() 数组的键名可以带冒号(:username)也可以不带(username),两种写法都有效。

查询

PDO 提供了 query()(无参数查询)和 prepare() + execute()(带参数查询)两种方式,以及多种获取结果的方法。

`php <?php // 查询多行 $stmt = $pdo->query("SELECT * FROM users"); $users = $stmt->fetchAll(); // 返回所有行 foreach ($users as $user) { echo $user['username'] . "\n"; }

// 带参数查询 $stmt = $pdo->prepare("SELECT * FROM users WHERE id = :id"); $stmt->execute([':id' => 1]); $user = $stmt->fetch(); // 返回单行 echo $user['username'];

// 直接绑定到变量 $stmt = $pdo->prepare("SELECT * FROM users WHERE status = ?"); $stmt->execute(['active']); while ($row = $stmt->fetch()) { echo $row['username'] . "\n"; } ?> `

fetchAll() 一次性返回所有结果,适合数据量小的场景;fetch() 每次返回一行,适合逐行处理大量数据。PDO::FETCH_CLASS 模式可以将结果直接映射为指定类的对象,在 ORM 实现中非常有用。

更新与删除

`php <?php // 更新 $stmt = $pdo->prepare("UPDATE users SET email = :email WHERE id = :id"); $stmt->execute([':email' => 'new@example.com', ':id' => 1]); echo "影响行数:" . $stmt->rowCount();

// 删除 $stmt = $pdo->prepare("DELETE FROM users WHERE id = ?"); $stmt->execute([1]); ?> `

rowCount() 返回受影响的行数,对于 SELECT 语句,某些数据库驱动可能不返回正确的行数,应使用 fetchAll() 后取 count() 来获取查询结果数量。

事务

PDO 的事务 API 非常简洁,beginTransaction()commit()rollBack() 三个方法配合 try/catch 使用,可以优雅地处理事务逻辑。

`php <?php try { $pdo->beginTransaction();

$pdo->prepare("UPDATE accounts SET balance = balance - ? WHERE id = ?") ->execute([500, 1]);

$pdo->prepare("UPDATE accounts SET balance = balance + ? WHERE id = ?") ->execute([500, 2]);

$pdo->commit(); echo "转账成功"; } catch (PDOException $e) { $pdo->rollBack(); echo "失败:" . $e->getMessage(); } ?> `

事务中如果发生异常,catch 块中的 rollBack() 会撤销所有已执行的操作,保证数据一致性。注意 DDL 语句(如 CREATE TABLEALTER TABLE)在 MySQL 中会隐式提交事务,不能被回滚。

封装数据库类

实际项目中通常封装一个 DB 类,隐藏 PDO 的细节,提供更简洁的接口,并通过单例模式确保连接复用。

`php <?php class DB { private static ?PDO $instance = null;

public static function getInstance(): PDO { if (!self::$instance) { self::$instance = new PDO( 'mysql:host=localhost;dbname=myapp;charset=utf8mb4', 'root', 'password', [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, ] ); } return self::$instance; }

public static function query(string $sql, array $params = []): array { $stmt = self::getInstance()->prepare($sql); $stmt->execute($params); return $stmt->fetchAll(); }

public static function execute(string $sql, array $params = []): int { $stmt = self::getInstance()->prepare($sql); $stmt->execute($params); return $stmt->rowCount(); } }

// 使用 $users = DB::query("SELECT * FROM users WHERE status = ?", ['active']); DB::execute("UPDATE users SET last_login = NOW() WHERE id = ?", [1]); ?> `

这个简单的封装类可以根据项目需要进一步扩展,例如添加 insert() 方法返回 lastInsertId(),添加 transaction() 方法接受回调函数,添加查询日志功能等。在大型项目中,可以考虑使用 Eloquent、Doctrine 等成熟的 ORM 框架。

不同数据库的 DSN

PDO 最大的优势之一是支持多种数据库,只需修改 DSN 字符串即可切换,业务代码无需改动。

`php <?php // MySQL $dsn = 'mysql:host=localhost;dbname=myapp;charset=utf8mb4';

// PostgreSQL $dsn = 'pgsql:host=localhost;dbname=myapp';

// SQLite(无需用户名密码) $pdo = new PDO('sqlite:/path/to/database.db');

// SQL Server $dsn = 'sqlsrv:Server=localhost;Database=myapp';

SQLite 是一个轻量级的嵌入式数据库,不需要独立的数据库服务器,非常适合小型应用、原型开发和单元测试。在测试环境中,可以使用内存 SQLite(sqlite::memory:)来加速测试,每次测试后数据自动清空。

常见问题

Q1:PDO 的 ATTR_EMULATE_PREPARES 设为 truefalse 有什么区别?

A:true(默认)表示 PDO 在客户端模拟预处理,实际发送给数据库的是已替换参数的完整 SQL;false 表示使用数据库原生预处理,SQL 和参数分两次发送。原生预处理更安全(参数永远不会被解析为 SQL),也能让数据库缓存查询计划提升性能,推荐设为 false。但某些旧版 MySQL 驱动对原生预处理支持不完整,如遇问题可临时改回 true

Q2:fetch()fetchAll() 应该如何选择?

A:fetchAll() 一次性将所有结果加载到内存,适合数据量小(几百行以内)的场景,代码简洁;fetch() 每次只加载一行,内存占用恒定,适合处理大量数据(几千行以上)。如果只需要第一行结果,用 fetch()fetchAll()[0] 更高效,因为不需要加载其余行。

Q3:如何防止 PDO 连接在长时间空闲后断开(MySQL gone away)?

A:MySQL 默认会在连接空闲超过 wait_timeout(默认 8 小时)后断开。解决方案:① 在执行查询前检查连接是否有效,失效则重连;② 使用连接池(如 PHP-FPM 的持久连接 PDO::ATTR_PERSISTENT,但需谨慎使用);③ 定期执行 SELECT 1 保持连接活跃;④ 在框架层面实现自动重连逻辑。

Q4:PDO 能防止所有类型的 SQL 注入吗?

A:正确使用 PDO 预处理语句可以防止参数值注入,但有两个盲区:① 表名和列名不能参数化,如果需要动态表名/列名,必须用白名单验证(in_array($column, $allowedColumns));② LIKE 查询中的通配符(%_)不会被转义,如果用户输入包含这些字符,可能导致全表扫描,需要手动转义:str_replace(['%', '_'], ['\\%', '\\_'], $input)`。