0

I have a situation where I need to iterate through a models relationships (1 to many) and call a function on the children. i.e.

$parent = ParentClass::find(1)
$children = $parent->children;
foreach($children as $child)
{
    $child->function();
}

The function inside the child needs to access fields from the parent model. i.e.

public function function()
{
    if($this->parent->attribute)
        return "True!";
    return "False";
}

Executing this as above, ends up with an N+1 query problem. Each child ends up executing "SELECT * FROM parents WHERE id = 1".

I've had 3 thoughts on how to fix this, though two of them feel awful, and the last one just doesn't seem great to me, so I'm asking is there a better solution to this than what I have considered?

Solution 1: Eager load the parent onto the child models. i.e.

...
$children = $parent->children;
$children->load('parent');
foreach($children as $child)
...

I think it's pretty obvious why this would be awful, this would be a tremendous memory hog to have the parent model stored in memory N+1 times.

Solution 2: Pass the parent to the child as an argument, i.e.

...
foreach($children as $child)
{
    $child->function($parent);
}
...

Avoids the memory issue of 1, but it still feels ugly. I would think the child should know it's parent, and not need to be told it through the method args.

Solution 3: Add remember() to the child's parent relationship i.e.

public function parent()
{
    return $this->hasOne('Parent')->remember(1);
}

Since all the children have the same parent, this will cache the query, and avoid calling it for every child. This seems the best of these 3 solutions, but I don't like the idea of having this be something that becomes mandatory on the relationship.

Is there a better approach to this that I am not considering? Maybe a better place to include the remember function?

Thank you, -Wally

EpicWally
  • 287
  • 3
  • 14

2 Answers2

0

If you have the inverse relationship defined you could just do something like:

$parent = ParentClass::find(1);
foreach ($parent->children as $child) {
    $requiredParentAttr = $parent->attr;
    $child->function($requiredParentAttr);
    // whatever else
}
Matthew Brown
  • 4,876
  • 3
  • 18
  • 21
  • I posted a simplified example, the function relies on many attributes of the parent. Also, in your example, you wouldn't need to go through $child-parent->attr, since the $parent is still in scope, you could simply do $requiredAttr = $parent->attr; – EpicWally Apr 14 '15 at 18:18
0

I think I found a solution that I am content with. Still doesn't feel "Eloquent" to me, but I don't think there's likely to be a better solution, (Though feel free to prove me wrong.)

foreach($children as $child)
{
    $child->parent = $parent;
    $child->function();
}

Since we already have the parent object, just give assign the parent as a reference to each child. This avoids any memory issues. This is even better than the remember() option, in that it never performs a 2nd query for the parent. This keeps the method signatures clean and logical.

EpicWally
  • 287
  • 3
  • 14