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_id
的Category
模型。这是数据库迁移代码:
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 递归关系的主要内容,如果未能解决你的问题,请参考以下文章