PHP数据库查询与结果数组化:从MySQLi到PDO的深度指南与安全实践192


在现代Web开发中,PHP作为服务器端脚本语言的佼佼者,与数据库的交互是其核心功能之一。无论是构建动态网站、API服务还是后台管理系统,从数据库中检索数据并将其高效、安全地组织成PHP数组,是每位PHP开发者必须掌握的技能。本文将深入探讨PHP如何查询数据库,特别是如何将查询结果转化为各种形式的PHP数组,涵盖了从传统的MySQLi扩展到现代推荐的PDO扩展,并着重强调了数据安全和最佳实践。

一、PHP与数据库交互的基础:理解连接与执行

在开始查询之前,PHP需要与数据库建立连接。当前PHP主要通过两种扩展与关系型数据库(如MySQL、PostgreSQL、SQLite等)进行交互:
MySQLi (MySQL Improved Extension):专为MySQL数据库设计,提供了面向对象和面向过程两种API。
PDO (PHP Data Objects):一个数据库抽象层,支持多种数据库驱动,提供统一的API,是官方推荐的数据库交互方式。

无论选择哪种扩展,基本流程都是一致的:
建立数据库连接。
构建SQL查询语句。
执行查询语句。
处理查询结果(通常是获取数据行并组织成数组)。
关闭数据库连接(或让PHP自动关闭)。

下面,我们将分别通过MySQLi和PDO来演示如何实现这些步骤,并重点关注结果数组的生成。

二、使用MySQLi扩展查询数据库并将结果处理为数组

MySQLi是与MySQL数据库紧密结合的扩展,提供了相对直接的API。我们将以一个简单的用户表(`users`)为例,该表包含`id`、`username`和`email`字段。

2.1 建立连接


使用面向对象的方式建立连接是推荐的,因为它更现代且易于管理。<?php
$servername = "localhost";
$username = "root";
$password = "your_password";
$dbname = "your_database";
// 创建连接
$conn = new mysqli($servername, $username, $password, $dbname);
// 检查连接是否成功
if ($conn->connect_error) {
die("连接失败: " . $conn->connect_error);
}
// 设置字符集,防止中文乱码
$conn->set_charset("utf8mb4");
echo "数据库连接成功 (MySQLi)!<br>";
?>

2.2 执行查询与获取结果数组


执行SELECT查询后,`mysqli_query()`(或`$conn->query()`)会返回一个`mysqli_result`对象。我们需要从这个对象中逐行或一次性地获取数据,并将其转化为PHP数组。

2.2.1 `fetch_assoc()`:获取关联数组(最常用)


`fetch_assoc()`方法从结果集中获取一行作为关联数组(字段名作为键)。<?php
// ...连接代码...
$sql = "SELECT id, username, email FROM users";
$result = $conn->query($sql);
$users = []; // 用于存储所有用户的数组
if ($result->num_rows > 0) {
// 循环遍历结果集
while ($row = $result->fetch_assoc()) {
$users[] = $row; // 将每行作为关联数组添加到$users数组中
echo "ID: " . $row["id"]. " - 用户名: " . $row["username"]. " - 邮箱: " . $row["email"]. "<br>";
}
echo "<br>所有用户数据已组织成关联数组:<br>";
print_r($users);
} else {
echo "0 结果";
}
// 释放结果集
$result->free();
// 关闭连接
$conn->close();
?>

`$users`数组最终会是一个包含多个关联数组的数组,每个内层数组代表数据库中的一行记录。

2.2.2 `fetch_row()`:获取索引数组


`fetch_row()`方法从结果集中获取一行作为索引数组(从0开始的数字作为键)。<?php
// ...连接代码...
$sql = "SELECT id, username, email FROM users";
$result = $conn->query($sql);
$users_indexed = [];
if ($result->num_rows > 0) {
while ($row = $result->fetch_row()) {
$users_indexed[] = $row;
echo "ID: " . $row[0]. " - 用户名: " . $row[1]. " - 邮箱: " . $row[2]. "<br>";
}
echo "<br>所有用户数据已组织成索引数组:<br>";
print_r($users_indexed);
}
// ...释放和关闭...
?>

2.2.3 `fetch_array()`:获取关联和/或索引数组


`fetch_array()`可以根据参数获取关联数组、索引数组或两者兼有。默认是两者兼有。<?php
// ...连接代码...
$sql = "SELECT id, username, email FROM users";
$result = $conn->query($sql);
if ($result->num_rows > 0) {
// 获取关联和索引数组
$row_both = $result->fetch_array(MYSQLI_BOTH);
echo "<br>同时包含关联和索引键的数组:<br>";
print_r($row_both);
// 获取关联数组 (等同于fetch_assoc)
$result->data_seek(0); // 重置结果集指针
$row_assoc = $result->fetch_array(MYSQLI_ASSOC);
echo "<br>只包含关联键的数组 (MYSQLI_ASSOC):<br>";
print_r($row_assoc);
}
// ...释放和关闭...
?>

2.2.4 `fetch_all()`:一次性获取所有结果


对于小型结果集,`fetch_all()`可以一次性将所有数据提取到多维数组中。它接受一个可选参数,指定返回类型(`MYSQLI_ASSOC`、`MYSQLI_NUM`、`MYSQLI_BOTH`)。<?php
// ...连接代码...
$sql = "SELECT id, username, email FROM users";
$result = $conn->query($sql);
if ($result->num_rows > 0) {
$all_users_assoc = $result->fetch_all(MYSQLI_ASSOC);
echo "<br>所有用户数据一次性获取 (关联数组):<br>";
print_r($all_users_assoc);
$result->data_seek(0); // 重置结果集指针
$all_users_num = $result->fetch_all(MYSQLI_NUM);
echo "<br>所有用户数据一次性获取 (索引数组):<br>";
print_r($all_users_num);
}
// ...释放和关闭...
?>

注意:对于大型结果集,`fetch_all()`可能会消耗大量内存,因为它一次性将所有数据加载到PHP中。在这种情况下,逐行获取(如`fetch_assoc()`循环)是更好的选择。

三、使用PDO扩展查询数据库并将结果处理为数组

PDO是PHP官方推荐的数据库访问方式,因为它提供了统一的接口,支持多种数据库,并且原生支持预处理语句,极大地提升了安全性。

3.1 建立连接


PDO连接通常在`try-catch`块中进行,以便更好地处理连接错误。<?php
$dsn = "mysql:host=localhost;dbname=your_database;charset=utf8mb4";
$username = "root";
$password = "your_password";
try {
$pdo = new PDO($dsn, $username, $password);
// 设置错误模式为抛出异常,便于调试
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// 设置默认的查询结果获取方式为关联数组,简化后续操作
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
echo "数据库连接成功 (PDO)!<br>";
} catch (PDOException $e) {
die("连接失败: " . $e->getMessage());
}
?>

3.2 执行查询与获取结果数组


PDO执行查询主要有两种方式:`query()`和`prepare()`/`execute()`。对于不带参数的简单SELECT查询,可以使用`query()`。

3.2.1 `fetch()`:逐行获取数据


`fetch()`方法从结果集中获取下一行。通过传递不同的`PDO::FETCH_*`常量,可以控制返回的数组类型。默认是`PDO::FETCH_ASSOC`(如果设置了`ATTR_DEFAULT_FETCH_MODE`)。<?php
// ...PDO连接代码...
$sql = "SELECT id, username, email FROM users";
$stmt = $pdo->query($sql); // 返回PDOStatement对象
$users = [];
while ($row = $stmt->fetch()) { // 默认PDO::FETCH_ASSOC
$users[] = $row;
echo "ID: " . $row["id"]. " - 用户名: " . $row["username"]. " - 邮箱: " . $row["email"]. "<br>";
}
echo "<br>所有用户数据已组织成关联数组:<br>";
print_r($users);
// 演示其他获取模式
// 重置语句对象(如果需要再次遍历)
$stmt = $pdo->query($sql);
$row_num = $stmt->fetch(PDO::FETCH_NUM);
echo "<br>索引数组 (PDO::FETCH_NUM):<br>";
print_r($row_num);
$stmt = $pdo->query($sql);
$row_both = $stmt->fetch(PDO::FETCH_BOTH);
echo "<br>关联和索引数组 (PDO::FETCH_BOTH):<br>";
print_r($row_both);
// ...后续关闭连接 (PDO会在脚本结束时自动关闭,但显式设置为null是好习惯)
$pdo = null;
?>

3.2.2 `fetchAll()`:一次性获取所有结果


与MySQLi的`fetch_all()`类似,PDO的`fetchAll()`可以一次性获取所有结果到多维数组中。同样,它接受一个可选参数来指定返回类型。<?php
// ...PDO连接代码...
$sql = "SELECT id, username, email FROM users";
$stmt = $pdo->query($sql);
$all_users_assoc = $stmt->fetchAll(); // 默认PDO::FETCH_ASSOC
echo "<br>所有用户数据一次性获取 (关联数组):<br>";
print_r($all_users_assoc);
$stmt = $pdo->query($sql); // 重新执行以获取新的结果集
$all_users_num = $stmt->fetchAll(PDO::FETCH_NUM);
echo "<br>所有用户数据一次性获取 (索引数组):<br>";
print_r($all_users_num);
$pdo = null;
?>

四、关键的安全实践:预处理语句与参数绑定

无论是MySQLi还是PDO,最关键的安全实践是使用预处理语句(Prepared Statements)和参数绑定(Parameter Binding)来防止SQL注入攻击。

SQL注入是一种常见的Web安全漏洞,攻击者通过在输入数据中插入恶意SQL代码,改变查询的意图,从而窃取、篡改或删除数据。

预处理语句的工作原理是:先将SQL语句的结构发送到数据库服务器进行预编译,然后再将参数值单独发送到服务器。数据库服务器不会将参数值作为SQL代码的一部分来解释,从而避免了注入风险。

4.1 MySQLi的预处理语句


<?php
// ...MySQLi连接代码...
$search_username = "alice"; // 用户输入,可能包含恶意代码
$sql = "SELECT id, username, email FROM users WHERE username = ?";
$stmt = $conn->prepare($sql); // 预处理SQL语句
if ($stmt === false) {
die("SQL预处理失败: " . $conn->error);
}
// 绑定参数:'s'表示字符串类型
$stmt->bind_param("s", $search_username);
// 执行预处理语句
$stmt->execute();
// 获取结果集
$result = $stmt->get_result();
$found_users = [];
if ($result->num_rows > 0) {
while ($row = $result->fetch_assoc()) {
$found_users[] = $row;
}
echo "<br>通过预处理语句查询到的用户:<br>";
print_r($found_users);
} else {
echo "<br>未找到用户: " . htmlspecialchars($search_username);
}
// 关闭语句和连接
$stmt->close();
$conn->close();
?>

`bind_param()`的第一个参数是类型字符串,例如`'i'`代表整数,`'d'`代表双精度浮点数,`'s'`代表字符串,`'b'`代表BLOB。

4.2 PDO的预处理语句(推荐)


<?php
// ...PDO连接代码...
$search_email = "bob@"; // 用户输入
$sql = "SELECT id, username, email FROM users WHERE email = :email"; // 命名占位符
// 或者 $sql = "SELECT id, username, email FROM users WHERE email = ?"; // 问号占位符
$stmt = $pdo->prepare($sql); // 预处理SQL语句
// 绑定参数
$stmt->bindParam(':email', $search_email, PDO::PARAM_STR); // 命名占位符绑定
// 或者 $stmt->bindParam(1, $search_email, PDO::PARAM_STR); // 问号占位符绑定
// 执行预处理语句
$stmt->execute();
$found_users = $stmt->fetchAll(); // 获取所有结果为关联数组
if (count($found_users) > 0) {
echo "<br>通过PDO预处理语句查询到的用户:<br>";
print_r($found_users);
} else {
echo "<br>未找到邮箱为 " . htmlspecialchars($search_email) . " 的用户.";
}
$pdo = null;
?>

PDO的参数绑定更加灵活,支持命名占位符(如`:email`)和问号占位符(`?`)。`bindParam()`和`bindValue()`都可以用来绑定参数,`bindParam()`绑定的是变量的引用,而`bindValue()`绑定的是一个值。

五、处理复杂结果集与多维数组

在实际应用中,我们经常需要处理来自JOIN查询的复杂结果,或将扁平的数据库结果组织成具有层级结构的多维数组,以便于前端展示或进一步的数据处理。

例如,我们可能需要显示每个用户的订单列表。假设我们有`users`表和`orders`表,`orders`表包含`id`, `user_id`, `order_date`, `total_amount`。<?php
// ...PDO连接代码...
// 查询所有用户及其订单
$sql = "
SELECT
AS user_id,
,
,
AS order_id,
o.order_date,
o.total_amount
FROM
users u
LEFT JOIN
orders o ON = o.user_id
ORDER BY
, o.order_date
";
$stmt = $pdo->query($sql);
$raw_results = $stmt->fetchAll();
$users_with_orders = [];
foreach ($raw_results as $row) {
$user_id = $row['user_id'];
// 如果用户尚未添加到结果数组中,则初始化
if (!isset($users_with_orders[$user_id])) {
$users_with_orders[$user_id] = [
'id' => $row['user_id'],
'username' => $row['username'],
'email' => $row['email'],
'orders' => [] // 为该用户添加一个空订单数组
];
}
// 如果该行有订单数据(即不是NULL,且订单ID存在),则添加到用户订单列表中
if ($row['order_id'] !== null) {
$users_with_orders[$user_id]['orders'][] = [
'order_id' => $row['order_id'],
'order_date' => $row['order_date'],
'total_amount' => $row['total_amount']
];
}
}
echo "<br>组织成多维数组的用户及其订单:<br>";
print_r($users_with_orders);
$pdo = null;
?>

通过这种方式,我们将扁平的JOIN查询结果重构为更具逻辑结构的多维数组,其中每个用户都有一个`orders`子数组,方便前端展示(例如,在一个循环中显示用户,在嵌套循环中显示该用户的订单)。

六、性能优化与最佳实践

高质量的数据库交互不仅要实现功能,还要考虑性能和可维护性。
始终使用预处理语句:这是防止SQL注入的最佳实践,也是性能优化的一部分,因为数据库可以缓存预编译的查询计划。
选择合适的获取方式:

对于大型结果集,逐行获取(`fetch_assoc()`或`fetch()`循环)比一次性获取所有数据(`fetch_all()`或`fetchAll()`)更节省内存。
如果只需要少量数据,`fetch()`通常是高效的。


只查询需要的字段:避免使用`SELECT *`,只选择你需要的列,可以减少数据传输量和内存消耗。
限制结果集大小:使用`LIMIT`和`OFFSET`进行分页查询,避免一次性加载所有数据。
关闭连接/释放资源:虽然PHP脚本执行完毕会自动关闭数据库连接并释放资源,但对于长时间运行的脚本或连接池,显式关闭连接 (`$conn->close()` 或 `$pdo = null;`) 是一个好习惯,尤其是在不需要进一步与数据库交互时。
错误处理:始终实现健壮的错误处理机制。MySQLi使用`$conn->error`和`$stmt->error`,PDO使用`try-catch`块和`PDO::ATTR_ERRMODE`。
配置管理:将数据库连接参数(服务器地址、用户名、密码、数据库名)存储在配置文件中,而不是硬编码在脚本里,提高安全性和可维护性。
封装数据库操作:将数据库连接和常用查询封装到类或函数中,创建自己的数据库层或使用ORM(如Laravel Eloquent),可以提高代码的复用性、可读性和可测试性。
设置字符集:确保数据库连接和表的字符集设置一致(通常是`utf8mb4`),以避免中文乱码问题。

七、总结

PHP查询数据库并将结果组织成数组是Web开发中的一项基本而关键的任务。本文详细介绍了使用MySQLi和PDO两种主流扩展实现这一目标的方法,从基础连接、执行查询到各种数组获取方式(关联数组、索引数组、多维数组),并特别强调了通过预处理语句来保障数据安全的至关重要性。通过遵循文中介绍的最佳实践,开发者可以编写出更高效、更安全、更易于维护的数据库交互代码。

在实际项目中,我们强烈推荐使用PDO及其预处理语句,它不仅提供了更强大的功能和更好的兼容性,更重要的是,它为PHP应用程序提供了坚实的安全基础,有效抵御SQL注入等常见攻击。掌握这些知识,你就能自信地在PHP项目中处理各种数据库查询和数据组织任务。

2025-10-17


上一篇:PHP数据库自动化部署:编写高效的创建与初始化脚本

下一篇:PHP数组深度解析:高效扁平化与智能合并策略