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

几个必须的连接选项:

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(默认): 同时有数字和字符串键,数据冗余,不推荐

选择技巧:

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 '%?%'"

几个常用技巧:

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:原子性、一致性、隔离性、持久性
// 适合:转账、订单+库存、多表联动等需要"全或无"的场景

事务的关键点:

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. 常见陷阱

8. 进阶:封装 Database 类

实际项目不会到处写 raw SQL,通常会用 ORM(如 Laravel Eloquent、Doctrine)或查询构造器(如 Illuminate Database)。它们底层都是 PDO,但提供更友好的 API:

小结

这一章你掌握了 PHP 数据库操作的全部核心:PDO 连接、查询、预处理防注入、CRUD、事务、错误处理。下一篇文件上传——让用户传图片、文档。

← 上一篇 PHP 面向对象

下一篇 PHP 文件上传

✈️💬