本文详细介绍了如何使用php将扁平化的sql联接查询结果转换为具有层级结构的嵌套数组,例如将问卷及其包含的问题组织成一个主从式数据结构。通过一个高效的单次遍历循环,利用父级id作为数组键进行条件判断和数据聚合,避免了重复数据并确保了正确的嵌套格式,最终生成符合预期的json输出。
在Web开发中,我们经常需要从关系型数据库中检索数据并将其组织成更符合前端展示或API接口需求的层级结构。一个常见的场景是,当存在“一对多”关系(如一个问卷包含多个问题)时,通过SQL JOIN 操作获取的数据通常是扁平化的,即每个子记录都会重复父记录的信息。本文将指导您如何高效地将这种扁平数据转换为嵌套的PHP数组结构。
1. 问题描述与数据模型
假设我们有两个表:questionnaires (问卷) 和 questions (问题),它们之间通过一个联接表 questionnaireshasquestions 建立多对多关系。我们希望得到的数据结构是一个包含问卷信息(ID、标题)及其所有相关问题(ID、文本)的嵌套数组,类似以下JSON格式:
[ { "id": "x", "title": "x", "questions": [ {"id": "x", "text": "x"}, {"id": "x2", "text": "x2"} ] }]登录后复制
为了获取所有问卷及其对应的问题,我们通常会使用 INNER JOIN 查询:
SELECt questionnaires.id AS QuestionnaireId, questionnaires.title AS QuestionnaireTitle, questions.id AS QuestionId, questions.text AS Question FROM questionnaires INNER JOIN questionnaireshasquestions qa ON qa.idQuestionnaire = questionnaires.id INNER JOIN questions ON questions.id = qa.idQuestion;登录后复制
这条查询会返回一个扁平化的结果集,其中每个问卷-问题对都会作为一行数据。例如,如果问卷A有3个问题,那么问卷A的信息会在结果集中出现3次。
立即学习“PHP免费学习笔记(深入)”;
2. 初始尝试及存在的问题
许多初学者在处理这种扁平数据时,可能会尝试在循环中直接构建数组。一个常见的错误尝试如下:
$data = [];while ($row = $conn->fetch()) { if (!isset($data['questionnaires'][$row['QuestionnaireId']])) { $data['questionnaires'][] = [ 'id' => $row['QuestionnaireId'], 'title' => $row['QuestionnaireTitle'], 'questions' => [ 'id' => $row['QuestionId'], 'text' => $row['Question'] ] ]; } else { // 这里的逻辑可能导致问题,例如覆盖或不正确的追加 $data['questionnaires'][$row['QuestionnaireId']][] = [ 'questions' => [ 'id' => $row['QuestionId'], 'text' => $row['Question'] ] ]; }}登录后复制
这种方法通常会导致以下问题:
重复的父级记录: 如果不正确地处理 else 分支,可能会为每个问题都创建一个新的问卷记录。子记录覆盖: 如果 questions 字段直接被赋值而不是追加到数组中,那么同一个问卷的后续问题会覆盖掉之前的问题。不正确的数组索引: [] 操作符在非关联数组中使用时会创建新的数字索引,这可能导致最终输出的JSON格式不符合预期。3. 正确的嵌套数组构建方法
解决上述问题的关键在于,使用父级记录的唯一ID作为临时数组的键,这样可以确保每个父级记录只被初始化一次,并且其子级记录可以正确地被追加到对应的子数组中。

即构数智人是由即构科技推出的AI虚拟数字人视频创作平台,支持数字人形象定制、短视频创作、数字人直播等。


以下是优化的PHP代码实现:
<?php// 假设 $conn 是您的数据库连接对象,并且 $conn->fetch() 方法可以逐行获取查询结果// 示例数据模拟,实际应用中替换为您的数据库查询循环$rows = [ ['QuestionnaireId' => '1', 'QuestionnaireTitle' => 'Are you hungry?', 'QuestionId' => '1', 'Question' => 'How is your passion?'], ['QuestionnaireId' => '1', 'QuestionnaireTitle' => 'Are you hungry?', 'QuestionId' => '2', 'Question' => 'Do you drink?'], ['QuestionnaireId' => '2', 'QuestionnaireTitle' => 'How are you feeling?', 'QuestionId' => '1', 'Question' => 'How is your passion?'], ['QuestionnaireId' => '2', 'QuestionnaireTitle' => 'How are you feeling?', 'QuestionId' => '3', 'Question' => 'Do you like fish?'], ['QuestionnaireId' => '5', 'QuestionnaireTitle' => 'Is testing working?', 'QuestionId' => '4', 'Question' => 'How is the testing?']];$data = ['questionnaires' => []]; // 初始化一个用于存放最终数据的数组foreach ($rows as $row) { // 实际应用中替换为 while ($row = $conn->fetch()) $questionnaireId = $row['QuestionnaireId']; // 检查当前问卷ID是否已经存在于 $data['questionnaires'] 中 if (!isset($data['questionnaires'][$questionnaireId])) { // 如果是第一次遇到这个问卷ID,则初始化问卷信息 // 并为其 'questions' 字段初始化一个空数组,用于后续添加问题 $data['questionnaires'][$questionnaireId] = [ 'id' => $row['QuestionnaireId'], 'title' => $row['QuestionnaireTitle'], 'questions' => [], // 初始化一个空数组来收集问题 ]; } // 将当前行的问题信息添加到对应问卷的 'questions' 数组中 $data['questionnaires'][$questionnaireId]['questions'][] = [ 'id' => $row['QuestionId'], 'text' => $row['Question'] ];}// 由于我们使用了问卷ID作为键,最终的 $data['questionnaires'] 会是一个关联数组// 如果需要一个索引数组(数字键),可以使用 array_values()$finalQuestionnaires = array_values($data['questionnaires']);// 打印最终的JSON格式数据echo json_encode($finalQuestionnaires, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);?>登录后复制
代码解析:
$data = ['questionnaires' => []];: 初始化一个名为 $data 的数组,其中包含一个 questionnaires 键,它的值是一个空数组。这个空数组将用于存储我们构建的嵌套问卷数据。$questionnaireId = $row['QuestionnaireId'];: 从当前行中提取问卷ID,作为后续判断的依据。if (!isset($data['questionnaires'][$questionnaireId])): 这是核心判断逻辑。它检查 $data['questionnaires'] 中是否已经存在以当前 $questionnaireId 为键的元素。如果不存在(第一次遇到此问卷ID):$data['questionnaires'][$questionnaireId] = [...]: 使用当前 $questionnaireId 作为键,创建一个新的问卷数组。这样做的好处是,同一个问卷ID的记录都会指向同一个内存位置,不会重复创建问卷对象。'questions' => []: 在创建问卷时,立即为其 questions 字段初始化一个空数组。这是至关重要的一步,它确保了 questions 始终是一个数组,可以接受多个问题。如果已存在(此问卷ID之前已处理过): if 条件不满足,程序直接跳到 else 块(在优化后的代码中,实际上是跳过 if 块,直接执行后续的追加逻辑)。$data['questionnaires'][$questionnaireId]['questions'][] = [...]: 无论当前问卷是第一次出现还是已经存在,我们都将当前行的问题信息追加到对应问卷的 questions 数组中。[] 操作符确保了新问题被添加到数组的末尾,而不是覆盖现有问题。$finalQuestionnaires = array_values($data['questionnaires']);: 在循环结束后,$data['questionnaires'] 是一个关联数组,其键是 QuestionnaireId。如果最终需要一个索引数组(即从0开始的数字键),可以使用 array_values() 函数将其转换为一个普通的索引数组,这通常是JSON数组的期望格式。4. 最终输出示例
经过上述处理,json_encode($finalQuestionnaires) 将生成如下结构的数据:
[ { "id": "1", "title": "Are you hungry?", "questions": [ { "id": "1", "text": "How is your passion?" }, { "id": "2", "text": "Do you drink?" } ] }, { "id": "2", "title": "How are you feeling?", "questions": [ { "id": "1", "text": "How is your passion?" }, { "id": "3", "text": "Do you like fish?" } ] }, { "id": "5", "title": "Is testing working?", "questions": [ { "id": "4", "text": "How is the testing?" } ] }]登录后复制
这正是我们期望的、结构清晰的嵌套数组格式。
5. 注意事项与最佳实践
唯一标识符作为键: 确保用于作为父级数组键的字段(如 QuestionnaireId)是唯一的。如果不是唯一的,数据可能会被错误地合并。单次遍历效率: 这种方法只需对数据库查询结果进行一次遍历,效率较高。避免在循环内部执行额外的数据库查询,那会导致N+1查询问题,严重影响性能。内存使用: 对于非常大的结果集,一次性将所有数据加载到内存中可能会消耗大量内存。在这种情况下,可以考虑使用流式处理或分页。错误处理: 在实际应用中,应加入适当的错误处理机制,例如检查数据库连接是否成功,查询是否返回结果等。json_encode 选项: 使用 JSON_PRETTY_PRINT 可以使输出的JSON更易读,JSON_UNESCAPED_UNICODE 确保中文字符不会被转义。总结
通过利用父级记录的唯一ID作为临时数组的键,并巧妙地在循环中进行条件判断,我们能够高效且准确地将扁平化的SQL联接结果转换为复杂的嵌套PHP数组。这种模式在处理一对多关系数据时非常实用,能够帮助开发者构建出结构清晰、易于消费的数据接口。
以上就是PHP从SQL联接结果构建嵌套数组的有效方法的详细内容,更多请关注php中文网其它相关文章!