laravel 4 - join latest result in group by query - mysql

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.

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

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

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;

Related

How to access columns of subqueries with jooq?

i am having troubles understanding how to access columns from a subquery (MySQL). Here is my code:
Personne personne = Personne.PERSONNE.as("personne");
Evenement evenement = Evenement.EVENEMENT.as("evenement");
Genealogie genealogie = Genealogie.GENEALOGIE.as("genealogie");
Lieu lieu = Lieu.LIEU.as("lieu");
SelectField<?>[] select = { DSL.countDistinct(personne.ID).as("countRs"), lieu.LIBELLE.as("libelleRs"),
lieu.ID.as("idVille") };
Table<?> fromPersonne = evenement.innerJoin(personne).on(personne.ID.eq(evenement.IDPERS))
.innerJoin(genealogie).on(genealogie.ID.eq(personne.IDGEN)).innerJoin(lieu)
.on(lieu.ID.eq(evenement.IDLIEU));
Table<?> fromFamille = evenement.innerJoin(personne).on(personne.IDFAM.eq(evenement.IDFAM))
.innerJoin(genealogie).on(genealogie.ID.eq(personne.IDGEN)).innerJoin(lieu)
.on(lieu.ID.eq(evenement.IDLIEU));
GroupField[] groupBy = { lieu.ID };
Condition condition = //conditionally build, not relevant i think
result = create.select(DSL.asterisk())
.from(create.select(select).from(fromPersonne).where(condition).groupBy(groupBy)
.union(create.select(select).from(fromFamille).where(condition).groupBy(groupBy)))
// i would like something like this but i don't know how: .groupBy(groupBy).fetch();
Basicly what i have is:
SELECT
*
FROM(
(SELECT
countRs, libelleRs, idVille
FROM
fromPersonne
WHERE
-- conditions
GROUP BY lieu.ID)
UNION
(SELECT
countRs, libelleRs, idVille
FROM
fromFamille
WHERE
-- conditions
GROUP BY lieu.ID)
)GROUP BY lieu.ID -- this is where i need help
In a plain MySQL query i would just give an alias to the union and then make a reference to the column i want to group by using the alias but it seems like it does not work like this with JOOQ.
I just need to group the results of the subqueries together but i don't know how to make a reference to the subqueries columns... I am sure i would have to reference my subqueries in objects outside of that "main select" to be able to access the columns or something along those lines but i am lost in all the object types.
You have to assign your derived table to a local variable and dereference columns from it, e.g.
Table<?> t = table(
select(...).from(...).groupBy(...).unionAll(select(...).from(...).groupBy(...))
).as("t");
Field<Integer> tId = t.field(lieu.ID);

Convert Raw SQL Query to Laravel Eloquent left outer join

can anyone help me turn the query in to Eloquent?
Model Orders::
Model TaskCard::
select
ifnull(data, o.created_at) as dataZlecenia,o.id, o.nazwa
from
orders o
left outer join
(select max(created_at) as data,order_id from task_cards
group by order_id) as a
on a.order_id=o.id
order by dataZlecenia desc
Seems like you have wrong namespace as well, try using App\ModelName instead of App\Models\ModelName. And use english for naming functions/classes :)
Check if your foreign keys are used correctly. If you want it to work just out of the box, orders table should have client_id as foreign key and id as primary key, while clients table has to have primary key id. Then this solution will work:
public function order(){
return $this->hasOne('App\Order');
}
via https://laravel.com/docs/5.8/eloquent-relationships
EDIT:
for better understanding :) in create_orders_table migration:
$table->bigIncrements('id');
$table->unsignedBigInteger('client_id');
$table->foreign('client_id')->references('id')->on('clients')->onDelete('cascade');
Moreover I can't see a reason why your client can have just one order. I'd use in Client model:
public function orders(){
return $this->hasMany('App\Order');
}
And in Order model:
public function client(){
return $this->belongsTo('App\Client');
}
This way your client will be able to have multiple orders and an order will have just one assigned client.

Laravel list music genres with sub model popularity

I have 4 tables:
MusicGenre
Artist
Song
SongInfo
weekly_hit is a column on the SongInfo table.
MusicGenre is connected to Artist like this:
public function artists()
{
return $this->hasMany('App\Models\Artist\ArtistInfo','genre_id','id');
}
Artist is connected to Song like this:
public function songs()
{
return $this->hasMany('App\Models\Song\Songs','artist_id');
}
And Song is connected to SongInfo like this:
public function info()
{
return $this->hasOne('App\Models\Song\SongsInfo','song_id','id');
}
There is a no problem querying the table.
My problem is that I want to get the best music genres using weekly_hit in SongInfo table.
Edit:
I resolved that problem with raw code
"select music_genres.*,
sum(distinct song_info.weekly_hit) as song_popularity
from `music_genres`
left join `artist_info` on
`music_genres`.`id` = `artist_info`.`genre_id`
left join songs on
artist_info.artist_id = songs.artist_id
left join songs_info on
songs.id = songs_info.song_id
group by music_genres.name
order by song_popularity DESC
limit 5
But, I can't get songs. I want to get 5 song ordered by weekly_hit in songs_info table from all returned music genres.
Guys i still searching a solution?
Can somebody help me?
You'll want to use Eloquent's hasManyThrough() relation to reach that deep in the relationship chain.

laravel 4 - how to Limit (Take and Skip) for Eloquent ORM?

TL;DR
Can you limit an Eloquent ORM query like using take() and skip() so that the resulting mysql query is also limited, and it doesn't have to return the entire dataset?
If so, how would you modify:
$test = User::find(1)->games->toArray();
To include limit 3 offset 2?
Tables:
users games userGames
-- id -- id -- user_id
-- name -- name -- game_id
-- steam_id
Models:
class User extends Eloquent {
public function games() {
return $this->belongsToMany('Game', 'userGames', 'user_id', 'game_id');
}
}
class Game extends Eloquent {
public function users() {
return $this->belongsToMany('User', 'userGames', 'user_id', 'game_id');
}
}
Limit in Query Builder
Using the regular Laravel Query Builder I can get all games that belong to user of id 1, and limit the result with take() and skip():
$test = DB::table('games')
->join('userGames', 'userGames.game_id', '=', 'games.id')
->where('userGames.user_id', '=', '1')->take(3)->skip(2)->get();
By listening to the illuminate.query event I can see that the query generated by this is:
select * from `games`
inner join `userGames`
on `userGames`.`game_id` = `games`.`id`
where `userGames`.`user_id` = ?
limit 3 offset 2
Limit in Eloquent ORM
When I try to recreate the same query with Eloquent:
$test = User::find(1)->games->take(2)->toArray();
I'm able to use take but adding skip causes an error. Also the resulting query does not actually contain the limit:
select `games`.*, `userGames`.`user_id` as `pivot_user_id`,
`userGames`.`game_id` as `pivot_game_id` from `games`
inner join `userGames`
on `games`.`id` = `userGames`.`game_id`
where `userGames`.`user_id` = ?
So it seems that the entire result is being queried first, which is not ideal when dealing with large data sets.
Question:
Is it possible to limit an Eloquent ORM query so that at the MYSQL Query level it also limits the result, equivalent to limit 3 offset 2?
User::find(1)->games()->take(3)->skip(2)->get();
I think this should give you your collection. :)
->games will give you a collection, where ->games() will offer a query builder instance.
Enjoy Laravel!

Is a for...each loop the best way to extract a List of primitives from an IQueryable of custom objects?

For example if I had an IQueryable of Person objects and each Person had an Age ID and I wanted to make another query that selected all rows from a table that contains an integer ID that was in this IQueryable of IDs...
Could I use something like this?
List<int> AgeList = new List<int>();
foreach(Person p in Persons)
{
AgeList.Add(p.Age);
}
var ageStats = from a in db.Ages
where AgeList.Contains(a.Age)
select a;
Or what would be a better way so I didn't need to loop?
Thanks
It sounds like you want to perform a join. Since you mentioned that Persons is an IQueryable, is it coming from the database as well?
Either a join or SelectMany (which translates to a join in LINQ to SQL) should suffice.
Join:
var ageStats = from a in db.Ages
join p in db.Persons on a.Age equals p.Age
select a;
SelectMany:
var ageStats = from a in db.Ages
from p in db.Persons
where a.Age == p.Age
select a;
You can do
List<int> AgeList = Persons.Select(p => p.Age);
However, even tho the msdn page for .Select does not say much, I'm pretty sure .Select still is a loop, so you won't really gain any performance by writing it like that
var ageStats = from a in db.Ages
where Persons.Select(p => p.Age).Contains(a.Age)
select a;