lazychaser / laravel-nestedset

Effective tree structures in Laravel 4-8
3.67k stars 471 forks source link

eager load ancestors/descendants on related model #271

Open ashgibson opened 6 years ago

ashgibson commented 6 years ago

I have a data structure with Products and Categories (nested set) but these models are scoped to an account_id. How can get a product and eager load the category and descendants or ancestors?

Due to the scope, I can see the query is adding scope with account_id = NULL.

$products = Product::with('category.ancestors')->get();
class Product extends Model {

   protected $guarded = [
      'id',
   ];

   public function account() {
      return $this->belongsTo( Account::class );
   }

  public function category() {
     return $this->belongsTo(Category::class);
  }
}
class Category extends Model {

   use NodeTrait;

   protected $guarded = [
      'id',
      NestedSet::LFT,
      NestedSet::RGT,
      NestedSet::PARENT_ID
   ];

   protected function getScopeAttributes() {
      return [ 'account_id' ];
   }

   public function account() {
      return $this->belongsTo( Account::class );
   }

  public function products() {
     return $this->hasMany(Product::class);
  }
}
ashgibson commented 6 years ago

Any comment, update on this?

kelyak commented 5 years ago

Same issue and I think I found the issue. As lazychaser said, this has been tested but for the case where you try to load the model with its ancestors/descendants. The issue arise when you try to eagerload the model with an intermediary relation like "category.ancestors", and here is why :

This is as far as I got for now. But if you're still on the issue (since you came back 8 months after your first post), it might be helpful. I'll try adding more information or even make PR if I find a solution.

ashgibson commented 5 years ago

It is still an issue but I have a fix which is working for my application. The fix does have potential performance issues but it is working for the moment.

In case anyone is interested, I have added some code to my CategoryResource class as follows:

public function toArray($request)
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'lft' => $this->_lft,
            'rgt' => $this->_rgt,
            'children' => CategoryResource::collection($this->whenLoaded('children')),
            'ancestors' => CategoryResource::collection($this->whenLoaded('ancestors', function() {
                return $this->getAncestors();
            }))
        ];
    }

My ItemResource class does this

'category' => CategoryResource::make($this->whenLoaded('category')),

Because the scope_id is null the ancestors relationship is loaded but contains no data. This code does a lazy load of the ancestors for each item as Laravel loops through the collection and transforms the data with the resource. The getAncestors() applies the correct scoping.

Ciaro commented 5 years ago

I can confirm that this is an issue.

I'm attempting to implement nodes in https://laravel-json-api.readthedocs.io/en/latest/, but it's tricky to do with this bug.

Eager loading is done automatically in the library when loading related resourced (with the include parameter).

In Schema.php I currently also have to use the work-around by calling the relationship directly, but that gives a ton of overhead...

return [
     'descendants' => [
                self::SHOW_SELF => true,
                self::SHOW_RELATED => true,
                self::SHOW_DATA => isset($includedRelationships['descendants']),
                self::DATA => function () use ($resource) {
                    // Directly calling $resource->descendants does not work,
                    // cause of this issue https://github.com/lazychaser/laravel-nestedset/issues/271.
                    // @todo: this needs to be addressed at some point, since there's a lot of overhead when called with include.
                    return $resource->getDescendants();
                },
            ],
]

I tried to xdebug this, but the internals make it pretty hard to follow. Of what I can tell, the relationship is accessed before there's actually data, so the scope parameter is null.

Ciaro commented 5 years ago

After some more digging I came up with a solution for this issue:

https://github.com/Ciaro/laravel-nestedset/commits/fix-scoped-relationships

@ashgibson have a look and see if that fixes the issue :)