PDO 与 MySQL
预处理语句、防 SQL 注入。
35 分钟
PDO 与 MySQL
这一课你将能做到
- 用 PDO 正确连接 MySQL:异常模式、真实预处理、关联数组取回三件配置一步到位
- 用预处理语句完成增删改查,从原理上理解它为什么能根治 SQL 注入
- 用事务保证「下单扣库存」这类多步操作的原子性
为什么要学
业务数据最终都落在数据库,而 PHP 操作数据库的事实标准就是 PDO(PHP Data Objects):一套统一接口支持 MySQL、PostgreSQL、SQLite 等,配合预处理语句把 SQL 注入这个二十年的经典漏洞从源头堵死。你的 Spring Boot 项目里 MyBatis 的 #{} 占位符,底层思想和 PDO 预处理完全一致——学这一课也是在反向巩固 JDBC 知识。
人话:PDO 是「数据库万能遥控器」,预处理是「先把 SQL 模板交给数据库审核,再把数据当纯数据递过去」——数据和指令永不混合,注入自然无从谈起。
先建立图像
拼接 SQL 与预处理 SQL 的本质区别:
拼接(危险):
"SELECT * FROM users WHERE name = '" + $input + "'"
输入:' OR '1'='1
数据库收到:SELECT * FROM users WHERE name = '' OR '1'='1'
↑ 数据变成了指令!
预处理(安全):
SQL 模板先发:SELECT * FROM users WHERE name = ? → 数据库预编译
数据后发: [''' OR ''1''=''1'] → 只当字面量
数据库收到: 查找用户名恰好是 ' OR '1'='1 的人 → 查不到,安全
跟着做
步骤 1:规范建连
准备数据库与表(在 MySQL 客户端执行):
CREATE DATABASE IF NOT EXISTS demo CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE demo;
CREATE TABLE users (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL UNIQUE,
balance DECIMAL(10,2) NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;
db.php——把正确姿势固化下来:
<?php
declare(strict_types=1);
$pdo = new PDO(
'mysql:host=127.0.0.1;port=3306;dbname=demo;charset=utf8mb4',
'root',
'secret',
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // 出错抛异常
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // 关联数组取回
PDO::ATTR_EMULATE_PREPARES => false, // 用数据库真预处理
]
);
三个配置缺一不可:不开异常,错误静默;不设 FETCH_ASSOC,取回的数组带数字键;不关模拟预处理,某些场景(如 LIMIT 参数绑定)行为有差异且绕过了数据库端校验。
步骤 2:查询(占位符两种写法)
<?php
// 问号占位符:按位置传参
$stmt = $pdo->prepare('SELECT id, name, balance FROM users WHERE balance > ? ORDER BY balance DESC LIMIT ?');
$stmt->bindValue(1, 100.0, PDO::PARAM_STR);
$stmt->bindValue(2, 10, PDO::PARAM_INT); // LIMIT 必须绑成 int
$stmt->execute();
$rich = $stmt->fetchAll();
// 命名占位符:可读性更好
$stmt = $pdo->prepare('SELECT * FROM users WHERE name LIKE :kw AND id > :minId');
$stmt->execute(['kw' => '%张%', 'minId' => 0]); // LIKE 的 % 写在值里,不写进 SQL 模板
foreach ($stmt as $row) {
echo $row['id'], ': ', $row['name'], ' 余额 ', $row['balance'], PHP_EOL;
}
// 单行查询与取单值
$user = $pdo->prepare('SELECT * FROM users WHERE id = ?');
$user->execute([1]);
$row = $user->fetch(); // 没有时返回 false,判断用 === false 或 if ($row)
$count = (int) $pdo->query('SELECT COUNT(*) FROM users')->fetchColumn();