1

I have a collection called User, I also have an array containing two models Post relating to the User model.

The User collection contains a primary key id and each model in my Post collection I have a foreign key user_id.

I am currently executing the following:

foreach ($users as $user) {
    foreach ($posts as $post) {
        if ($post->user_id == $user->id) {
            $user->posts->push($post);
        }
    }
}

This somewhat works, but not entirely because it pulls in all related posts instead of the recent two posts a user has made.


The array looks like the following:

enter image description here

My User schema looks like; with a hasMany relationship to Post:

enter image description here

Ash
  • 3,242
  • 2
  • 23
  • 35
zen
  • 1,115
  • 3
  • 28
  • 54
  • Are you simply trying to associate N `$posts` with `$user`? – Ash Jan 06 '16 at 00:47
  • Why would you want to insert some of the manually? You should more than likely be using Laravel relationships and doing something like `$users = Users::with('Posts')->all()` – Jeremy Harris Jan 06 '16 at 00:51
  • @ash, yes pretty much. – zen Jan 06 '16 at 00:56
  • @Jeremy Harris, as I mentioned in my question, I only want two most recent posts of each user and I haven't been able to figure out how to do that in eloquent so had to run a manual query for that data. – zen Jan 06 '16 at 00:56
  • @zen I hope my answer helps you solve your problem :) – Ash Jan 06 '16 at 00:56
  • 1
    The below solutions will only work if you are retrieving one user. For a collection, they won't work because it is not as simple as adding limit or take. It's more complex, and I suggest reading this answer: http://stackoverflow.com/questions/26247467/laravel-limit-each-child-item-efficiently#answer-26251935 – Thomas Kim Jan 08 '16 at 17:39
  • @ThomasKim Awesome! That did it! Thank you so much. If you want to put that in the answer I will mark it and release bounty to it. – zen Jan 09 '16 at 03:51
  • Thanks :) I posted it as an answer, although I feel weird doing that because it wasn't my answer. Lol. – Thomas Kim Jan 11 '16 at 17:19

3 Answers3

3

You can load the posts associated to a User using with, something like

$user = User::with('posts')->find($id);

But your scenario sounds specifically collecting the latest two Post belonging to a User. To limit your results you can also use scopes.

Something like the following on your Post model would work:

public function scopeLatest($query, $latest = 2)
{
    return $query->limit($latest);
}

Then collect these by:

// The user record.
$user = User::find($id);

// Latest 2 posts for this user.
$posts = $user->posts()->latest();

// Latest 5 posts for this user.
$posts = $user->posts()->latest(5);

However, should you with to load the latest 2 posts with the user in a single query - then you could make a new relation:

public function latestPosts()
{
    return $this->hasMany(Post::class,  'post_id', 'id') 
        ->orderBy('created_at', 'ASC')
        ->limit(2);
}

This would work in the following way:

// Load the user with the latest 2 posts.
$user = User::with('latestPosts')->find($userId);

// Access these using; this will be a Collection containing 2 `Post` records.
dd($user->latestPosts);

Basically with Eloquent, when you call $this->latestPosts Eloquent will run latestPosts() and hydrate the related records. Using with this hydration occurs with a single query and the relations are already defined.

The difference between the method latestPosts() and the property $latestPosts is simple.

The method will always return a specific Relation Collection allowing you to chain additional conditions;

So: $user->latestPosts()->get() is the same as $user->latestPosts.

Ash
  • 3,242
  • 2
  • 23
  • 35
  • Nah that won't work. Looks like `sync` method actually deletes stuff from the database which I do not want. The second solution is a problem because that will fetch all `posts` not just 2 most recent for each user. The third solution also will not work because that fetches all posts for the user rather than only last 2. – zen Jan 06 '16 at 02:39
  • With `sync` as my example shows you can parse false in the second parameter to NOT delete things. – Ash Jan 06 '16 at 02:42
  • @zen I've updated my answer, this will help clear things up. – Ash Jan 06 '16 at 02:50
  • does `sync` work on this type of relationship? I'm reading about it in docs and I only see it mentioned for many-to-many associations. I will check out the other parts of your answer and respond soon. – zen Jan 06 '16 at 02:56
  • I've updated my answer with a reference to the source. – Ash Jan 06 '16 at 03:02
  • I do want to keep it in a single query. So I'm trying to implement the last solution. Now is that supposed to be for the User or Post model? Also how do any of these you posted determine the "last 2" rows of each group? Wouldn't it have to look at `created_at` timestamp or at least the `id` column? – zen Jan 06 '16 at 04:07
  • @zen I have updated your question and my answer I'm sure this will solve your problem. – Ash Jan 06 '16 at 09:37
  • What is the `$userId` variable in the `find` clause? Since I want last 2 posts from ALL users, am I supposed to put an array with all user's IDs there? – zen Jan 06 '16 at 14:19
2

You cannot use query constraints / eager loading to do this. Doing so will only work if you are retrieving the posts for one user. However, if you try to retrieve the posts for multiple users, it will fail because eager loading / query constraints will limit the related results as a whole. To understand, you have to look at the queries Eloquent generates. Lets take a look at an example where you only need one user's posts.

$user = User::with(['posts' => function($query) {
    $query->limit(2);
}])->find(1);

In this example, we are getting a user with a primary key of 1. We also also retrieving his/her posts but limiting it so we only retrieve 2 posts. This works, and it will generate 2 queries similar to this:

select * from `users` where `users`.`id` = 1 limit 1
select * from `posts` where `posts`.`user_id` in (1) limit 2

Okay. Now, why doesn't this work if you try to get more than 1 user (or a collection of users)? For example:

$user = User::with(['posts' => function($query) {
    $query->limit(2);
}])->get();

In this case, I changed find(1) to get(), and it will generate 2 queries like this:

select * from `users`
select * from `posts` where `posts`.`user_id` in (?, ?, ?, ... ?) limit 2

It's important to take a look at the second query. It's retrieving all the related posts, but at the end, you'll see that it has limit 2. In other words, it's limiting the entire related collection to only 2, which is why query constraints do not work for this.

Achieving this is actually pretty complex, but a fellow member (Jarek Tkaczyk) came up with a solution using MySQL variables, which you can find here: Laravel - Limit each child item efficiently

Community
  • 1
  • 1
Thomas Kim
  • 15,326
  • 2
  • 52
  • 42
0

You can do this a bit simpler with https://laravel.com/docs/5.2/eloquent-relationships#eager-loading constraints.

Example: Users have many Dogs, but only take 2

    $user = App\User::with(['dogs'  => function ($query) {
        $query->limit(2);
    }])->find($user_id);

    dump($user);

The anonymous constraining function would also have an orderBy in your case

jmadsen
  • 3,635
  • 2
  • 33
  • 49
  • What is the `$user_id` variable supposed to be? Since I want last 2 posts from ALL users, am I supposed to put an array with all user's IDs there? – zen Jan 06 '16 at 14:20
  • @zen if you omit `find`, it'll get all users but you will need `->all()` – Ash Jan 06 '16 at 15:09
  • Yea so this doesn't really answer the question since it still needs to run once per each user. I need a combination of `all()` and `limit(2)` per each so it can grab all data in one query. The only reason I did an additional manual query was to fetch all the data for all users I need in one run. – zen Jan 06 '16 at 15:27
  • obviously, I didn't write the exact code for you. I assumed you could figure out the differences you need to make for your own work – jmadsen Jan 06 '16 at 22:03
  • No I am saying your code will not work. It has to run each time for each individual user, so if I had 500 users, it would run 500 times. I could have done that already. My question was to get all users in one query. – zen Jan 06 '16 at 22:16