【问题标题】:Laravel Recursive RelationshipsLaravel 递归关系
【发布时间】: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}

感谢您的任何见解。

【问题讨论】:

    标签: php laravel eloquent


    【解决方案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;
                }
            }
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2022-01-21
          • 2020-06-12
          • 2018-07-05
          • 2020-11-03
          • 2020-11-09
          • 2019-10-18
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多