Alex
Alex

Reputation: 4669

SQL Subquery JOIN issue with Laravel Query Builder

I'm stuck at understanding something melted between Laravel Query Builder and pure SQL I guess.

I have 2 tables :

ex:

user

user_id
1

user_action

user_id    datetime              action_type
1          2017-12-01 12:10:00   y
1          2017-12-01 12:00:00   x

My need :

Now I want to retrieve a user list, with the newest action for each user. So I need to get only 1 row in the JOIN from user to user_action, and this row have to be the one with newest datetime. So in my ex; I would like to get only the record with the datetime 2017-12-01 12:10:00 with a LEFT JOIN (to retrieve user even if there's no action).

I tried the following :

$userActionSubquery = DB::table('user_action')
        ->select()
        ->orderBy('datetime', 'DESC')
        ->limit(1);
$query->leftJoinSub($userActionSubquery, 'user_action', function ($join) {
         $join->on('user_action.user_id', '=', 'user.user_id');
})->groupBy('user_id');

By doing so, I'm not getting anything from user_action table, BUT it works if the action of the given user is the newest of the table ! I thought the $join->on would filter user id but the JOIN subquery is ran without this filter, and I can't but a WHERE in subquery because I don't have a user_id to give ! it's a query to get a list of user !

PS : the query generated is

SELECT user.user_id FROM user left join (SELECT * FROM user_action ORDER BY datetime DESC LIMIT 1) as user_action on user_action.user_id = user.user_id WHERE user.tenant_id in ('7') GROUP BY `user_id`

I'm missing something to succeed, thanks for your help!

Upvotes: 0

Views: 160

Answers (1)

CloudyCity
CloudyCity

Reputation: 113

Try this:

$sub = \DB::table('user_action')
    ->select([
        'user_id',
        \DB::raw('SUBSTRING_INDEX( GROUP_CONCAT( action_type ORDER BY datetime DESC ), ",", 1 ) AS last_action_type')
    ])
    ->groupBy('user_id');

$data = \DB::table('user')
    ->leftJoinSub($sub, 'tmp', function ($join) {
        $join->on('tmp.user_id', '=', 'user.id');
    })->get();

sql:

SELECT * FROM user LEFT JOIN ( SELECT user_id, SUBSTRING_INDEX( GROUP_CONCAT( action_type ORDER BY datetime DESC ), ',', 1 ) AS last_action_type FROM user_action GROUP BY user_id ) AS tmp ON tmp.user_id = user.id

It works in my test env:

user

id   name
1    john
2    jack

user_action

id   user_id   datetime              action_type
1    1         2020-09-24 23:51:00   login
2    1         2020-09-24 23:51:20   search
3    2         2020-09-24 23:30:00   login
4    2         2020-09-25 00:00:00   coding

result

Illuminate\Support\Collection Object
(
    [items:protected] => Array
        (
            [0] => stdClass Object
                (
                    [id] => 1
                    [name] => john
                    [user_id] => 1
                    [last_action_type] => search
                )

            [1] => stdClass Object
                (
                    [id] => 2
                    [name] => jack
                    [user_id] => 2
                    [last_action_type] => coding
                )

        )

)

Upvotes: 1

Related Questions