PHP数据如何使用PDO扩展 PHP数据抽象层操作的详细解析
在PHP开发中,操作数据库是非常常见的需求。不同的数据库厂商提供了不同的扩展,比如操作MySQL可能用mysqli,操作PostgreSQL可能用pgsql,这些扩展的语法差异很大,切换数据库时代码需要大量修改。PDO(PHP Data Objects)作为PHP的数据抽象层,提供了一套统一的接口来操作不同的数据库,开发者只需要掌握一套API,就能兼容几乎所有主流数据库,大大降低了数据库切换的成本。
PDO的核心优势
PDO之所以被广泛使用,主要得益于以下几个特点:
- 跨数据库兼容:支持MySQL、PostgreSQL、SQLite、Oracle等数十种主流数据库,无需修改业务逻辑代码即可切换数据库类型。
- 预处理语句支持:原生支持参数化查询,从根源上避免SQL注入风险,比拼接SQL字符串安全得多。
- 面向对象接口:所有操作都通过对象方法完成,符合现代PHP的面向对象编程习惯,代码结构更清晰。
- 错误处理灵活:支持异常模式、警告模式、静默模式三种错误处理方式,开发者可以根据需求选择合适的方式。
PDO的基础使用流程
使用PDO操作数据库通常分为四个步骤:创建连接、执行SQL、处理结果、关闭连接(可选,PHP脚本结束会自动关闭)。下面我们逐个步骤详细说明。
1. 创建PDO连接
首先需要实例化PDO类,传入数据源名称(DSN)、数据库用户名、密码以及可选的连接选项。DSN的格式根据数据库类型不同有所区别,比如MySQL的DSN格式是mysql:host=主机名;port=端口;dbname=数据库名;charset=字符集。
这里推荐使用异常模式处理错误,这样如果连接失败会直接抛出异常,方便我们捕获问题:
<?php
// 数据库连接配置
$dbHost = '127.0.0.1';
$dbPort = 3306;
$dbName = 'test_db';
$dbUser = 'root';
$dbPass = '123456';
$charset = 'utf8mb4';
// 构造MySQL的DSN
$dsn = "mysql:host={$dbHost};port={$dbPort};dbname={$dbName};charset={$charset}";
try {
// 实例化PDO对象,设置错误模式为异常模式
$pdo = new PDO($dsn, $dbUser, $dbPass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // 错误时抛出异常
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC // 默认返回关联数组
]);
echo "数据库连接成功";
} catch (PDOException $e) {
// 捕获连接异常,输出错误信息
die("数据库连接失败:" . $e->getMessage());
}
?>上面的代码中,我们通过PDO::ATTR_ERRMODE将错误模式设为异常模式,这样后续执行SQL出错时也会抛出异常,统一用try-catch处理即可。PDO::ATTR_DEFAULT_FETCH_MODE设置了默认的查询结果返回格式为关联数组,后续查询时如果不指定返回格式,就会默认返回关联数组。
2. 执行普通SQL查询(非预处理)
如果是执行没有参数的简单SQL,比如查询全表数据、执行不接收外部参数的DDL语句,可以直接使用query()方法或者exec()方法:
query():用于执行查询类SQL(SELECT、SHOW等),返回PDOStatement对象,后续可以通过这个对象获取查询结果。exec():用于执行增删改类SQL(INSERT、UPDATE、DELETE、DDL等),返回受影响的行数。
下面是查询示例:
<?php
// 承接上面的连接代码,$pdo已创建
try {
// 执行查询SQL,获取所有用户数据
$sql = "SELECT id, username, email FROM users";
$stmt = $pdo->query($sql);
// 获取所有结果,默认是关联数组(因为之前设置了默认返回模式)
$userList = $stmt->fetchAll();
echo "查询到" . count($userList) . "条用户数据:<br/>";
foreach ($userList as $user) {
echo "ID:{$user['id']},用户名:{$user['username']},邮箱:{$user['email']}<br/>";
}
// 执行INSERT语句,返回受影响的行数
$insertSql = "INSERT INTO users (username, email) VALUES ('test_user', 'test@ipipp.com')";
$affectedRows = $pdo->exec($insertSql);
echo "插入成功,受影响行数:{$affectedRows}";
} catch (PDOException $e) {
echo "SQL执行失败:" . $e->getMessage();
}
?>需要注意的是,这种直接拼接SQL的方式不能用于接收外部参数的场景,比如接收用户输入的用户名查询数据,否则会有SQL注入风险,这时候就需要用到预处理语句。
3. 使用预处理语句操作数据
预处理语句是PDO的核心安全特性,它将SQL逻辑和参数分开传递,数据库会先编译SQL逻辑,再绑定参数执行,从根源上避免SQL注入。预处理语句的使用分为两步:准备SQL、绑定参数并执行。
查询操作示例
<?php
// 承接上面的连接代码,$pdo已创建
try {
// 准备预处理SQL,用?作为占位符
$sql = "SELECT id, username, email FROM users WHERE username = ? AND email = ?";
$stmt = $pdo->prepare($sql);
// 绑定参数并执行,第一个参数是占位符位置(从1开始),第二个是参数值
$stmt->execute(['test_user', 'test@ipipp.com']);
// 获取单条结果
$user = $stmt->fetch();
if ($user) {
echo "查询到用户:ID={$user['id']},用户名={$user['username']}";
} else {
echo "未查询到对应用户";
}
// 也可以使用命名占位符,可读性更好
$sql2 = "SELECT * FROM users WHERE id = :user_id";
$stmt2 = $pdo->prepare($sql2);
// 绑定命名参数
$stmt2->bindParam(':user_id', $userId, PDO::PARAM_INT);
$userId = 1;
$stmt2->execute();
$user2 = $stmt2->fetch();
echo "<br/>ID为1的用户信息:" . json_encode($user2, JSON_UNESCAPED_UNICODE);
} catch (PDOException $e) {
echo "预处理执行失败:" . $e->getMessage();
}
?>上面的代码展示了两种占位符的使用方式:?匿名占位符和:name命名占位符。命名占位符的可读性更强,尤其在参数较多的时候,能清晰知道每个参数对应的含义。bindParam方法可以指定参数的类型,比如PDO::PARAM_INT表示整数类型,避免数据库收到错误类型的参数。
增删改操作示例
增删改操作和查询操作类似,只是执行的SQL类型不同,同样使用预处理避免注入:
<?php
// 承接上面的连接代码,$pdo已创建
try {
// 插入数据
$insertSql = "INSERT INTO users (username, email, age) VALUES (:username, :email, :age)";
$stmt = $pdo->prepare($insertSql);
// 执行时传入参数数组,键名对应命名占位符
$stmt->execute([
':username' => 'new_user',
':email' => 'new@ipipp.com',
':age' => 25
]);
// 获取最后插入的自增ID
$lastInsertId = $pdo->lastInsertId();
echo "插入成功,新用户ID:{$lastInsertId}<br/>";
// 更新数据
$updateSql = "UPDATE users SET age = :age WHERE username = :username";
$stmt = $pdo->prepare($updateSql);
$stmt->execute([
':age' => 26,
':username' => 'new_user'
]);
echo "更新成功,受影响行数:{$stmt->rowCount()}<br/>";
// 删除数据
$deleteSql = "DELETE FROM users WHERE id = :id";
$stmt = $pdo->prepare($deleteSql);
$stmt->execute([':id' => $lastInsertId]);
echo "删除成功,受影响行数:{$stmt->rowCount()}";
} catch (PDOException $e) {
echo "增删改执行失败:" . $e->getMessage();
}
?>这里用到了rowCount()方法,它可以返回当前操作受影响的行数,对于增删改操作,这个方法通常可以用来判断操作是否生效。lastInsertId()方法可以获取最后一次插入操作的自增主键ID,适合插入后需要用到新ID的场景。
4. 事务处理
如果有一组SQL操作需要要么全部成功,要么全部失败(比如转账操作,扣钱和加钱必须同时成功或同时失败),就需要用到事务。PDO支持事务操作,相关方法有beginTransaction()开启事务、commit()提交事务、rollBack()回滚事务。
<?php
// 承接上面的连接代码,$pdo已创建
try {
// 开启事务
$pdo->beginTransaction();
// 第一步:从A账户扣100元
$sql1 = "UPDATE accounts SET balance = balance - 100 WHERE user_id = 1";
$pdo->exec($sql1);
// 第二步:给B账户加100元
$sql2 = "UPDATE accounts SET balance = balance + 100 WHERE user_id = 2";
$pdo->exec($sql2);
// 如果两步都执行成功,提交事务
$pdo->commit();
echo "转账成功";
} catch (PDOException $e) {
// 如果任何一步出错,回滚事务,所有操作都不会生效
$pdo->rollBack();
echo "转账失败,已回滚:" . $e->getMessage();
}
?>需要注意的是,事务生效的前提是数据库引擎支持事务,比如MySQL的InnoDB引擎支持事务,而MyISAM引擎不支持,使用前要确认表的引擎类型。
常见注意事项
- 确保PHP已经安装了对应的PDO驱动,比如操作MySQL需要
pdo_mysql扩展,可以在phpinfo()中查看已安装的PDO驱动。 - 所有接收外部参数的SQL都使用预处理语句,不要直接拼接用户输入到SQL中,避免SQL注入。
- 连接数据库后如果长时间不使用,可以适当关闭连接,不过PHP脚本结束后会自动释放连接,一般无需手动关闭。
- 错误信息不要直接暴露给用户,生产环境可以记录到日志中,避免泄露数据库结构等敏感信息。