PHP 数据库(PDO)
真实项目的数据要持久化——存进数据库。PHP 通过 PDO(PHP Data Objects) 扩展访问数据库,它是推荐的标准方式:统一接口、支持预处理语句(从根本上防 SQL 注入)、支持事务、跨多种数据库。请永远用 PDO,不要再用旧的 mysqli 或 mysql 扩展。
1. 连接数据库
连接用 DSN(Data Source Name) 描述,不同数据库 DSN 不同:
<?php
// DSN(Data Source Name):描述连接信息
$dsn = "mysql:host=localhost;dbname=test;charset=utf8mb4";
// charset=utf8mb4 必加!支持完整 Unicode(含 emoji)
$user = "root";
$pass = "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, // 关闭模拟预处理
]);
echo "连接成功";
} catch (PDOException $e) {
// 不要把详细错误显示给用户(信息泄露)
error_log("DB 连接失败:" . $e->getMessage());
die("数据库暂不可用");
}
// 其他数据库 DSN 示例:
// SQLite: sqlite:/path/to/db.sqlite
// PostgreSQL: pgsql:host=localhost;dbname=test
// SQL Server: sqlsrv:Server=localhost;Database=test几个必须的连接选项:
- charset=utf8mb4:支持完整 Unicode(中文、emoji)。不要用 utf8(它是 utf8mb3 的别名,存 emoji 会报错)。
- ERRMODE_EXCEPTION:出错抛异常,方便 try/catch。
- FETCH_ASSOC:默认返回关联数组(默认是 BOTH,数据冗余)。
- EMULATE_PREPARES = false:关闭模拟预处理,用真正的数据库预处理(更安全、类型更准)。
2. 查询与 fetch
查询用 query()(无参数)或 prepare()(带参数,推荐)。结果用 fetch / fetchAll / fetchColumn 三种方式取:
<?php
// query:用于无参数的 SELECT,返回 PDOStatement
$stmt = $pdo->query("SELECT id, name, email FROM users");
// 三种主要 fetch 方式
// 1. fetch:取一行(指针下移)
while ($row = $stmt->fetch()) {
echo $row["name"] . "\n";
}
// 2. fetchAll:一次性取所有行
$users = $pdo->query("SELECT * FROM users")->fetchAll();
foreach ($users as $u) {
echo $u["name"];
}
// 3. fetchColumn:取一列的第一行
$count = $pdo->query("SELECT COUNT(*) FROM users")->fetchColumn();
echo $count;
// FETCH_ASSOC / FETCH_OBJ / FETCH_NUM
// ASSOC: 关联数组(推荐)
// OBJ: 对象(用 $row->name 访问)
// NUM: 数字下标($row[0])
// FETCH_BOTH(默认): 同时有数字和字符串键,数据冗余,不推荐选择技巧:
- 大数据集:用
fetch()+ while 循环,一行一行取,内存友好。 - 小数据集:用
fetchAll()一次取完,代码简洁。 - 单值(count、sum):用
fetchColumn()。
3. 预处理语句(最重要!)
预处理(prepared statement)是防止 SQL 注入的根本手段。原理:先发 SQL 模板(占位符),再传参数,数据库分别处理结构和数据,参数自动转义,根本不可能注入:
<?php
// ⚠️ 永远不要拼接 SQL!永远用 prepare!
// ❌ 反面教材(SQL 注入漏洞):
// $sql = "SELECT * FROM users WHERE name='" . $_POST["name"] . "'";
// $pdo->query($sql);
// ✅ 正确写法:预处理语句
$stmt = $pdo->prepare(
"SELECT id, name, email FROM users WHERE age > :min_age ORDER BY id"
);
// 命名参数(:name)
$stmt->execute(["min_age" => 18]);
$users = $stmt->fetchAll();
// 问号参数(?)
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([42]);
$user = $stmt->fetch();
// bindValue / bindParam(显式绑定 + 类型)
$stmt = $pdo->prepare("INSERT INTO users (name, age) VALUES (:name, :age)");
$stmt->bindValue(":name", "小明", PDO::PARAM_STR);
$stmt->bindValue(":age", 25, PDO::PARAM_INT);
$stmt->execute();核心铁律:永远不要拼接 SQL!永远用 prepare! 无论参数看起来多么"安全"(哪怕是内部生成的 ID),都养成用 prepare 的习惯。
4. CRUD 完整示例
增(Create)、查(Read)、改(Update)、删(Delete)是数据库四大基本操作:
<?php
// CREATE:插入
$stmt = $pdo->prepare(
"INSERT INTO users (name, email, age) VALUES (?, ?, ?)"
);
$stmt->execute(["小明", "xm@test.com", 25]);
echo "新用户 ID:" . $pdo->lastInsertId();
// READ:查询
$stmt = $pdo->prepare("SELECT * FROM users WHERE age >= ? ORDER BY age");
$stmt->execute([18]);
$users = $stmt->fetchAll();
// UPDATE:更新
$stmt = $pdo->prepare("UPDATE users SET age = ? WHERE id = ?");
$stmt->execute([26, 1]);
echo "影响行数:" . $stmt->rowCount();
// DELETE:删除
$stmt = $pdo->prepare("DELETE FROM users WHERE id = ?");
$stmt->execute([1]);
// LIKE 模糊查询
$stmt = $pdo->prepare("SELECT * FROM users WHERE name LIKE ?");
$stmt->execute(["%小明%"]);
// 注意:LIKE 的 % 要在 PHP 里拼,不能写 "... LIKE '%?%'"几个常用技巧:
- lastInsertId():获取自增主键的最后一个 ID(插入后立即调用)。
- rowCount():获取 INSERT/UPDATE/DELETE 影响的行数。
- LIKE 模糊查询:
%通配符必须在 PHP 里拼进参数,不能写进 SQL 模板。 - 批量插入:一条 SQL 插入多行,
INSERT INTO users (name) VALUES (?), (?), (?)。
5. 事务
事务保证一组操作要么全部成功,要么全部失败。典型场景是转账、订单+库存扣减:
<?php
// 事务:多条语句要么全成功,要么全回滚
try {
$pdo->beginTransaction();
$pdo->prepare("UPDATE accounts SET balance = balance - ? WHERE id = ?")
->execute([100, 1]);
$pdo->prepare("UPDATE accounts SET balance = balance + ? WHERE id = ?")
->execute([100, 2]);
$pdo->commit();
echo "转账成功";
} catch (PDOException $e) {
// 任一步失败,整个事务回滚
$pdo->rollBack();
error_log("转账失败:" . $e->getMessage());
echo "转账失败,已回滚";
}
// 事务保证 ACID:原子性、一致性、隔离性、持久性
// 适合:转账、订单+库存、多表联动等需要"全或无"的场景事务的关键点:
- 引擎必须是 InnoDB(MyISAM 不支持事务,已逐渐被淘汰)。
- 事务期间不要有耗时的网络请求,会锁住数据。
- 异常一定要
rollBack(),否则连接断开才回滚,期间数据不一致。 - 嵌套事务用 PDO::savepoint,大型项目用 ORM 帮你管。
6. 错误处理
开启 ERRMODE_EXCEPTION 后,所有错误都通过异常抛出。错误处理的核心原则是分级:
<?php
// 必须开启 ERRMODE_EXCEPTION(连接时设置)
// PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
try {
$pdo->prepare("SELECT * FROM nonexistent_table")->execute();
} catch (PDOException $e) {
// 错误信息分级处理:
// - 系统级错误(连接失败、SQL 语法错) → 记日志 + 友好提示
// - 业务错误(如重复键) → 转成业务异常处理
if ($e->getCode() === "23000") {
// 唯一约束冲突(重复键)
throw new UserExistsException("用户名已存在");
}
error_log("DB 错误:" . $e->getMessage());
throw new RuntimeException("数据库错误");
}
// 不要做的事:
// ❌ echo $e->getMessage(); // 泄露表名、SQL,容易被攻击
// ❌ die($e); // 同上
// ❌ 不处理异常 // 程序崩溃 + 信息泄露7. 常见陷阱
- IN 子句:不能直接
WHERE id IN (?)。要动态拼占位符:$placeholders = implode(",", array_fill(0, count($ids), "?")),然后execute($ids)。 - LIMIT ? 在模拟预处理下可能出错:关闭
EMULATE_PREPARES,或bindValue(..., PDO::PARAM_INT)。 - 表名/列名不能作为参数:预处理只能绑定值。如果一定要动态表名,用白名单校验。
- N+1 查询:循环里查数据库是性能杀手。用 JOIN 或 IN 一次查完。
8. 进阶:封装 Database 类
实际项目不会到处写 raw SQL,通常会用 ORM(如 Laravel Eloquent、Doctrine)或查询构造器(如 Illuminate Database)。它们底层都是 PDO,但提供更友好的 API:
- 查询构造器:链式方法构造 SQL,如
DB::table配合 where 链式调用,语法接近自然语言。 - ORM:一行映射一个类,把表当成对象操作,如
User::where类似的链式调用。 - 迁移工具:用 PHP 代码管理数据库结构(git 友好),不再手动改表。
- 建议:新手先用 PDO 打基础,做项目再上 Eloquent。
小结
这一章你掌握了 PHP 数据库操作的全部核心:PDO 连接、查询、预处理防注入、CRUD、事务、错误处理。下一篇文件上传——让用户传图片、文档。
← 上一篇 PHP 面向对象
下一篇 PHP 文件上传 →