CGuy's avatar
Level 5

Eager Load latest relation on many to many relationships

Hey,

I've got a problem that seemed quite simple at first but nobody seems to have found an answer yet.

I have two classes: Articles and Tags in a many to many relationship. What I am trying to do is accessing the latest Article foreach Tag in an efficient way.

I tried the following approaches:

$tags = Tag::with(['article' => function($query) {$query->latest()->take(2); }])->get();

But this only returns two articles for the whole query and not for each tag.

Then I tried to add a new accessor in the Tag class.

public function latestArticle()
{
    return $this->belongsToMany('App\Article')->latest()->take(2);
}

But I end up with the same problem.

What I am ultimately trying to achieve is listing all the tags paginated and ordered by the number of articles that are linked to them, with the two latest articles for each of those tags.

Does anybody have an idea how to achieve this? thanks

0 likes
9 replies
CGuy's avatar
Level 5

Hi, thanks for your answer. I don't think this would be a good way as, for a many to many relationship, you would need to make a new request for each article which is highly inefficient.

phildawson's avatar

Then I tried to add a new accessor in the Tag class.

@gchaumont I would have though that should work?

class Tag extends Model 
{
    public function articles()
    {
        return $this->belongsToMany('App\Article');
    }
    
    public function latestArticle()
    {
        return $this->articles()->latest()->take(2);
    }
}
$tags = Tag::with('latestArticle')->get();
veve286's avatar

You cant do in eager loading because Eager loading is finding relation like this.


select * from article wherIn('tag_id',[1,2,3])

So you cant limit result in eager loading. One way to do is chunking result using collection method.

You can limit like this


    @foreach( $tags as $tag)
        $tag->article->take(2);
    @endforeach( )

or you can override parent model method in your tag model like this.
    
public function setRelation($relation, $value)
    {
    if($relation=='article'){
        //only limit for articles relation
            $this->relations[$relation] = $value->take(2);
        }
        else{
            $this->relations[$relation] = $value;
        }
    return $this;
}

CGuy's avatar
Level 5

Thanks for the answers, but in both cases, it only loads two relations for the whole query instead of two relations for each tag.

veve286's avatar

Tag::with('article')->get( ); call ur data like this and override your tag model like I said. It should work cause it is tested.

JarekTkaczyk's avatar

@gchaumont Regarding my article and the linked comment - no, you won't need new request for each article - it is eager loading. It will run exactly 2 queries.

Other than that you could tweak that solution anyway, and use a bit of raw query magic, but it will be much harder to implement and you won't be able to abstract it.

The above will be OK for latest related model, but for n per parent - that's completely different story: http://softonsofa.com/tweaking-eloquent-relations-how-to-get-n-related-models-per-parent/

Mind that for m-m relation it is not that easy in eloquent, so I wouldn't implement it.

And last thing - to order by relation or its count, you need joins - there is no other way, unless you load all the models.

staudenmeir's avatar

Laravel has no native support for eager loading with a limit per parent.

I created a package for it: https://github.com/staudenmeir/eloquent-eager-limit

Use the HasEagerLimit trait in both the parent and the related model.

class Tag extends Model {
    use \Staudenmeir\EloquentEagerLimit\HasEagerLimit;
}

class Article extends Model {
    use \Staudenmeir\EloquentEagerLimit\HasEagerLimit;
}

Then you can apply ->take(2) to your relationship and you get the expected result.

1 like

Please or to participate in this conversation.