PHP 连接数据库最稳妥的做法是使用 PDO 预处理语句(PDO::prepare 配合 bindParam),它同时兼容 MySQL、PostgreSQL 等多种驱动,并能天然防住 SQL 注入。本文把 PDO、MySQLi 以及"裸写 mysql 扩展"三种思路放在一起对比,给出可复制的代码与常见踩坑点。无论你用 Laravel、ThinkPHP 还是原生写法,底层都绕不开"建立连接、设置字符集、执行参数化查询、处理事务"这四步。读完你能在十分钟内把一段易注入的拼接 SQL 改写成安全写法,并理解为什么字符集错配会让中文变成乱码。

一、思路对比:mysql、mysqli 与 PDO 怎么选
PHP 连接数据库有三条历史路线。最早是已废弃的 mysql_* 函数,它在 PHP 7 起被彻底移除,任何新项目都不应再出现 mysql_connect 这类调用。其次是 MySQLi(MySQL Improved),它是官方为 MySQL 专门打造的扩展,支持面向对象和过程两种写法,能做预处理、事务和存储过程。最后是 PDO(PHP Data Objects),它是一层抽象的数据库访问层,一次学会后可换数据库类型而不必重写业务代码。
实际选型时,如果你只连 MySQL 且追求极致简单,MySQLi 够用;但绝大多数团队更推荐 PDO,因为它用同一套接口屏蔽了不同数据库的差异。无论选哪个,都不要再回到裸拼接 SQL 的老路。下面这张对照表给出三种方式在连接、预处理、事务上的关键差异,帮助你快速判断。
// 三种连接写法对照
// 1) 已废弃的 mysql 扩展(不要用)
// $conn = mysql_connect('localhost','user','pass');
// 2) MySQLi 面向对象
$mysqli = new mysqli('localhost','user','pass','testdb');
$mysqli->set_charset('utf8mb4');
// 3) PDO(推荐)
$dsn = 'mysql:host=localhost;dbname=testdb;charset=utf8mb4';
$pdo = new PDO($dsn, 'user', 'pass', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);
想快速查函数签名和常量,可随时翻 PHP 速查手册,里面把 PDO、mysqli 的方法都列成了速查表,比翻官方文档更省时间。
二、用 PDO 建立连接并设置字符集
字符集是 PHP 连接数据库时最容易翻车的点。很多新手用了 utf8 却仍然乱码,原因是 MySQL 的 utf8 实际只支持三字节,存不了 emoji 和部分生僻字,正确写法是 utf8mb4。在 DSN 里直接追加 charset=utf8mb4 比事后调用 set names 更安全,因为前者在握手阶段就生效,避免了一次额外的查询。
连接参数建议带上错误模式与默认取数模式。PDO::ATTR_ERRMODE 设为 ERRMODE_EXCEPTION 能让错误以异常形式抛出,方便用 try/catch 统一处理;ATTR_DEFAULT_FETCH_MODE 设成 FETCH_ASSOC 则让查询结果直接是关联数组,少写一堆数字索引。下面给出一份生产可用的连接封装。
<?php
function getPdo(): PDO {
$dsn = 'mysql:host=127.0.0.1;port=3306;dbname=testdb;charset=utf8mb4';
$pdo = new PDO($dsn, 'app_user', 'app_pass', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false, // 用真实预处理,防注入更彻底
]);
return $pdo;
}
注意 ATTR_EMULATE_PREPARES 设为 false 会强制使用数据库原生预处理,避免某些驱动在模拟模式下仍拼接参数的隐患。连不上时优先检查 host 是否可达、账号是否有远程权限,而不是反复改 charset。
三、参数化查询:彻底告别 SQL 注入
SQL 注入的本质是"把用户输入当成了 SQL 指令"。当查询写成 "SELECT * FROM users WHERE name='$name'" 时,攻击者输入 admin' OR '1'='1 就会把条件永远置真。参数化查询(预处理语句)把"SQL 模板"和"数据"分两次发给数据库:模板先编译,占位符再单独绑定,数据永远不会被当作指令解析,注入自然无从下手。
占位符有两种写法:命名占位符 :name 和问号占位符 ?。命名方式可读性更好,适合字段多的场景。执行时通过 bindParam 或 execute 传入数组,PDO 会负责转义与类型处理。下面演示一个登录校验,注意我们永远不拼接密码到 SQL 里,而是交给数据库比对。
$pdo = getPdo();
$stmt = $pdo->prepare('SELECT id, password_hash FROM users WHERE email = :email');
$stmt->execute([':email' => $email]);
$user = $stmt->fetch();
if ($user && password_verify($password, $user['password_hash'])) {
echo '登录成功,用户ID:' . $user['id'];
} else {
echo '邮箱或密码错误';
}
如果业务逻辑必须用 LIKE 模糊匹配,记得给参数前后加上百分号再绑定,例如 $stmt->execute([':kw' => "%$kw%"]),而不是把 % 写死在 SQL 模板里,这样既安全又灵活。
四、事务与完整示例:保证多步写操作原子性
多步写操作如果不加事务,很容易出现"扣了库存却没生成订单"的中间态。PDO 用 beginTransaction、commit、rollBack 三件套保证原子性:要么全部成功,要么整体回滚。典型场景是转账——从 A 扣钱和给 B 加钱必须同时成立,任意一步失败都要撤销前一步。
下面的完整示例把前面学到的连接、预处理、事务串起来,演示一个"下单并扣减库存"的流程,并展示异常时如何回滚。
<?php
$pdo = getPdo();
try {
$pdo->beginTransaction();
$pdo->prepare('UPDATE products SET stock = stock - ? WHERE id = ? AND stock >= ?')
->execute([1, $productId, 1]);
$pdo->prepare('INSERT INTO orders(user_id, product_id) VALUES(?, ?)')
->execute([$userId, $productId]);
$pdo->commit();
echo '下单成功';
} catch (Exception $e) {
$pdo->rollBack();
echo '下单失败:' . $e->getMessage();
}
想看一个从建表、注册、登录到下单的完整项目,建议跟着 PHP+MySQL Web 教程 动手做一遍,把事务和预处理真正跑通。务必在 commit 之前不要输出任何会破坏事务状态的调试信息。
五、常见陷阱排查:乱码、注入与连接泄漏
PHP 连接数据库的坑大多集中在三处。第一是字符集错配:数据库、连接、页面输出三处 charset 不一致就会乱码,最稳妥是三者统一为 utf8mb4,并在 PDO 的 DSN 中固定。第二是误以为转义函数能替代预处理:addslashes 或 mysql_real_escape_string 在宽字节或特殊字符下仍有绕过风险,参数化才是正解。第三是连接泄漏:在循环里反复 new PDO 却不关闭,会把数据库最大连接数耗尽。
排查时先确认报错信息,PDO 默认静默,所以务必把 ERRMODE 打开;再检查 SQL 是否真的走了预处理。下面列出几条最常用的诊断手段。
// 诊断:打印驱动信息与会话字符集
echo $pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
$row = $pdo->query("SHOW VARIABLES LIKE 'character_set_connection'")->fetch();
print_r($row);
// 连接泄漏排查:避免每次请求都新建连接,用单例或连接池复用

实际部署时,先在 DSN 里配置好字符集与错误模式,运行一段最小连接脚本验证能否成功连上;若连接失败或中文乱码,检查账号权限与三处 charset 是否一致,再用预处理重写易注入的拼接 SQL 从而修复安全隐患。代码按官方文档整理,可直接复用。
总结
PHP 连接数据库的核心不是"连上就行",而是"安全地连、正确地查"。PDO 凭借统一的接口和原生的参数化查询成为首选,MySQLi 则适合只服务 MySQL 的轻量场景。记住四件事:字符集用 utf8mb4 且在 DSN 里声明、查询一律走预处理、多步写操作包事务、连接要复用别泄漏。把这些落到代码里,你的应用就迈过了安全与稳定的第一道门槛,这也是编程狮在数据库安全专题里反复强调的底线。
延伸学习
- PHP 零基础入门课程 系统学 PHP
- PHP 学习路线图笔记 看 PHP 学习路线
- PHP 教程 复习基础
常见问题
Q:PDO 和 MySQLi 到底该选哪个?
A:只连 MySQL 且追求简单可选 MySQLi;需要换数据库或重视统一接口,PDO 更优。两者都支持预处理,关键不在选谁,而在是否用参数化查询防注入。
Q:为什么设置了 utf8 还是中文乱码?
A:MySQL 的 utf8 仅三字节,存不了 emoji 和某些生僻字。应统一改用 utf8mb4,并在 PDO 的 DSN 中写 charset=utf8mb4,同时保证表、字段、页面输出一致。
Q:预处理就绝对安全吗?
A:预处理能杜绝绝大多数 SQL 注入,但表名、列名不能用占位符,仍需白名单校验。另外排序方向、LIMIT 数值等也要用整数强转,不能把用户输入直接拼进 SQL 模板。

免费 AI IDE



