2.2 数据库操作 PDO:寄存总库的存取凭证


文档摘要

2.2 数据库操作 PDO:寄存总库的存取凭证 本节摘要:PDO 是 PHP 官方的数据库统一接口,一套 API 对接 MySQL、PostgreSQL 等多种库。本节围绕寄存总库的日常作业展开:连接与错误模式配置、预处理与参数绑定(顺带讲清它为什么天然防注入)、结果集的三种取法、事务的状态流转,最后把 1.7 节的报名表落进真实的表里。这是全册技术含金量最高的一节,2.5 性能与 2.6 安全都建立在它之上。 连接:先定规矩再干活 建连接的代码不长,但每个选项都是规矩: 三个选项逐一说明。 让数据库错误以异常形式浮出水面,静默失败的连接是排错噩梦; 让每行数据以键值数组返回,列名即键; 关掉后预处理由数据库引擎亲自执行,参数与 SQL 结构彻底分离,防注入的效果才是铁的。

2.2 数据库操作 PDO:寄存总库的存取凭证

本节摘要:PDO 是 PHP 官方的数据库统一接口,一套 API 对接 MySQL、PostgreSQL 等多种库。本节围绕寄存总库的日常作业展开:连接与错误模式配置、预处理与参数绑定(顺带讲清它为什么天然防注入)、结果集的三种取法、事务的状态流转,最后把 1.7 节的报名表落进真实的表里。这是全册技术含金量最高的一节,2.5 性能与 2.6 安全都建立在它之上。

连接:先定规矩再干活

建连接的代码不长,但每个选项都是规矩:

<?php $dsn = 'mysql:host=127.0.0.1;port=3306;dbname=station;charset=utf8mb4'; $pdo = new PDO($dsn, 'station_user', '强口令', [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // 出错即抛异常,交给 1.9 的机制 PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // 结果默认取关联数组 PDO::ATTR_EMULATE_PREPARES => false, // 用数据库真预处理,不用客户端模拟 ]);

三个选项逐一说明。ERRMODE_EXCEPTION 让数据库错误以异常形式浮出水面,静默失败的连接是排错噩梦;FETCH_ASSOC 让每行数据以键值数组返回,列名即键;EMULATE_PREPARES 关掉后预处理由数据库引擎亲自执行,参数与 SQL 结构彻底分离,防注入的效果才是铁的。字符集直接写进 DSN,与 1.5 节"三端一致"呼应。

先用一张表当总库货架,本节全程用它:

CREATE TABLE passengers ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, name VARCHAR(20) NOT NULL, email VARCHAR(100) NOT NULL, seat VARCHAR(10) NOT NULL DEFAULT 'economy', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

预处理:参数只当数据,永不当代码

把 1.7 节的报名记录写进总库,用预处理写法:

<?php $sql = 'INSERT INTO passengers (name, email, seat) VALUES (?, ?, ?)'; $stmt = $pdo->prepare($sql); // 先交作业模板 $stmt->execute([$name, $email, $seat]); // 再交参数,顺序对应问号 echo '新增编号:', $pdo->lastInsertId(), "\n"; // 自增主键 echo '影响行数:', $stmt->rowCount(), "\n"; // 1

命名的写法可读性更好,参数多一些时尤其明显:

<?php $sql = 'UPDATE passengers SET seat = :seat WHERE id = :id'; $stmt = $pdo->prepare($sql); $stmt->execute(['seat' => 'business', 'id' => 7]);

预处理为什么能防注入?看数据流。传统拼接写法把用户输入直接嵌进 SQL 文本,输入 ' or '1'='1 之类的片段时,数据库分不清哪部分是语句、哪部分是数据,攻击者由此改写语句结构。预处理把作业分成两步:先提交只有占位符的模板,数据库先把语句结构定死;参数后到,无论内容多邪恶都只按"数据"填空,结构永远不变。下图把两条数据流并排画出,差异一目了然:

图:拼接与预处理的两条数据流对比

图:拼接与预处理的两条数据流对比

查询与取值:三种取法

读操作同样走预处理,取值按需选模式:

<?php // 单行 $stmt = $pdo->prepare('SELECT * FROM passengers WHERE id = :id'); $stmt->execute(['id' => 7]); $row = $stmt->fetch(); // 关联数组或 false echo $row['name'] ?? '查无此人', "\n"; // 多行 $stmt = $pdo->query('SELECT name, seat FROM passengers ORDER BY id DESC LIMIT 10'); while ($r = $stmt->fetch()) { echo "{$r['name']} 选了 {$r['seat']}\n"; } // 一次全取(结果集不大时顺手) $rows = $stmt->fetchAll(); echo '共 ', count($rows), " 行\n";

注意分寸:fetchAll 把全部结果装进内存,几百行无妨、几十万行会把月台撑爆——大结果集用 fetch 逐行流式处理,或交给 2.4 节的生成器。

事务:要么全成,要么全不算

"旅客买票"要写两张表:订单表加一行、座位表改状态。写到一半崩了怎么办?事务保证原子性:要么两步都生效,要么全部回滚。它的状态流转如下:

代码上的固定节奏是"开、做、验、定":

<?php $pdo->beginTransaction(); try { $pdo->prepare('INSERT INTO orders (passenger_id, trip, fare) VALUES (?, ?, ?)') ->execute([$pid, 'G102', 240]); $orderId = $pdo->lastInsertId(); $stmt = $pdo->prepare('UPDATE seats SET state = ? WHERE no = ? AND state = ?'); $stmt->execute(['sold', '12A', 'free']); if ($stmt->rowCount() !== 1) { // 座位没锁到:抢注失败 throw new RuntimeException('座位已被他人锁定'); } $pdo->commit(); // 两步一起落定 echo "下单成功,订单号 {$orderId}\n"; } catch (Throwable $e) { $pdo->rollBack(); // 两步一起作废 echo '已回滚:', $e->getMessage(), "\n"; }

那个 AND state = ?rowCount 核对的小技巧值得品味:它把"检查再占用"合成了一句原子操作,两个并发请求只有一个能锁到座位,比先查后改的写法稳得多。资金、库存、配额这类"一降一升"的联动写操作,一律套用本模式。

实战:报名表数据正式入库

把 1.7 的 signup.php 第三步从落文件换成落总库,其他三步原样保留:

<?php // ……第一、二步与 1.7 相同(收件定型、逐项校验)…… try { $stmt = $pdo->prepare( 'INSERT INTO passengers (name, email, seat) VALUES (:n, :e, :s)' ); $stmt->execute(['n' => $name, 'e' => $email, 's' => $seat]); } catch (PDOException $e) { if ($e->getCode() === '23000') { // 唯一键冲突:邮箱重复 exit('该邮箱已登记过'); } logError('signup', $e->getMessage()); // 1.9 的登记函数 http_response_code(500); exit('月台临时故障,请稍后再试'); } echo '受理成功,编号 ', $pdo->lastInsertId();

预期结果:首次提交得到自增编号;同一邮箱重复提交得到友好提示而不是报错页。到这里,"进站、安检、入库、回执"的完整服务链路在无框架条件下全线贯通——第 3 章框架所做的,就是把这条链路的重复劳动自动化。

常见坑

⚠️ LIMIT 子句绑参数失效:部分驱动把整数参数绑成字符串导致语法错误,遇到时对整数强制转型或用 bindValue 加类型。

⚠️ 长连接与单例滥用:一个请求一份连接足够,全局复用一个 PDO 单例在长脚本里会遇到"服务器断开了空闲连接",重连逻辑要备好。

本节要点回顾

  • 连接四选项:异常模式、关联取法、关模拟预处理、DSN 定字符集,一次配齐终身受益。
  • 预处理是铁律:带输入的查询必预处理,结构与数据分离,注入无门。
  • 取值看体量:单行 fetch、逐行流式处理大结果集,fetchAll 只给小结果。
  • 事务三拍子:开、做、验,rowCount 核对后 commit,任何异常 rollback。
  • 原子占用技巧UPDATE ... WHERE 条件 加行数核对,把竞态消灭在一句 SQL 里。

下一节转向出站方向:响应报文的装配车间,状态码、头部与 JSON 的规矩。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U