materialized path字段必须用varchar类型而非整数,子节点path=父节点path+'.'+自身id,插入时需先save获取id再更新path,查询用like匹配,移动节点需事务内原子化更新整棵子树path。

Materialized Path字段命名必须用字符串类型,不能用整数
很多开发者直接把 path 字段设成 INT 或 BIGINT,结果插入 "1.3.7" 时被 MySQL 强转成 1,后续所有层级查询全失效。Laravel 的 Eloquent 不会阻止你建错字段类型,但 Materialized Path 本质是带分隔符的路径字符串,必须用 VARCHAR(建议至少 VARCHAR(255)),否则 where('path', 'like', '1.3.%') 永远不匹配。
建表时务必这样写:
Schema::create('categories', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('path', 255); // ✅ 不是 integer
$table->timestamps();
});
常见错误现象:Category::where('path', 'like', '1.%')->get() 返回空集合,但手动查数据库确认有 path = '1.5' 的记录——八成是字段类型错了。
插入新节点时 path 值要拼接父级 path + 自身 ID
Materialized Path 的核心逻辑是:子节点的 path = 父节点 path + '.' + 当前 ID。Eloquent 本身不提供自动拼接,得自己算。别在模型里写 $this->path = $parent->path . '.' . $this->id ——因为 $this->id 还没生成(save() 前是 null)。
正确做法是先 save() 获取 ID,再更新 path:
$category = new Category(['name' => 'Electronics']); $category->save(); // 先存,拿到 ID $parent = Category::find($parentId); $category->path = $parent ? $parent->path . '.' . $category->id : $category->id; $category->save(); // 再更新 path
注意点:
- 根节点的
path就是自身id(如'1'),不是'1.'或'.1' - 如果用
create(),记得用事务包裹,避免中间状态泄露 - 不要依赖
creating模型事件自动设path,因为$this->id此时不可用
查询子树用 like + escape,别用正则或 JSON 函数
查某个节点的所有后代,最高效方式是 where('path', 'like', $targetPath . '.%')。但默认 . 在 SQL LIKE 中不是通配符,所以不用 escape;真正要小心的是用户输入可能含 % 或 _ ——比如分类名是 “100% Organic”,若误把名字当 path 拼接,就会出错。
安全写法:
$node = Category::findOrFail($id);
$children = Category::where('path', 'like', $node->path . '.%')->get();
性能关键点:
- 给
path字段加 B-tree 索引(MySQL 默认),LIKE '1.3.%'能走索引前缀扫描 - 避免用
REGEXP或JSON_CONTAINS,它们无法利用索引,大数据量下慢几倍 - 不要写
whereRaw("path REGEXP '^{$node->path}\.'")——易注入且不走索引
移动节点时 path 更新必须原子化,且要递归改子树
把节点 A 移到节点 B 下,不只是改 A 的 path,A 的整个子树的 path 都得重算。例如 A 原 path 是 '1.5',B 是 '2.4',那 A 新 path 是 '2.4.8'(8 是 A 的 ID),而原来 '1.5.9' 要变成 '2.4.8.9'。
操作必须在一个事务里完成,且推荐用「先查后更」而非纯 SQL:
DB::transaction(function () use ($nodeId, $newParentId) {
$node = Category::findOrFail($nodeId);
$newParent = Category::findOrFail($newParentId);
$oldPath = $node->path;
$newPathPrefix = $newParent->path . '.' . $node->id;
// 更新自身
$node->path = $newPathPrefix;
$node->save();
// 更新全部后代:path 以 oldPath 开头的,都替换成 newPathPrefix
Category::where('path', 'like', $oldPath . '.%')
->update(['path' => DB::raw("REPLACE(path, '{$oldPath}.', '{$newPathPrefix}.')")]);
});
容易踩的坑:
-
REPLACE()是 MySQL 函数,SQLite 不支持,换数据库时得重写 - 如果子树很深,
UPDATE可能锁表时间长,高并发场景要考虑加缓存层或异步任务 - 别漏掉对
path字段加唯一索引——重复 path 会导致树结构错乱,但 Eloquent 不校验
Materialized Path 看似简单,真正难的是移动、删除、并发更新时的路径一致性。字段类型、ID 时机、SQL 索引覆盖、事务边界——每个点松动一点,树就散了。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











