haheute
haheute

Reputation: 2199

laravel 4 - join latest result in group by query

This query gives a pagination of all 'albums' with a picture and description for each. Now I am trying to get always the latest picture of each album.

I have tried to add a second orderBy('pics.created_at') , but that did not work. I think I need some kind of subquery but don't know how.

        $query = AlbumPic::select(DB::raw('COUNT(pics.id) as picscount, 
                                           pics.url, 
                                           pics.user_id, 
                                           pics.created_at,
                                           albums.id as album_id, 
                                           albums.title, 
                                           albums.text, 
                                           users.username'))
                        ->join('albums','albums.id','=','album_pic.album_id')
                        ->join('pics','pics.id','=','album_pic.pic_id')
                        ->join('users','users.id','=','pics.user_id');

                if(!is_null($user_id)) 
                  $query->where('album_pic.user_id',$user_id);

        $albums = $query->groupBy('albums.id')
                        ->orderBy('albums.created_at','desc')
                        ->paginate(20);

edit

I made a mistake. I don't have created_at and updated_at in the album_pic table . So my 'Album' - model/relations are now like this:

public function pics()
{
    return $this->belongsToMany('Pic');
}

public function latestPic()
{
    return $this->belongsToMany('Pic')->latest('pics.created_at');
}

And the query now looks like this:

        $q = Album::with('pics')->with('latestPic.users');

                if(!is_null($user_id)) 
                  $q->where('albums.user_id',$user_id);

        $albums = $q->orderBy('albums.created_at','desc')
                    ->paginate(20);

This works. Only thing I would like to improve is the way, the pictures per album are counted. Now I get all with with('pics') and then do a count($album->pics) in the view. If there is a way to not load everything, but only count the pictures, it would be nice.

Upvotes: 1

Views: 1975

Answers (3)

damiani
damiani

Reputation: 7371

You need to get the MAX(created_at) inside a subquery; see MySQL select MAX(datetime) not returning max value for example.

Really, though, if you're doing this in Laravel, it would be better to set these all up as relations and leverage the power of Eloquent. Then, you can define a relationship for pictures that uses ->latest() to return the most recent. See laravel eloquent query group by last id for an example (which uses one table, but the principle is the same for multiple tables).


Here's how you could set this up using Eloquent relations:

User model (User.php)

class User extends Eloquent {

    public function albums()
    {
        return $this->hasMany('Album');
    }
}

Album model (Album.php)

class Album extends Eloquent {

    public function pics()
    {
        return $this->belongsToMany('Pic');
    }

    public function latestPic()
    {
        return $this->belongsToMany('Pic')->latest('album_pic.created_at');
    }
}

Because you have a many-to-many relationship between albums and pics, in the latestPic() relation, you must specify the album_pic.created_at field for latest()—since we are actually interested in the order of entries in the pivot table, rather than in the pics table.

Finally, link this all together. For example, for a user with id of 1:

$albums = User::find(1)->albums()->with('pics')->with('latestPic')->paginate(20);
    foreach($albums as $album) {

        echo('<br>Album:');
        var_dump($album->title);

        echo('All pics:');
        foreach($album->pics as $pic) {
            var_dump($pic->url);
        }

        echo('Latest pic:');
        $latestPic = $album->latestPic->first();

        if ($latestPic) {
            var_dump($latestPic->url);
        }
}

Note that we are eager loading the pics and latestPic to reduce the number on calls to the database. Also note that accessing the $latestPic->url is wrapped in an if statement, otherwise albums that do not have any photos will throw an error since $album->latestPic would return null.

As @cedie correctly noted, Laravel doesn't handle pagination all that efficiently when using a groupBy statement, but that shouldn't be a problem in this case. The underlying queries do not use groupBy, so you should be save to use ->paginate(20).

Upvotes: 2

Strawberry
Strawberry

Reputation: 33935

Perhaps ypu can figure out how to adapt this for your purposes...

SELECT ap.*
     , p.* 
  FROM album_pic ap 
  JOIN pics p
    ON p.id = ap.pic_id
  JOIN 
     ( SELECT ap.*
            , MAX(p.created_at) max_created_at 
         FROM album_pics ap 
         JOIN p.*
           ON p.id = ap.pic_id
     ) x
    ON x.album_id = ap.album_id
   AND x.max_created_at = p.created_at;

Upvotes: 0

Bogdan
Bogdan

Reputation: 44526

Try using this in your select query:

max(pics.created_at) as created_at

instead of this:

pics.created_at

So your code should look like this:

AlbumPic::select(DB::raw('COUNT(pics.id) as picscount, 
                                           pics.url, 
                                           pics.user_id, 
                                           max(pics.created_at) as created_at,
                                           albums.id as album_id, 
                                           albums.title, 
                                           albums.text, 
                                           users.username'))

Upvotes: 0

Related Questions