58

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 function immediateChildAccounts()
{
    return $this->hasMany('Account', 'act_parent', 'act_id');
}

public function parentAccount()
{
    return $this->belongsTo('Account', 'act_parent', 'act_id');
}

This works fine. What I want to do is get all children under a certain account. Currently, I'm doing this:

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;
}

This also works, but I have to worry if it's slow. This project is the re-write of an old project we use at work. We will have several thousand accounts that we migrate over to this new project. For the few test accounts I have, this method poses no performance issues.

Is there a better solution? Should I just run a raw query? Does Laravel have something to handle this?

In summary What I want to do, for any given account, is get every single child account and every child of it's children and so on in a single list/collection. A diagram:

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

If I run A->immediateChildAccounts(), I should get {B, C}
If I run A->allChildAccounts(), I should get {B, D, C, E, F} (order doesn't matter)

Again, my method works, but it seems like I'm doing way too many queries.

Also, I'm not sure if it's okay to ask this here, but it is related. How can I get a list of all accounts that don't include the child accounts? So basically the inverse of that method above. This is so a user doesn't try to give an account a parent that's already it's child. Using the diagram from above, I want (in pseudocode):

Account::where(account_id not in (A->allChildAccounts())). So I would get {G, H}

Thanks for any insight.

Troncoso
  • 2,343
  • 3
  • 33
  • 52

9 Answers9

121

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();
Jarek Tkaczyk
  • 78,987
  • 25
  • 159
  • 157
  • 1
    This isn't what I'm trying to do. I need all child accounts in a single collection. The method I provided does this, I'm just not sure how efficient it is. As well, the second solution is not what I'm looking for either. I need all accounts that are not children of the account calling the function, not childless accounts. – Troncoso Oct 30 '14 at 22:21
  • 1
    1 This is what you're doing right now, only you call db query N times more than with suggested solution. 2 then rephrase your question, because it is not what you wrote. Even now after your comment it's unclear of what you're asking - direct children? descendants? – Jarek Tkaczyk Oct 30 '14 at 23:05
  • 2
    Wow. you're genius. My code is two times faster now :) – Nurbol Alpysbayev Feb 24 '18 at 09:34
  • 2
    @Roark that's Eloquent in a nutshell ;) – Jarek Tkaczyk Mar 16 '18 at 22:09
  • @JarekTkaczyk can you help me to limit tree ? F.e I dont want to select more than 4 levels. – Yur Gasparyan Aug 14 '18 at 09:50
  • @YurGasparyan I don't recommend this solution for anything deeply nested. there's also no automatic way to limit as you request, so you have to come up with custom logic. – Jarek Tkaczyk Aug 16 '18 at 13:28
  • @JarekTkaczyk thanks. I think the best solution to have tree structure is use the nestedset ! – Yur Gasparyan Aug 16 '18 at 13:56
  • @YurGasparyan nested set or closure table probably will do, depending on your needs. I recommend this https://www.slideshare.net/quipo/trees-in-the-database-advanced-data-structures – Jarek Tkaczyk Aug 21 '18 at 12:17
  • Great, Save time and much helpful – Qazi Dec 05 '18 at 11:01
  • @JarekTkaczyk I tried this solution. it is working in tinker but not in my controller. what should I do? it is giving an error: Class 'Category' not found. but I have `use App\Category;` in my controller. – Akshay Rathod Jul 23 '20 at 16:05
  • @AkshayRathod import class in your controller with `use App\Category;` on top. In general I suggest you use PHPStorm (or another IDE) which will do it for you and help a lot if you're beginning your journey with PHP. Enjoy! – Jarek Tkaczyk Jul 25 '20 at 05:25
  • @JarekTkaczyk I had `use App\Category;` but still it was not working. I changed `$this->hasMany('Category', 'parent');` to `$this->hasMany(Category::class, 'parent');` and it worked. But thank you for your suggestions and I use Vscode :) – Akshay Rathod Jul 25 '20 at 05:52
  • Not working with Eloquent API Resources, how can I use them? – Daniyal Javani Aug 24 '20 at 08:52
  • @DaniyalJavani don't – Jarek Tkaczyk Aug 25 '20 at 10:24
18

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;
Jonas Staudenmeir
  • 24,815
  • 6
  • 63
  • 109
17
public function childrenAccounts()
{
    return $this->hasMany('Account', 'act_parent', 'act_id')->with('childrenAccounts');
}

This code returns all children accounts (recurring)

George
  • 301
  • 2
  • 4
  • This is quite useful when you are declaring Hierarchical data from the same model. Simpler as you can name the relationship as `children` which is compatible with most UI elements that display them. – Yousof K. Jan 16 '20 at 16:21
8

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');
    });
Vahid Amiri
  • 10,769
  • 13
  • 68
  • 113
  • this is a good solution tbh, but by doing this, it could cause a lot of queries, e.g. with depth of 3, and each has 5 data, the first query is getting 5 rows, second query is getting the children of those 5 rows, and.... next query will be recursive queries of the last query result which is 5, and if there is another depth, it will be additional 25 queries, CMIIW (not tested yet) – Raiika Aug 07 '21 at 14:16
5

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');
 }
}
Meki
  • 395
  • 1
  • 7
  • 13
  • This will not get all the sub children records. – Sizzling Code Jan 31 '19 at 13:14
  • The only difference between the accepted answer and mine, is that the accepted one defines an accessor for that, while in my case the relation is eager loaded. The acceppted solution is indeed more flexible in usage though... – Meki Feb 04 '19 at 06:42
  • 1
    @SizzlingCode Yes, it will. Every `Item` model will recursively eager load `children`, which is actually a little dodgy if there's no protection against root cycles. The accepted answer is just a bloated (unnecessary) version of this. – Stephen Lake Nov 24 '19 at 13:49
3

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.

2

I'm doing something similar. I think the answer is to cache the output, and clear the cache any time the database is updated (provided your accounts themselves don't change much?)

Elliot
  • 1,457
  • 13
  • 40
0

You could also add this relation to the "with" property of your model if you think this is something that you need always to have access to :

protected $with = [
    'childrenAccounts'
];
Vahidrk
  • 1,178
  • 10
  • 5
0

I made managed_by in Table (users) and this Solution get all unlimited levels of children Recursively.

in [User] Model

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

in [helpers] file (My magic Solution )

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;
        }
    }
Mostafa Mahmoud
  • 153
  • 3
  • 7