Laravel eager loading with limit - php

I have two tables, say "users" and "users_actions", where "users_actions" has an hasMany relation with users:
users
id | name | surname | email...
actions
id | id_action | id_user | log | created_at
Model Users.php
class Users {
public function action()
{
return $this->hasMany('Action', 'user_id')->orderBy('created_at', 'desc');
}
}
Now, I want to retrieve a list of all users with their LAST action.
I saw that doing Users::with('action')->get();
can easily give me the last action by simply fetching only the first result of the relation:
foreach ($users as $user) {
echo $user->action[0]->description;
}
but I wanted to avoid this of course, and just pick ONLY THE LAST action for EACH user.
I tried using a constraint, like
Users::with(['action' => function ($query) {
$query->orderBy('created_at', 'desc')
->limit(1);
}])
->get();
but that gives me an incorrect result since Laravel executes this query:
SELECT * FROM users_actions WHERE user_id IN (1,2,3,4,5)
ORDER BY created_at
LIMIT 1
which is of course wrong. Is there any possibility to get this without executing a query for each record using Eloquent?
Am I making some obvious mistake I'm not seeing? I'm quite new to using Eloquent and sometimes relationship troubles me.
Edit:
A part from the representational purpose, I also need this feature for searching inside a relation, say for example I want to search users where LAST ACTION = 'something'
I tried using
$actions->whereHas('action', function($query) {
$query->where('id_action', 1);
});
but this gives me ALL the users which had had an action = 1, and since it's a log everyone passed that step.
Edit 2:
Thanks to #berkayk looks like I solved the first part of my problem, but still I can't search within the relation.
Actions::whereHas('latestAction', function($query) {
$query->where('id_action', 1);
});
still doesn't perform the right query, it generates something like:
select * from `users` where
(select count(*)
from `users_action`
where `users_action`.`user_id` = `users`.`id`
and `id_action` in ('1')
) >= 1
order by `created_at` desc
I need to get the record where the latest action is 1

I think the solution you are asking for is explained here http://softonsofa.com/tweaking-eloquent-relations-how-to-get-latest-related-model/
Define this relation in User model,
public function latestAction()
{
return $this->hasOne('Action')->latest();
}
And get the results with
User::with('latestAction')->get();

I created a package for this: https://github.com/staudenmeir/eloquent-eager-limit
Use the HasEagerLimit trait in both the parent and the related model.
class User extends Model {
use \Staudenmeir\EloquentEagerLimit\HasEagerLimit;
}
class Action extends Model {
use \Staudenmeir\EloquentEagerLimit\HasEagerLimit;
}
Then simply chain ->limit(1) call in your eager-load query (which seems you already do), and you will get the latest action per user.

My solution linked by #berbayk is cool if you want to easily get latest hasMany related model.
However, it couldn't solve the other part of what you're asking for, since querying this relation with where clause would result in pretty much the same what you already experienced - all rows would be returned, only latest wouldn't be latest in fact (but latest matching the where constraint).
So here you go:
the easy way - get all and filter collection:
User::has('actions')->with('latestAction')->get()->filter(function ($user) {
return $user->latestAction->id_action == 1;
});
or the hard way - do it in sql (assuming MySQL):
User::whereHas('actions', function ($q) {
// where id = (..subquery..)
$q->where('id', function ($q) {
$q->from('actions as sub')
->selectRaw('max(id)')
->whereRaw('actions.user_id = sub.user_id');
})->where('id_action', 1);
})->with('latestAction')->get();
Choose one of these solutions by comparing performance - the first will return all rows and filter possibly big collection.
The latter will run subquery (whereHas) with nested subquery (where('id', function () {..}), so both ways might be potentially slow on big table.

Let change a bit the #berkayk's code.
Define this relation in Users model,
public function latestAction()
{
return $this->hasOne('Action')->latest();
}
And
Users::with(['latestAction' => function ($query) {
$query->where('id_action', 1);
}])->get();

To load latest related data for each user you could get it using self join approach on actions table something like
select u.*, a.*
from users u
join actions a on u.id = a.user_id
left join actions a1 on a.user_id = a1.user_id
and a.created_at < a1.created_at
where a1.user_id is null
a.id_action = 1 // id_action filter on related latest record
To do it via query builder way you can write it as
DB::table('users as u')
->select('u.*', 'a.*')
->join('actions as a', 'u.id', '=', 'a.user_id')
->leftJoin('actions as a1', function ($join) {
$join->on('a.user_id', '=', 'a1.user_id')
->whereRaw(DB::raw('a.created_at < a1.created_at'));
})
->whereNull('a1.user_id')
->where('aid_action', 1) // id_action filter on related latest record
->get();
To eager to the latest relation for a user you can define it as a hasOne relation on your model like
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;
class User extends Model
{
public function latest_action()
{
return $this->hasOne(\App\Models\Action::class, 'user_id')
->leftJoin('actions as a1', function ($join) {
$join->on('actions.user_id', '=', 'a1.user_id')
->whereRaw(DB::raw('actions.created_at < a1.created_at'));
})->whereNull('a1.user_id')
->select('actions.*');
}
}
There is no need for dependent sub query just apply regular filter inside whereHas
User::with('latest_action')
->whereHas('latest_action', function ($query) {
$query->where('id_action', 1);
})
->get();
Migrating Raw SQL to Eloquent
Laravel Eloquent select all rows with max created_at
Laravel - Get the last entry of each UID type
Laravel Eloquent group by most recent record

Laravel Uses take() function not Limit
Try the below Code i hope it's working fine for u
Users::with(['action' => function ($query) {
$query->orderBy('created_at', 'desc')->take(1);
}])->get();
or simply add a take method to your relationship like below
return $this->hasMany('Action', 'user_id')->orderBy('created_at', 'desc')->take(1);

Related

Laravel query builder add complex query result

I have three models with the following hierarchy :
User
id
....some other properties
Journey
id
user_id
budget
....some other properties
Confirmation
id
journey_id
user_id
....some other properties
I have a HasMany from User to Journey, a HasMany from Journey to Confirmation.
I want to get the sum for a column of the journeys table by going through the confirmations table but I cannot create an intermediate HasManyThrough relation between User and Journey by using Confirmation.
I have tried to do
public function journeysMade(): HasManyThrough
{
return $this->hasManyThrough(Journey::class, Confirmation::class);
}
// And after,
User::with(...)->withSum('journeysMade','budget')
But it was not possible because the relations are not adapted.
With hindsight, the sql query I want to translate would look like
select coalesce(sum(journeys.budget), 0) as income
from journeys
inner join confirmations c on journeys.id = c.journey_id
where c.user_id = ? and c.status = 'finalized';
How can I implement this query considering how I will use my query builder :
$driversQueryBuilder = User::with(['profile', 'addresses']); // Here
$pageSize = $request->input('pageSize', self::DEFAULT_PAGE_SIZE);
$pageNumber = $request->input('pageNumber', self::DEFAULT_PAGE_NUMBER);
$driversPaginator = (new UserFilterService($driversQueryBuilder))
->withStatus(Profile::STATUS_DRIVER)
->withCountry($request->input('country'))
->withSex($request->input('sex'))
->withActive($request->has('active') ? $request->boolean('active') : null)
->get()
->paginate(perPage: $pageSize, page: $pageNumber);
return response()->json(['data' => $driversPaginator]);
The reason why I want to get a builder is because UserFilterService expects a Illuminate\Database\Eloquent\Builder.
Do you have any idea about how I can solve this problem ?
Not 100% sure what exactly you want to sum, but I think you need the following query
$user->whereHas('journeys', function($query) {
$query->whereHas('confirmations', function($subQuery) {
$subQuery->sum('budget);
}
});
If you the above query isn't summing the budget you need, you just add another layer of abstraction with whereHas methods to get exactly what you need. Hope this helps!
EDIT:
$user->whereHas('confirmations', function($q) {
$q->withSum('journeys', 'budget')->journeys_sum_budget;
}

Laravel scope with two relations

I have a main table, "help_pages" which stores id's of either articles or videos. There are two other tables: "articles" and "videos."
In the example below, $portalType is either 'reseller' or 'global' for the purposes of my application. All was well as long as I didn't have a flag on videos to be unpublished. Now I'm having trouble filtering the scope function by BOTH articles and videos that are published.
This is in my models/HelpPage.php:
public function scopeByPortalType($query, $portalType) {
return $query->where('portal_type', '=', $portalType)
->leftjoin('articles', 'content_id', '=', 'articles.id')
->where('articles.published', '=', '1');
}
what I wish I could do is just add a second
->leftJoin('videos', 'content_id', '=', videos.id')
->where('videos.published', '=', '0');
but this returns too many rows. I tried creating a temp table with a UNION for both articles and videos:
CREATE TEMPORARY TABLE IF NOT EXISTS tmp_assets AS
(SELECT id, 'Article' AS content_type FROM articles WHERE published = 1)
UNION
(SELECT id, 'Video' AS content_type FROM videos WHERE published = 1)
and then joining that, but no dice. This might be a smaller deal than I'm making it, but I'm lost now!
Laravel version 4.2
Well, it's not pefect, but it does what I need it to do:
public function scopeByPortalType($query, $portalType) {
$q = $query
->whereRaw("
(id IN (SELECT help_pages.id FROM help_pages INNER JOIN articles ON content_id=articles.id WHERE articles.published='1' AND content_type='Article')
OR
id IN (SELECT help_pages.id FROM help_pages INNER JOIN videos ON content_id=videos.id WHERE videos.published='1' AND content_type='Video'))")
->where('portal_type', '=', $portalType);
return $q;
}
I wish I could've figured out a way to do it using actual Eloquent, but every step led me down a rabbit hole with a dead end. This worked!
The Eloquent way to do this would be setting up the relationships so you could utilize eager loading.
Relationships: http://laravel.com/docs/4.2/eloquent#relationships
Eager Loading: http://laravel.com/docs/4.2/eloquent#eager-loading
Setup relations:
class HelpPage extends Eloquent {
public function articles()
{
return $this->hasMany('Article');
}
public function videos()
{
return $this->hasMany('Video');
}
}
Then you can utilize eager loading like this:
$help_pages = HelpPage::with(array('articles' => function($query)
{
$query->where('published', '=', 1);
},
'videos' => function($query)
{
$query->where('published', '=', 1);
}
))->get();
You might be able to then leverage whereHas and orWhereHas on the actual query rather than my example above.

Select a table based on select result from another table

I have a table Registrationrequests where I have course_id and user_id and some other field.
$users_id = Registrationrequest::where('course_id', $query_course_user)->where('registered', 1)->get();
From the above query it gives me an array of result. But I need to take the details of these user_id from another table Users. I'm using Laravel. Table models are Registrationrequest and User
How can I get the user details from the above select result? I'm not that good in Joins. Any advice?
Use Eloquent's whereHas method:
$courseId = Request::get('course_id');
$users = User::whereHas('registrationRequests', function($query) use ($courseId)
{
$query->where('course_id', $courseId)->where('registered', 1);
});
This assumes you have set up the proper relationship in your User model. If not, add this method to your user model:
public function registrationRequests()
{
return $this->hasMany('Registrationrequest');
}

Laravel Eloquent Join vs Inner Join?

So I am having some trouble figuring out how to do a feed style mysql call, and I don't know if its an eloquent issue or a mysql issue. I am sure it is possible in both and I am just in need of some help.
So I have a user and they go to their feed page, on this page it shows stuff from their friends (friends votes, friends comments, friends status updates). So say I have tom, tim and taylor as my friends and I need to get all of their votes, comments, status updates. How do I go about this? I have a list of all the friends by Id number, and I have tables for each of the events (votes, comments, status updates) that have the Id stored in it to link back to the user. So how can I get all of that information at once so that I can display it in a feed in the form of.
Tim commented "Cool"
Taylor Said "Woot first status update~!"
Taylor Voted on "Best competition ever"
Edit #damiani
So after doing the model changes I have code like this, and it does return the correct rows
$friends_votes = $user->friends()->join('votes', 'votes.userId', '=', 'friend.friendId')->orderBy('created_at', 'DESC')->get(['votes.*']);
$friends_comments = $user->friends()->join('comments', 'comments.userId', '=', 'friend.friendId')->orderBy('created_at', 'DESC')->get(['comments.*']);
$friends_status = $user->friends()->join('status', 'status.userId', '=', 'friend.friendId')->orderBy('created_at', 'DESC')->get(['status.*']);
But I would like them all to happen at once, this is because mysql sorting thousands of records in order is 100x faster then php taking 3 lists, merging them and then doing it. Any ideas?
I'm sure there are other ways to accomplish this, but one solution would be to use join through the Query Builder.
If you have tables set up something like this:
users
id
...
friends
id
user_id
friend_id
...
votes, comments and status_updates (3 tables)
id
user_id
....
In your User model:
class User extends Eloquent {
public function friends()
{
return $this->hasMany('Friend');
}
}
In your Friend model:
class Friend extends Eloquent {
public function user()
{
return $this->belongsTo('User');
}
}
Then, to gather all the votes for the friends of the user with the id of 1, you could run this query:
$user = User::find(1);
$friends_votes = $user->friends()
->with('user') // bring along details of the friend
->join('votes', 'votes.user_id', '=', 'friends.friend_id')
->get(['votes.*']); // exclude extra details from friends table
Run the same join for the comments and status_updates tables. If you would like votes, comments, and status_updates to be in one chronological list, you can merge the resulting three collections into one and then sort the merged collection.
Edit
To get votes, comments, and status updates in one query, you could build up each query and then union the results. Unfortunately, this doesn't seem to work if we use the Eloquent hasMany relationship (see comments for this question for a discussion of that problem) so we have to modify to queries to use where instead:
$friends_votes =
DB::table('friends')->where('friends.user_id','1')
->join('votes', 'votes.user_id', '=', 'friends.friend_id');
$friends_comments =
DB::table('friends')->where('friends.user_id','1')
->join('comments', 'comments.user_id', '=', 'friends.friend_id');
$friends_status_updates =
DB::table('status_updates')->where('status_updates.user_id','1')
->join('friends', 'status_updates.user_id', '=', 'friends.friend_id');
$friends_events =
$friends_votes
->union($friends_comments)
->union($friends_status_updates)
->get();
At this point, though, our query is getting a bit hairy, so a polymorphic relationship with and an extra table (like DefiniteIntegral suggests below) might be a better idea.
Probably not what you want to hear, but a "feeds" table would be a great middleman for this sort of transaction, giving you a denormalized way of pivoting to all these data with a polymorphic relationship.
You could build it like this:
<?php
Schema::create('feeds', function($table) {
$table->increments('id');
$table->timestamps();
$table->unsignedInteger('user_id');
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
$table->morphs('target');
});
Build the feed model like so:
<?php
class Feed extends Eloquent
{
protected $fillable = ['user_id', 'target_type', 'target_id'];
public function user()
{
return $this->belongsTo('User');
}
public function target()
{
return $this->morphTo();
}
}
Then keep it up to date with something like:
<?php
Vote::created(function(Vote $vote) {
$target_type = 'Vote';
$target_id = $vote->id;
$user_id = $vote->user_id;
Feed::create(compact('target_type', 'target_id', 'user_id'));
});
You could make the above much more generic/robust—this is just for demonstration purposes.
At this point, your feed items are really easy to retrieve all at once:
<?php
Feed::whereIn('user_id', $my_friend_ids)
->with('user', 'target')
->orderBy('created_at', 'desc')
->get();

Counting related rows in a child table

I have been trying to do some queries and getting a count on related tables using eloquent.
Tables:
requests
contact (belongs to requests)
history (belongs to contact)
As such X number of requests each have Y number of contacts which in term each have Z number of histories
Using sql I can do something like this to get all the counts.
SELECT
id,
(
SELECT count(contact.id)
FROM contact
WHERE contact.requests_id = requests.id
) AS n_contact,
(
SELECT count(history.id)
FROM contact INNER JOIN history ON (history.contact_id = contact.id)
WHERE contact.requests_id = requests.id
) AS n_history
FROM requests;
But I am a bit lost when using eloquent to build queries. If for instance I was selecting all contacts for a given request at what point would I join/count the history? Or do I need to add in some accessor's into the relevant Models for these 3 tables?
public function getAllContacts($id) {
return Requests::where('requests.id', '=', $id)
->join('requests', 'contact.requests_id', '=', 'requests.id')
->select('contact.*', 'requests.name');
->get();
}
Thanks in advance for any help.
You can use helper relation for this, if you'd like to use Eloquent instead of manual joins:
// Request model
public function contactsCount()
{
return $this->hasOne('Contact')->selectRaw('request_id, count(*) as aggregate')->groupBy('request_id');
}
public function getContactsCountAttribute()
{
if ( ! array_key_exists('contactsCount', $this->relations)) $this->load('contactsCount');
return $this->getRelation('contactsCount')->aggregate;
}
The same would go for Contact model towards History model.
For counting far relation (Request -> History) you can use hasManyThrough relation with a little adjustment.
This way you can eager load those aggregates for multiple models without n+1 issue, nice and easy:
$requests = Request::with('contactsCount', 'contacts.historyCount')->get();
// 1 query for reuqests, 1 query for contacts count and 2 queries for contacts.historyCount
// example output
$requests->first()->contactsCount; // 17
$requests->first()->contacts->first()->historyCount; // 5
/* Make Relation in the Request Model */
public function contacts()
{
return $this->hasMany('App\Model\Contact', 'request_id', 'id');
}
/* use withCount() to get the total numner of contacts */
public function getAllContacts($id) {
return Requests::with('contacts')
->withCount('contacts')
->find($id);
}

Categories