不能靠单次INSERT解决,必须先解析层级并按拓扑序逐层插入,通过两阶段法构建名称到ID映射表,避免lastInsertId()时序错误和循环引用,辅以缩进/关键词识别层级、原始行号定位报错。
直接说结论:不能靠单次 INSERT 解决,必须先解析层级(比如 Excel 或 JSON 中的 parent_id 或 level 字段),再按拓扑顺序逐层插入,否则外键约束或空 id 会导致失败。
典型场景是 Excel 表格里有“姓名、职位、上级姓名、部门”这类字段,需自动识别出校长 → 年级组长 → 班主任 → 任课教师的树状结构。关键不是“怎么递归”,而是“怎么保证父节点一定比子节点先入库”。
array_values() 重排原始数据,确保索引连续usort() 按深度排序(如 level 字段);若无深度字段,得先用 buildTreeByParentId() 遍历一遍算出每行的层级$nameToId['张校长'] = 1,后续遇到“上级姓名=张校长”就填 parent_id = 1
常见错误是边查边插、反复 SELECT 查父级,性能崩且容易死锁。正确做法是两阶段:第一阶段只收集所有待插入数据并建立名称/编码到临时键的映射;第二阶段批量插入,并用映射表填充 parent_id。
示例逻辑片段:
// 假设 $rows 是从 Excel 解析出的数组,含 name, parent_name 字段 $map = []; // name => id(插入后写入) usort($rows, fn($a, $b) => strlen($a['parent_name']) - strlen($b['parent_name'])); // 粗略按父级长度排,更稳的做法是先拓扑排序foreach ($rows as $row) { $parentId = null; if (!empty($row['parent_name']) && isset($map[$row['parent_name']])) { $parentId = $map[$row['parent_name']]; } $stmt = $pdo->prepare("INSERT INTO contacts (name, parent_id) VAL
UES (?, ?)"); $stmt->execute([$row['name'], $parentId]); $map[$row['name']] = $pdo->lastInsertId(); }
detectCycle() 提前报错,而不是等插入时报 Integrity constraint violation
parent_name 匹配要加 trim() 和大小写统一,Excel 经常有多余空格或全角字符因为 lastInsertId() 只返回当前连接最后一次插入的 ID,一旦中间穿插了其他 INSERT(比如日志表、审计表),映射就会错位。更糟的是,如果用了连接池(如 Swoole 或 PDO::ATTR_PERSISTENT),lastInsertId() 可能属于别人的操作。
dept-001)作为主键,插入前就生成好,父子关系靠字符串关联,完全规避 ID 时序问题INSERT ... SELECT 批量插入 + ROW_NUMBER()(MySQL 8.0+)配合临时内存表预生成 ID 映射SELECT MAX(id) 查父 ID——竞态条件下必然出错用户上传的 Excel 很少自带 level 或 parent_id,多数是靠缩进、空格数或“职务”字段关键词(如“副”、“助理”、“代课”)推测层级。这时候递归只是手段,校验才是重点。
mb_substr_count($name, ' ')(全角空格)或 preg_match_all('/^\s+/', $line) 统计缩进,比肉眼判断可靠getAncestors($name, $rows) 向上追溯,检查是否成环、是否跨部门(比如高三年级组长出现在小学部数据块里)真正难的不是写递归函数,是处理用户随手填的 Excel 里混着空行、合并单元格、手误多打一个空格、用“/”分隔多个上级这些现实情况。留个字段存原始行号,出错时能准确定位到 Excel 第几行,比堆递归逻辑有用得多。