Laravel 递归关系

Posted

技术标签:

【中文标题】Laravel 递归关系【英文标题】:Laravel Recursive Relationships 【发布时间】:2014-12-26 11:18:44 【问题描述】:

我正在 Laravel 中开发一个项目。我有一个可以有父或子的 Account 模型,所以我的模型设置如下:

public function immediateChildAccounts()

    return $this->hasMany('Account', 'act_parent', 'act_id');


public function parentAccount()

    return $this->belongsTo('Account', 'act_parent', 'act_id');

这很好用。我想要做的是让所有孩子都在某个帐户下。目前,我正在这样做:

public function allChildAccounts()

    $childAccounts = $this->immediateChildAccounts;
    if (empty($childAccounts))
        return $childAccounts;

    foreach ($childAccounts as $child)
    
        $child->load('immediateChildAccounts');
        $childAccounts = $childAccounts->merge($child->allChildAccounts());
    

    return $childAccounts;

这也有效,但我不得不担心它是否很慢。这个项目是我们在工作中使用的一个旧项目的重写。我们将有数千个帐户迁移到这个新项目。对于我拥有的少数测试帐户,此方法不会造成性能问题。

有没有更好的解决方案?我应该只运行原始查询吗? Laravel 有什么可以处理的吗?

总结 对于任何给定的帐户,我想要做的是在单个列表/集合中获取每个子帐户及其孩子的每个孩子等等。一张图:

A -> B -> D
|--> C -> E
     |--> F 
G -> H

如果我运行 A->immediateChildAccounts(),我应该得到 B, C 如果我运行 A->allChildAccounts(),我应该得到 B, D, C, E, F(顺序无关紧要)

同样,我的方法有效,但似乎我执行的查询太多了。

另外,我不确定在这里问这个是否可以,但它是相关的。如何获取包含子帐户的所有帐户的列表?所以基本上与上述方法相反。这样用户就不会尝试为帐户提供已经是孩子的父母。使用上面的图表,我想要(在伪代码中):

Account::where(account_id 不在 (A->allChildAccounts()))。所以我会得到 G, H

感谢您的任何见解。

【问题讨论】:

【参考方案1】:

这是使用递归关系的方法:

public function childrenAccounts()

    return $this->hasMany('Account', 'act_parent', 'act_id');


public function allChildrenAccounts()

    return $this->childrenAccounts()->with('allChildrenAccounts');

然后:

$account = Account::with('allChildrenAccounts')->first();

$account->allChildrenAccounts; // collection of recursively loaded children
// each of them having the same collection of children:
$account->allChildrenAccounts->first()->allChildrenAccounts; // .. and so on

这样可以节省大量查询。这将为每个嵌套级别执行 1 个查询 + 1 个附加查询。

我不能保证它对你的数据有效,你一定要测试它。


这是针对没有孩子的帐户:

public function scopeChildless($q)

   $q->has('childrenAccounts', '=', 0);

然后:

$childlessAccounts = Account::childless()->get();

【讨论】:

这不是我想要做的。我需要一个集合中的所有子帐户。我提供的方法可以做到这一点,我只是不确定它的效率如何。同样,第二种解决方案也不是我想要的。我需要所有不是调用函数的帐户的子帐户,而不是无子帐户。 1 这就是您现在正在做的事情,只是您调用 db query N 倍于建议的解决方案。 2 然后改写你的问题,因为它不是你写的。即使现在在您发表评论之后,您也不清楚您在问什么 - 直系子女?后代? 哇。你是天才。我的代码现在快了两倍 :) @Roark 简而言之就是 Eloquent ;) @JarekTkaczyk 你能帮我限制树吗? F.e 我不想选择超过 4 个级别。【参考方案2】:
public function childrenAccounts()

    return $this->hasMany('Account', 'act_parent', 'act_id')->with('childrenAccounts');

此代码返回所有子帐户(循环)

【讨论】:

这在您从同一模型声明分层数据时非常有用。更简单,您可以将关系命名为 children,这与显示它们的大多数 UI 元素兼容。【参考方案3】:

我创建了一个使用公用表表达式 (CTE) 实现递归关系的包:https://github.com/staudenmeir/laravel-adjacency-list

您可以使用descendants 关系递归地获取帐户的所有子项:

class Account extends Model

    use \Staudenmeir\LaravelAdjacencyList\Eloquent\HasRecursiveRelationships;


$allChildren = Account::find($id)->descendants;

【讨论】:

我希望几个月前能找到这个。伟大的包乔纳斯!【参考方案4】:

供将来参考:

public function parent()

    // recursively return all parents
    // the with() function call makes it recursive.
    // if you remove with() it only returns the direct parent
    return $this->belongsTo('App\Models\Category', 'parent_id')->with('parent');


public function child()

    // recursively return all children
    return $this->hasOne('App\Models\Category', 'parent_id')->with('child');

这是针对具有id, title, parent_idCategory 模型。这是数据库迁移代码:

    Schema::create('categories', function (Blueprint $table) 
        $table->increments('id');
        $table->timestamps();
        $table->string('title');
        $table->integer('parent_id')->unsigned()->nullable();
        $table->foreign('parent_id')->references('id')->on('categories')->onUpdate('cascade')->onDelete('cascade');
    );

【讨论】:

这是一个很好的解决方案,但是这样做可能会导致很多查询,例如深度为 3,每个有 5 个数据,第一个查询获取 5 行,第二个查询获取这 5 行的子项,并且....下一个查询将是最后一个查询结果为 5 的递归查询,如果还有其他深度,将是额外的 25 个查询,CMIIW(尚未测试)【参考方案5】:

我们正在做类似的事情,但我们的解决方案是这样的:

class Item extends Model 
  protected $with = ['children'];

  public function children() 
    $this->hasMany(App\Items::class, 'parent_id', 'id');
 

【讨论】:

这不会获取所有子子记录。 接受的答案和我的唯一区别是,接受的答案定义了一个访问器,而在我的情况下,关系是急切加载的。接受的解决方案确实在使用上更加灵活...... @SizzlingCode 是的,它会的。每个Item 模型都会递归地加载children,如果没有针对根循环的保护,这实际上有点狡猾。接受的答案只是一个臃肿的(不必要的)版本。【参考方案6】:

我正在做类似的事情。我认为答案是缓存输出,并在数据库更新时清除缓存(假设您的帐户本身没有太大变化?)

【讨论】:

【参考方案7】:

我想我也想出了一个不错的解决方案。

class Organization extends Model

    public function customers()
    
        return $this->hasMany(Customer::class, 'orgUid', 'orgUid');
    

    public function childOrganizations()
    
        return $this->hasMany(Organization::class, 'parentOrgUid', 'orgUid');
    

    static function addIdToQuery($query, $org)
    
        $query = $query->orWhere('id', $org->id);
        foreach ($org->childOrganizations as $org)
        
            $query = Organization::addIdToQuery($query, $org);
        
        return $query;
    

    public function recursiveChildOrganizations()
    
        $query = $this->childOrganizations();
        $query = Organization::addIdToQuery($query, $this);
        return $query;
    

    public function recursiveCustomers()
    
         $query = $this->customers();
         $childOrgUids = $this->recursiveChildOrganizations()->pluck('orgUid');
         return $query->orWhereIn('orgUid', $childOrgUids);
    


基本上,我从查询构建器关系开始,并向其添加 orWhere 条件。在查找所有子组织的情况下,我使用递归函数来向下钻取关系。

一旦我有了 recursiveChildOrganizations 关系,我就运行了唯一需要的递归函数。所有其他关系(我已经展示了 recursiveCustomers 但你可以有很多)都使用这个。

我避免每次都实例化对象,因为查询构建器比创建模型和使用集合要快得多。

这比构建一个集合并递归地将成员推送给它(这是我的第一个解决方案)要快得多,并且由于每个方法都返回一个查询构建器而不是一个集合,它与范围或您想要的任何其他条件完美地堆叠在一起使用。

【讨论】:

【参考方案8】:

如果您认为这是您始终需要访问的东西,您也可以将此关系添加到模型的“with”属性中:

protected $with = [
    'childrenAccounts'
];

【讨论】:

这在性能方面可能非常危险【参考方案9】:

我在表(用户)中创建了 managed_by,这个解决方案递归地获得了所有无限级别的孩子。

在 [用户] 模型中

 public function Childs()
        return $this->hasMany('App\User', 'managed_by', 'id')->with('Childs');
    

在 [helpers] 文件中(我的魔法解决方案)

if (!function_exists('user_all_childs_ids')) 
        function user_all_childs_ids(\App\User $user)
        
            $all_ids = [];
            if ($user->Childs->count() > 0) 
                foreach ($user->Childs as $child) 
                    $all_ids[] = $child->id;
                    $all_ids=array_merge($all_ids,is_array(user_all_childs_ids($child))?user_all_childs_ids($child):[] );
                
            
            return $all_ids;
        
    

【讨论】:

以上是关于Laravel 递归关系的主要内容,如果未能解决你的问题,请参考以下文章

Laravel 递归 whereHas on BelongsToMany 关系

Laravel 一对多关系不起作用 - 返回递归

Laravel recursive where 递归关系

在laravel 5.1单元测试中嘲笑hasMany关系

如何在 laravel 中验证递归对象

laravel中的递归外键[重复]