I\'m working on a project in Laravel. I have an Account model that can have a parent or can have children, so I have my model set up like so:
public
I've created a package that uses common table expressions (CTE) to implement recursive relationships: https://github.com/staudenmeir/laravel-adjacency-list
You can use the descendants
relationship to get all children of an account recursively:
class Account extends Model
{
use \Staudenmeir\LaravelAdjacencyList\Eloquent\HasRecursiveRelationships;
}
$allChildren = Account::find($id)->descendants;
We're doing something similar, but our solution was this:
class Item extends Model {
protected $with = ['children'];
public function children() {
$this->hasMany(App\Items::class, 'parent_id', 'id');
}
}
This is how you can use recursive relations:
public function childrenAccounts()
{
return $this->hasMany('Account', 'act_parent', 'act_id');
}
public function allChildrenAccounts()
{
return $this->childrenAccounts()->with('allChildrenAccounts');
}
Then:
$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
This way you save a lot of queries. This will execute 1 query per each nesting level + 1 additional query.
I can't guarantee it will be efficient for your data, you need to test it definitely.
This is for childless accounts:
public function scopeChildless($q)
{
$q->has('childrenAccounts', '=', 0);
}
then:
$childlessAccounts = Account::childless()->get();
I think I've come up with a decent solution as well.
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);
}
}
Basically, I'm starting with a query builder relationship and adding orWhere conditions to it. In the case of finding all of the child organizations, I use a recursive function to drill down through the relationships.
Once I have the recursiveChildOrganizations relationship, I've run the only recursive function needed. All of the other relationships (I've shown recursiveCustomers but you could have many) use this.
I avoid instantiating the objects at every possible turn, since the query builder is so much faster than creating models and working with collections.
This is much faster than building a collection and recursively pushing members to it (which was my first solution), and since each method returns a query builder and not a collection, it stacks wonderfully with scopes or any other conditions you want to use.
For future reference:
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');
}
This is for a Category
model that has id, title, parent_id
. Here's the database migration code:
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');
});
public function childrenAccounts()
{
return $this->hasMany('Account', 'act_parent', 'act_id')->with('childrenAccounts');
}
This code returns all children accounts (recurring)