Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Eloquent eager loading with limits #4835

Closed
misotrnka opened this issue Jun 25, 2014 · 26 comments
Closed

Eloquent eager loading with limits #4835

misotrnka opened this issue Jun 25, 2014 · 26 comments

Comments

@misotrnka
Copy link

We are trying to implement eager loading with eloquent, where we want to get 5 conversations and for each conversation the 10 most recent messages. It seems that Eloquent only applies the limit once, returning 10 messages in total (as opposed to the expected result of up to 10 messages per each of 5 conversations).

The code:

 $conversations = ConversationModel::whereIn('id', $conversationsIds)
    ->with(
            array(
                'conversationUserData' => function ($query) use ($userId)
                {
                    $query->where('userId', '=', $userId);
                },
                'messages' => function ($query)
                {
                    $query->take(10);
                },
                'conversationUsers.user',
                'lastMessage.user',
            )
    )
    ->take(5)
    ->orderBy('created_at', 'DESC')
    ->get();

The problem seems to be the $query->take(10); part, which seems to apply only "once" on the whole query.

We tried to google on possible causes of this, but while it seems that quite a few people have the same problem, there is no good solution - with the exceptions of workarounds, like fetching the messages programatically, which are not really ellegant.

Is this a bug or is this intended behavior?

@FractalizeR
Copy link
Contributor

Is it related to #4841? Try to see what actual generated sql is by calling ->getQuery()->toSql() instead of get()

@taylorotwell
Copy link
Member

Yeah the limit is applied once.

@furey
Copy link

furey commented Nov 13, 2015

If anyone stumbles upon this issue, the following approach seems to work:

http://softonsofa.com/tweaking-eloquent-relations-how-to-get-n-related-models-per-parent/

I'm not exactly sure how it works, and it looks a little freaky, but it seems to work.

@darkylmnx
Copy link

darkylmnx commented Apr 8, 2017

still facing this in laravel 5.4 and there seems to be no easy wat to do this, based on how eloquent handles its queries today.

@Blackthorn42 any solution ?

@taylorotwell the problem here is, it will limit once, as you said, and thus : if i want to get 10 posts with 3 comment each, here i'll only get 3 comments for the whole query and not 3 comments per posts.

So far the only simple method was to load each comments by entry, giving 10 more queries in my example (10 more if you add the relationship with the comment author).

here was a solution way to complicated and more based on DB design than the use of the framework : https://softonsofa.com/tweaking-eloquent-relations-how-to-get-n-related-models-per-parent/

In IRL, if you are paging a feed for example, 20 posts per page, with their author and 5 comments per post with their author too

you get 2 reqs (post and post author) + 2 reqs (comment + comment author) x 20 (number of result) = 42 reqs

Is there a plan to solve this issue at some point ? Though this is more a performance issue than anything else but also for Eloquent's API to be cleaner, as the following, is really not clear when reading it, or should i say, doesn't act as the code seems to say it should act.

// this code says
// get the 20 last posts with their respective 5 last comments
// but doesn't do that
Post::with([
   'author',
   'comments' = >function ($query) {
      $query->latest()->take(5);
   },
   'comment.author'
])
->latest()
->paginate(20)

@diegosepulveda
Copy link

i am having the same issue, i thought that was the only one with that problem

@bart
Copy link
Contributor

bart commented Jun 22, 2017

Same issue here. Isn't there a simple solution like a conditional eager load statement?

@Kyslik
Copy link
Contributor

Kyslik commented Jun 22, 2017

This issue should be re-opened. Ping @themsaid.

@themsaid
Copy link
Member

This is not a bug, it's a limitation.

We're open to PRs that might address that limitation.

@powelski
Copy link

powelski commented Oct 2, 2017

This, among a few other things, just shows that we must treat Eloquent as a tool, not a solution. It's just a wrapper on SQL. And SQL itself never was, in my opinion, the most natural way to express querying data in anything programmed.

@raojeet
Copy link

raojeet commented Nov 2, 2017

@themsaid This is not a limitation after debugging and R&D I got the solution.

first append the relation count key Model
protected $appends = ['member_count'];

Then add relation
`
public function getMemberCountAttribute() {
return ($this->member) ? $this->member->count() : 0;
}

`

define relation with the limit

public function member() {
        return $this->hasMany(BudgetMember::class)->take(2);
    }

At last call the final function and you will get the desired result
Budget::paginate(10);
**Note: don't add with relation in select query getMemberCountAttribute will automatically add member (relation array) and count array **

I have also used this solution for nested relationship and it works perfectly

@darkylmnx
Copy link

@raojeet well... this isn't eager loading so your solution doesn't solves the issue.

@raojeet
Copy link

raojeet commented Nov 3, 2017

But you can use this trick with pagination.

@d3v97
Copy link

d3v97 commented Jan 5, 2018

Though this is not a good way to do this, it works. The idea is to fetch a chunk of result, then trim it down to how many rows you require. In this example, 5 posts for each category.

Also the category here is defined. If the category was not defined. You could use a limit (3).

Scenario of this example :

Tables :

  1. Posts Table
  2. Category Table

What I am trying to do ?

  1. Get 5 posts for each category using eloquent.

What I have ?
I have three categories defined ['A', 'B', 'C']

$result = $category::
    with(['posts' => function($q) {
        $q->take(50); //If you need 5 each for 3 where Ins, pull a big chunk of data, 50 for this example
    }])
    ->whereIn('category', ['A', 'B', 'C']) // If no category defined, replace with ->take(3)
    ->get()
    ->map(function ($query_result) { 

        //Though this is lame, but this is the only workaround I could figure out
        //Here $query_result is a collection and result of the query, received from get()
        //Only five posts are extracted for EACH category, rest are discarded

        //You can rebuild the whole result and format the way you want it
        $filtered_result['category'] = $query_result['category'];
        $filtered_result['id']       = $query_result['id'];
        //Returned collection originally, hence toArray()
        $filtered_result['post']     = $query_result['post']->take(5)->toArray();
        
        return $filtered_result;
    });

    dd($result->toArray());

Output :

array:3 [▼
  0 => array:3 [▼
    "category" => "A"
    "id" => 1
    "posts" => array:5 [▼
      0 => array:5 [▼
        "id" => 48059
        "category_id" => 1
        "post" => "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
        tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
        quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
        consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
        cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
        proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
      ]
      1 => array:5 [▶]
      2 => array:5 [▶]
      3 => array:5 [▶]
      4 => array:5 [▶]
    ]
  ]
  1 => array:3 [▶]
  2 => array:3 [▶]
]

@amjadkhan896
Copy link

@977developer performance issue.... and also needs to change a lot of code in the vue components

@collinticer
Copy link

I'm running into this today with Laravel 5.4 and I wanted to include my work around for any still encountering this issue.

For the original post for this issue, I'd assume that there is a relationship setup similar to this on the ConversationModel.

public function messages()
{
    return $this->hasMany('App\Message', 'conversation_model_id');
}

My workaround, is to include a new scoped relation based on the orignal messages() with the understanding that any time I call this new relation, I will only ever be taking a predetermined number of results. Because of this, it is probably easiest to apply any ordering inside of this function as well. This sort of hinders the use of this approach, but it works well for my use.

Here's what that looks like:

public function recent_messages()
{
    return $this->messages()->orderBy('created_date')->take(5);
}

And then, in my query, I am able to do something similar to this.

App\ConversationModel::where('user_id', 1)->with('recent_messages')->get();

May not work well in all use cases, but I wanted to include it here in case anyone else runs into this issue.

@mpyw
Copy link
Contributor

mpyw commented Jul 19, 2018

UNION ALL solution

#18014 (comment)

@KeitelDOG
Copy link

Be careful @collinticer as it will load only 5 messages for all the Users, and those 5 messages will be distributed to their respective User in the results. But it will not retrieve 5 messages for each User if ever each User had 5 or more messages. That's why you will see amongst all Users that has messages, some will has no message while some will have 1 or more messages.

@collinticer
Copy link

@KeitelDOG That makes sense and explains why I likely didn't notice that result during development. Thanks for catching that!

@WallyJ
Copy link

WallyJ commented Oct 4, 2018

@collinticer , so are you saying that your example does NOT give the expected outcome of 5 messages for each user? If not, have you found a better workaround? This should really be fixed in Laravel as a framework.

@collinticer
Copy link

@WallyJ alright so I did some further testing. In a single model use case, my solution works fine. So if you are only ever fetching a user and then outputting his most recent messages, my solution should work. So something like $user->recent_messages() would work.

However, if you are doing something where you are fetching a collection of users and trying to include their most recent message using something like $users->with('recent_messages') (key here being that it's a collection of users rather than a single user), then what @KeitelDOG describes is exactly right.

If you come up with a solution that works, I'd love to hear about it. I agree that it would be a nice feature to have it addressed directly in the Laravel Framework.

@WallyJ
Copy link

WallyJ commented Oct 8, 2018

So, I have logged in users, who have contacts, which have contact notes. I want the 3 most recent notes for each contact, belonging to a given user. This comes out as a collection of contacts for the logged in user. Sounds like your solution won't work for me as I am trying to use take(3) on each member of a collection, which doesn't seem to be a working function within Laravel at the moment. Am I understanding you correctly?

@collinticer
Copy link

Yes that's correct. Since you are working with a collection and trying to access a recent notes function for each item in the collection, my work around will not work for you. You could not simply do $user->contacts()->with('recent_notes') as that would (at most) contain three notes across the entirety of the contacts collections, rather than the three per contact as you would expect.

I think a pretty easy work around for this though would be something like this:

foreach($contact in $user->contacts){
    $contact->recent_notes()->get();
}

In many use cases, I think that little snippet would work fine if you don't mind iterating over the collection on page load like that.

@staudenmeir
Copy link
Contributor

I released the rejected PR as a package: https://github.com/staudenmeir/eloquent-eager-limit

@tillkruss
Copy link
Contributor

tillkruss commented Aug 29, 2019

If you're using PostgreSQL (and you should), just use the DISTINCT ON, no need for a package 👌🏻

SELECT DISTINCT ON (location) location, time, report
    FROM weather_reports
    ORDER BY location, time DESC;

Eager load the last message for each conversation:

$conversations = $user->conversations()
    ->with(['messages' => function ($query) {
        $query->selectRaw('DISTINCT ON (conversation_id) conversation_id, *');
        $query->orderByRaw('conversation_id, created_at DESC');
    }])
    ->get();

@AmirKaftari
Copy link

AmirKaftari commented Aug 16, 2020

just do like this :

$feed= Feed::with(['comments'])->get() ->map(function ($query) { $query->setRelation('comments', $query->comments->take(10)); return $query; });

replace your model on before query.
regards

@staudenmeir
Copy link
Contributor

Eager loading with limit will be supported natively in Laravel 11 (#49695).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests