如何为无限层级的分类与子分类建立多对多模型关联?
实现无限级分类的多对多递归关联
首先,根据你的表结构,subcategories其实是用来存储分类之间父子关系的中间关联表(多对多自关联),因为你允许任意分类拥有无限级子分类(甚至一个分类可以属于多个父分类)。下面我会一步步教你在Eloquent模型中实现这个关联:
1. 定义Category模型的关联关系
首先,在你的Category模型中,我们需要定义两个多对多自关联:一个用来获取子分类,一个用来获取父分类,再加上递归方法来获取所有层级的后代/祖先。
namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsToMany; class Category extends Model { protected $table = 'categories'; // 如果你的表没有时间戳字段,可设置为false public $timestamps = true; /** * 获取当前分类的直接子分类 */ public function children(): BelongsToMany { return $this->belongsToMany( self::class, 'subcategories', // 中间表名称 'parent_category_id', // 当前模型在中间表的外键(父分类ID) 'category_id' // 关联模型在中间表的外键(子分类ID) )->withTimestamps(); // 如需跟踪中间表的时间变化,保留此方法 } /** * 获取当前分类的直接父分类 */ public function parents(): BelongsToMany { return $this->belongsToMany( self::class, 'subcategories', 'category_id', // 当前模型在中间表的外键(子分类ID) 'parent_category_id' // 关联模型在中间表的外键(父分类ID) )->withTimestamps(); } /** * 递归获取所有层级的子分类(子分类的子分类,以此类推) */ public function allChildren() { return $this->children()->with('allChildren'); } /** * 递归获取所有层级的父分类(父分类的父分类,以此类推) */ public function allParents() { return $this->parents()->with('allParents'); } }
2. 基础使用示例
获取单个分类及其所有子分类
// 获取ID为1的分类,同时预加载所有层级的子分类 $category = Category::with('allChildren')->find(1); // 遍历直接子分类 foreach ($category->children as $child) { echo $child->title; } // 递归遍历所有层级的子分类 function printCategories($categories) { foreach ($categories as $category) { echo $category->title . "<br>"; if ($category->allChildren->isNotEmpty()) { printCategories($category->allChildren); } } } printCategories($category->allChildren);
获取所有根分类(没有父分类的分类)
// 获取所有无父分类的根节点,同时预加载所有子分类 $rootCategories = Category::whereDoesntHave('parents')->with('allChildren')->get();
3. 性能优化:使用递归查询(Laravel 9+)
如果分类层级较多,递归预加载可能产生N+1查询问题。Laravel 9及以上版本支持withRecursive方法,可一次性加载所有数据:
// 一次性获取所有分类及其所有子分类 $categories = Category::withRecursive(['allChildren'])->get();
4. 可选:生成树形结构数组
如果需要将分类数据格式化为前端常用的树形结构,可在模型中添加辅助方法:
/** * 将分类及其子分类转换为树形数组 */ public function toTreeArray() { $data = $this->only(['id', 'title', 'description']); $data['children'] = $this->allChildren->map(function ($child) { return $child->toTreeArray(); })->toArray(); return $data; }
使用示例:
$rootCategory = Category::with('allChildren')->find(1); $tree = $rootCategory->toTreeArray(); // $tree 即为包含所有子分类的树形结构数组
注意事项
- 确保
subcategories表的外键正确关联到categories表的id字段,迁移文件参考写法:
Schema::create('subcategories', function (Blueprint $table) { $table->id(); $table->unsignedBigInteger('category_id'); $table->unsignedBigInteger('parent_category_id'); $table->timestamps(); // 外键约束:删除分类时自动删除关联记录 $table->foreign('category_id')->references('id')->on('categories')->onDelete('cascade'); $table->foreign('parent_category_id')->references('id')->on('categories')->onDelete('cascade'); // 可选:添加唯一约束,避免同一父子关系重复添加 $table->unique(['category_id', 'parent_category_id']); });
- 如果你的业务场景是每个分类只能有一个父分类,更推荐在
categories表直接添加parent_id字段,使用belongsTo和hasMany的自关联,性能更好且实现更简单;但如果需要支持多父分类,上述多对多自关联方案完全适用。
内容的提问来源于stack exchange,提问作者Andreas Hunter
相关产品推荐
相关产品推荐

