You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为无限层级的分类与子分类建立多对多模型关联?

实现无限级分类的多对多递归关联

首先,根据你的表结构,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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.11 08:05:37