数据库操作,是Web开发中最核心的功能之一。用户数据、文章内容、订单信息、日志记录……几乎所有的Web应用都需要和数据库打交道。
PHP提供了多种数据库操作方式:
- mysql扩展:最早的MySQL扩展,PHP 5.5已废弃,PHP 7已移除
- mysqli扩展:MySQL的增强版扩展,支持面向对象和过程式两种风格
- PDO(PHP Data Objects):数据库抽象层,支持多种数据库(MySQL、PostgreSQL、SQLite等),统一的API
推荐使用PDO,因为:
- 支持多种数据库,切换数据库不需要改太多代码
- 支持预处理语句(Prepared Statements),防止SQL注入
- 面向对象的API,代码更优雅
- 支持事务
- 性能不错
今天,我们来详细学习PDO的使用,从基础连接到增删改查,从预处理语句到事务,从错误处理到性能优化,帮你掌握PHP数据库操作的最佳实践。
PDO基础
连接数据库
用PDO连接数据库,需要创建PDO对象:
// MySQL连接
$dsn = 'mysql:host=localhost;dbname=test;charset=utf8mb4';
$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) {
echo '连接失败:' . $e->getMessage();
exit;
}DSN(Data Source Name)格式:
- MySQL:
mysql:host=localhost;dbname=test;charset=utf8mb4;port=3306 - PostgreSQL:
pgsql:host=localhost;dbname=test - SQLite:
sqlite:/path/to/database.db - SQL Server:
sqlsrv:Server=localhost;Database=test
重要选项:
PDO::ATTR_ERRMODE:错误模式
- PDO::ERRMODESILENT:静默模式(默认),不报错,需要手动检查 - PDO::ERRMODEWARNING:警告模式,发出EWARNING - PDO::ERRMODEEXCEPTION:异常模式(推荐),抛出PDOException
PDO::ATTRDEFAULTFETCH_MODE:默认获取模式
- PDO::FETCHASSOC:关联数组 - PDO::FETCHNUM:索引数组 - PDO::FETCHOBJ:对象 - PDO::FETCHBOTH:两者都有(默认)
PDO::ATTREMULATEPREPARES:是否模拟预处理
- false:使用数据库原生的预处理(推荐,更安全) - true:PDO模拟预处理(某些数据库不支持原生预处理时用)
关闭连接
PDO连接在脚本结束时会自动关闭,也可以手动关闭:
$pdo = null; // 关闭连接执行SQL语句
exec():执行无结果集的SQL
exec()用于执行INSERT、UPDATE、DELETE等没有结果集的SQL,返回受影响的行数。
// INSERT
$sql = "INSERT INTO users (name, email) VALUES ('张三', 'zhangsan@example.com')";
$affectedRows = $pdo->exec($sql);
echo '插入了' . $affectedRows . '行,新ID:' . $pdo->lastInsertId();
// UPDATE
$sql = "UPDATE users SET name = '李四' WHERE id = 1";
$affectedRows = $pdo->exec($sql);
echo '更新了' . $affectedRows . '行';
// DELETE
$sql = "DELETE FROM users WHERE id = 1";
$affectedRows = $pdo->exec($sql);
echo '删除了' . $affectedRows . '行';注意:exec()不适合SELECT语句,因为它不返回结果集。SELECT用query()或预处理语句。
query():执行有结果集的SQL
query()用于执行SELECT等有结果集的SQL,返回PDOStatement对象。
$sql = "SELECT * FROM users";
$stmt = $pdo->query($sql);
// 获取所有行
$users = $stmt->fetchAll();
foreach ($users as $user) {
echo $user['name'] . ' - ' . $user['email'] . "\n";
}
// 逐行获取
while ($user = $stmt->fetch()) {
echo $user['name'] . ' - ' . $user['email'] . "\n";
}注意:query()也不应该用于有用户输入的SQL,因为有SQL注入风险。有用户输入时,用预处理语句。
预处理语句(Prepared Statements)
预处理语句,是PDO最重要的功能之一,能有效防止SQL注入。
原理:先发送SQL模板(用占位符代替实际值),数据库编译并缓存,然后再发送参数,数据库执行。这样,参数值永远不会被当作SQL代码执行,从而防止SQL注入。
占位符
PDO支持两种占位符:
- 问号占位符(?):按顺序绑定
$sql = "INSERT INTO users (name, email) VALUES (?, ?)";
$stmt = $pdo->prepare($sql);
$stmt->execute(['张三', 'zhangsan@example.com']);- 命名占位符(:name):按名称绑定
$sql = "INSERT INTO users (name, email) VALUES (:name, :email)";
$stmt = $pdo->prepare($sql);
$stmt->execute([
':name' => '张三',
':email' => 'zhangsan@example.com',
]);推荐使用命名占位符,更清晰,不容易出错。
INSERT示例
$sql = "INSERT INTO users (name, email, age) VALUES (:name, :email, :age)";
$stmt = $pdo->prepare($sql);
$stmt->execute([
':name' => '张三',
':email' => 'zhangsan@example.com',
':age' => 25,
]);
echo '新用户ID:' . $pdo->lastInsertId();批量INSERT
预处理语句可以多次执行,效率很高:
$sql = "INSERT INTO users (name, email) VALUES (:name, :email)";
$stmt = $pdo->prepare($sql);
$users = [
['name' => '张三', 'email' => 'zhangsan@example.com'],
['name' => '李四', 'email' => 'lisi@example.com'],
['name' => '王五', 'email' => 'wangwu@example.com'],
];
foreach ($users as $user) {
$stmt->execute($user);
}UPDATE示例
$sql = "UPDATE users SET name = :name, email = :email WHERE id = :id";
$stmt = $pdo->prepare($sql);
$stmt->execute([
':id' => 1,
':name' => '李四',
':email' => 'lisi@example.com',
]);
echo '更新了' . $stmt->rowCount() . '行';DELETE示例
$sql = "DELETE FROM users WHERE id = :id";
$stmt = $pdo->prepare($sql);
$stmt->execute([':id' => 1]);
echo '删除了' . $stmt->rowCount() . '行';SELECT示例
// 查询单条
$sql = "SELECT * FROM users WHERE id = :id";
$stmt = $pdo->prepare($sql);
$stmt->execute([':id' => 1]);
$user = $stmt->fetch(); // 获取一行
if ($user) {
echo $user['name'] . ' - ' . $user['email'];
} else {
echo '用户不存在';
}
// 查询多条
$sql = "SELECT * FROM users WHERE age > :age";
$stmt = $pdo->prepare($sql);
$stmt->execute([':age' => 18]);
$users = $stmt->fetchAll(); // 获取所有行
foreach ($users as $user) {
echo $user['name'] . "\n";
}
// 查询单个值
$sql = "SELECT COUNT(*) FROM users";
$count = $pdo->query($sql)->fetchColumn();
echo '用户总数:' . $count;fetch获取模式
fetch()和fetchAll()可以指定获取模式:
// 关联数组(推荐)
$user = $stmt->fetch(PDO::FETCH_ASSOC);
// 索引数组
$user = $stmt->fetch(PDO::FETCH_NUM);
// 对象
$user = $stmt->fetch(PDO::FETCH_OBJ);
echo $user->name;
// 两者都有
$user = $stmt->fetch(PDO::FETCH_BOTH);
// 绑定到指定类
class User {}
$user = $stmt->fetchObject('User');
// fetchAll获取某一列
$names = $stmt->fetchAll(PDO::FETCH_COLUMN, 0); // 获取第0列bindValue和bindParam
除了execute()传数组,还可以用bindValue()和bindParam()绑定参数:
$sql = "INSERT INTO users (name, email) VALUES (:name, :email)";
$stmt = $pdo->prepare($sql);
// bindValue:绑定值
$stmt->bindValue(':name', '张三', PDO::PARAM_STR);
$stmt->bindValue(':email', 'zhangsan@example.com', PDO::PARAM_STR);
$stmt->execute();
// bindParam:绑定变量引用(变量变化时,执行时用最新值)
$name = '张三';
$email = 'zhangsan@example.com';
$stmt->bindParam(':name', $name, PDO::PARAM_STR);
$stmt->bindParam(':email', $email, PDO::PARAM_STR);
$stmt->execute();
// 指定参数类型
$stmt->bindValue(':age', 25, PDO::PARAM_INT);
$stmt->bindValue(':active', true, PDO::PARAM_BOOL);
$stmt->bindValue(':null', null, PDO::PARAM_NULL);参数类型:
PDO::PARAM_STR:字符串PDO::PARAM_INT:整数PDO::PARAM_BOOL:布尔PDO::PARAM_NULL:NULLPDO::PARAM_LOB:大对象(如文件)
推荐用execute()传数组,简单方便。bindParam适合需要绑定变量引用的场景。
事务(Transaction)
事务,是一组SQL操作,要么全部成功,要么全部失败。适合需要保证数据一致性的场景(如转账、下单)。
try {
$pdo->beginTransaction(); // 开始事务
// 操作1:A账户减钱
$sql1 = "UPDATE accounts SET balance = balance - 100 WHERE id = 1";
$pdo->exec($sql1);
// 操作2:B账户加钱
$sql2 = "UPDATE accounts SET balance = balance + 100 WHERE id = 2";
$pdo->exec($sql2);
$pdo->commit(); // 提交事务
echo '转账成功';
} catch (Exception $e) {
$pdo->rollBack(); // 回滚事务
echo '转账失败:' . $e->getMessage();
}事务的特性(ACID):
- 原子性(Atomicity):要么全部成功,要么全部失败
- 一致性(Consistency):事务前后数据保持一致
- 隔离性(Isolation):并发事务之间互不干扰
- 持久性(Durability):事务提交后,数据永久保存
注意:
- MySQL的MyISAM引擎不支持事务,InnoDB支持
- 事务中不要有DDL语句(CREATE、ALTER等),因为DDL会隐式提交
- 长时间的事务会锁表,影响性能,尽量短
错误处理
异常模式(推荐)
设置PDO::ATTRERRMODE => PDO::ERRMODEEXCEPTION后,错误会抛出异常:
try {
$pdo = new PDO($dsn, $user, $pass, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
$pdo->exec("INSERT INTO users (name) VALUES ('张三')");
} catch (PDOException $e) {
echo '错误:' . $e->getMessage();
echo '错误码:' . $e->getCode();
}静默模式
默认的静默模式,需要手动检查错误:
$pdo->exec($sql);
if ($pdo->errorCode() !== '00000') {
$error = $pdo->errorInfo();
echo '错误:' . $error[2];
}
// PDOStatement的错误
$stmt->execute();
if ($stmt->errorCode() !== '00000') {
$error = $stmt->errorInfo();
echo '错误:' . $error[2];
}errorInfo()返回数组:
- [0]:SQLSTATE错误码(5位)
- [1]:驱动错误码
- [2]:错误信息
推荐用异常模式,代码更简洁,不容易遗漏错误。
常用操作封装
查询单条
function findOne(PDO $pdo, $table, $id) {
$sql = "SELECT * FROM {$table} WHERE id = :id";
$stmt = $pdo->prepare($sql);
$stmt->execute([':id' => $id]);
return $stmt->fetch();
}
$user = findOne($pdo, 'users', 1);查询全部
function findAll(PDO $pdo, $table, $where = [], $limit = null) {
$sql = "SELECT * FROM {$table}";
$params = [];
if (!empty($where)) {
$conditions = [];
foreach ($where as $key => $value) {
$conditions[] = "{$key} = :{$key}";
$params[":{$key}"] = $value;
}
$sql .= " WHERE " . implode(' AND ', $conditions);
}
if ($limit) {
$sql .= " LIMIT " . intval($limit);
}
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll();
}
$users = findAll($pdo, 'users', ['age' => 25], 10);插入
function insert(PDO $pdo, $table, $data) {
$fields = array_keys($data);
$placeholders = array_map(function($f) { return ":{$f}"; }, $fields);
$sql = "INSERT INTO {$table} (" . implode(', ', $fields) . ") VALUES (" . implode(', ', $placeholders) . ")";
$stmt = $pdo->prepare($sql);
$stmt->execute($data);
return $pdo->lastInsertId();
}
$id = insert($pdo, 'users', [
'name' => '张三',
'email' => 'zhangsan@example.com',
'age' => 25,
]);更新
function update(PDO $pdo, $table, $id, $data) {
$sets = [];
$params = [':id' => $id];
foreach ($data as $key => $value) {
$sets[] = "{$key} = :{$key}";
$params[":{$key}"] = $value;
}
$sql = "UPDATE {$table} SET " . implode(', ', $sets) . " WHERE id = :id";
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
return $stmt->rowCount();
}
update($pdo, 'users', 1, ['name' => '李四', 'age' => 26]);删除
function delete(PDO $pdo, $table, $id) {
$sql = "DELETE FROM {$table} WHERE id = :id";
$stmt = $pdo->prepare($sql);
$stmt->execute([':id' => $id]);
return $stmt->rowCount();
}
delete($pdo, 'users', 1);分页查询
function paginate(PDO $pdo, $table, $page = 1, $pageSize = 10) {
$offset = ($page - 1) * $pageSize;
// 查询总数
$countSql = "SELECT COUNT(*) FROM {$table}";
$total = $pdo->query($countSql)->fetchColumn();
// 查询当前页数据
$sql = "SELECT * FROM {$table} ORDER BY id DESC LIMIT :offset, :pageSize";
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->bindValue(':pageSize', $pageSize, PDO::PARAM_INT);
$stmt->execute();
$data = $stmt->fetchAll();
return [
'data' => $data,
'total' => $total,
'page' => $page,
'pageSize' => $pageSize,
'totalPages' => ceil($total / $pageSize),
];
}
$result = paginate($pdo, 'users', 1, 10);注意:LIMIT的参数必须用bindValue绑定为INT类型,不能用execute传数组(数组默认是STR类型,MySQL的LIMIT不接受字符串)。
性能优化
1. 使用预处理语句
预处理语句,SQL只编译一次,可以多次执行,效率高。而且能防止SQL注入。
2. 只查询需要的字段
不要用SELECT *,只查询需要的字段,减少数据传输和内存占用。
// 不好
$sql = "SELECT * FROM users";
// 好
$sql = "SELECT id, name, email FROM users";3. 使用LIMIT限制结果集
查询大量数据时,用LIMIT限制返回行数,避免内存溢出。
4. 合理使用索引
在WHERE、ORDER BY、JOIN的字段上建立索引,能大大提高查询速度。
CREATE INDEX idx_email ON users(email);
CREATE INDEX idx_name_age ON users(name, age);5. 避免在循环中查询
不要在循环中执行SQL,会导致大量数据库连接和查询。尽量用一次查询获取所有数据。
// 不好:N+1查询
$users = $pdo->query("SELECT * FROM users")->fetchAll();
foreach ($users as $user) {
$posts = $pdo->query("SELECT * FROM posts WHERE user_id = {$user['id']}")->fetchAll();
}
// 好:一次查询
$users = $pdo->query("SELECT * FROM users")->fetchAll();
$userIds = array_column($users, 'id');
$in = implode(',', array_fill(0, count($userIds), '?'));
$stmt = $pdo->prepare("SELECT * FROM posts WHERE user_id IN ($in)");
$stmt->execute($userIds);
$posts = $stmt->fetchAll();6. 使用事务批量插入
大量插入时,用事务包裹,能大大提高速度(因为事务只提交一次)。
$pdo->beginTransaction();
$stmt = $pdo->prepare("INSERT INTO users (name) VALUES (:name)");
for ($i = 0; $i < 1000; $i++) {
$stmt->execute([':name' => "用户{$i}"]);
}
$pdo->commit();7. 关闭游标
获取完结果后,关闭游标,释放资源:
$stmt->closeCursor();8. 使用持久连接(谨慎)
PDO支持持久连接(连接复用,避免重复连接开销),但要谨慎使用,可能导致连接数过多。
$pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_PERSISTENT => true,
]);安全注意事项
1. 永远用预处理语句,不要拼接SQL
// 危险:SQL注入
$id = $_GET['id'];
$sql = "SELECT * FROM users WHERE id = {$id}"; // 危险!
$pdo->query($sql);
// 安全:预处理语句
$id = $_GET['id'];
$sql = "SELECT * FROM users WHERE id = :id";
$stmt = $pdo->prepare($sql);
$stmt->execute([':id' => $id]);2. 表名和字段名不能用占位符
预处理语句的占位符只能用于值,不能用于表名、字段名、SQL关键字。如果需要动态表名/字段名,要用白名单验证。
// 表名用白名单
$allowedTables = ['users', 'posts', 'comments'];
$table = $_GET['table'];
if (!in_array($table, $allowedTables)) {
die('非法表名');
}
$sql = "SELECT * FROM {$table}";3. 数据库账号权限最小化
不要用root账号连接应用,创建专用账号,只授予必要的权限(如SELECT、INSERT、UPDATE、DELETE,不要GRANT、DROP、ALTER等)。
4. 错误信息不要暴露给用户
生产环境不要显示数据库错误信息,可能泄露表结构、字段名等敏感信息。记录日志,给用户友好的提示。
5. 敏感数据加密存储
密码、身份证号等敏感数据,不要明文存储。密码用password_hash()哈希,其他敏感数据用加密。
总结
PDO是PHP数据库操作的最佳实践,功能强大,安全可靠。
核心要点:
- 连接:创建PDO对象,设置错误模式为异常、默认获取模式为关联数组、禁用模拟预处理
- 执行SQL:exec()(无结果集)、query()(有结果集,无用户输入)
- 预处理语句:prepare() + execute(),用命名占位符,防止SQL注入,支持批量执行
- 获取结果:fetch()(单行)、fetchAll()(多行)、fetchColumn()(单个值)、fetchObject()(对象)
- 事务:beginTransaction() + commit() + rollBack(),保证数据一致性
- 错误处理:异常模式(推荐),try-catch捕获PDOException
- 常用封装:findOne、findAll、insert、update、delete、paginate
- 性能优化:预处理、只查需要的字段、LIMIT、索引、避免循环查询、事务批量插入、关闭游标
- 安全:永远用预处理、表名/字段名用白名单、账号权限最小化、不暴露错误信息、敏感数据加密
PDO是PHP开发者必须掌握的技能。掌握PDO,能让你的数据库操作更安全、更高效、更优雅。希望这篇文章能帮你更好地使用PDO。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录