1

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.

haheute
  • 2,129
  • 3
  • 32
  • 49
  • what's the question? – Ceeee Oct 27 '14 at 11:11
  • also, laravel says: "Note: Currently, pagination operations that use a groupBy statement cannot be executed efficiently by Laravel. If you need to use a groupBy with a paginated result set, it is recommended that you query the database manually and use Paginator::make." – Ceeee Oct 27 '14 at 11:12
  • ok. now, I always get the first picture from the album_pic table. I want to get the picture with the _latest created_at - value_ in pics - table. So that I don't have always the same picture, but always the latest picture of the album. – haheute Oct 27 '14 at 11:28
  • Can a picture belong to multiple albums? – Strawberry Oct 27 '14 at 11:49
  • yes, a picture can be in multiple albums. album_pic is a pivot table that only stores the relation of pics and albums. (id,pic_id,album_id,created_at,updated_at) – haheute Oct 27 '14 at 12:07
  • There is a fairly convoluted method of getting a `count` without loading all the pics, but I'm not sure if it's worth the trouble from a performance standpoint, and if you're not careful it can lead to a bunch of additional queries. The method is described [here](http://stackoverflow.com/a/26004948/4043861) and [here](http://laravel.io/forum/05-03-2014-eloquent-get-count-relation). – damiani Oct 27 '14 at 20:12
  • I do believe that in your edited `latestPic` code above, you should be calling `->latest('album_pic.created_at')` (and you should have a `created_at` column in your pivot table) rather than `->latest('pics.created_at')`. If pics can belong to multiple albums, then right now you are getting the pic in an album that was most recently added to the *database*, rather than the pic that was most recently added to that *album*. So if someone adds a photo to an album that has already been used in another album, that photo might not show up as the `latest` photo in that album. – damiani Oct 27 '14 at 20:19
  • If you do have `created_at` in your `album_pic` table, make sure you define the `belongsToMany` relationship to include `->withTimestamps();` – damiani Oct 27 '14 at 20:21

3 Answers3

2

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).

Community
  • 1
  • 1
damiani
  • 7,071
  • 2
  • 23
  • 24
  • sounds good. I tried with this relationship, but it gives me all pics instead of the latest. does `latest()` work with `belongsToMany` ? ````public function pics() { return $this->belongsToMany('Pic')->latest(); }```` – haheute Oct 27 '14 at 13:40
  • Yes, with some caveats. First, you need to specify the field in `latest` as `album_pic.created_at`, since you are interested in the order of pics in the *pivot table*, not their order in the `pics` table. (Otherwise, you would get the most recent pic added overall, not the most recent pic added to a particular album.) Then, when you access the `latestPic` relation, you want to `->first()`, not `->get()`. (BTW, all `latest()` does is add an `orderBy('created_at')` clause to your query.) I've edited the answer with tested code that should work for you... – damiani Oct 27 '14 at 16:29
0

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'))
Bogdan
  • 43,166
  • 12
  • 128
  • 129
0

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;
Strawberry
  • 33,750
  • 13
  • 40
  • 57