I'm trying to use a pivot variable of a parent relationship inside the eloquent withCount() method.
Background:
There is a application with a ManyToMany relationship between Users and Clusters. Users can send messages within a cluster. To keep track of the unread message count for a user in a specific cluster i keep track of the last read message id in the join table, like so:
table: cluster_user
cluster_id | user_id | last_read_message_id
-------------------------------------------
1 | 59 | 3
2 | 62 | 8
The User() model has a belongsToMany() relation with the Cluster() model
The Cluster() model has a belongsToMany() relation with the User() model
The Cluster() model has a hasMany() relation with the Messages() model
The Message() model has a belongsTo() relation with the Cluster() model
Now I would like to list all the clusters of the authenticated user including a unread message count.
Currently I'm stuck on this:
$clusters = Auth::user()->clusters()->withCount(['messages' => function ($query) {
$query->where('messages.id', '>', '???');
}])->get();
I've already tried:
$clusters = Auth::user()->clusters()->withCount(['messages' => function ($query) {
$query->where('messages.id', '>', 'cluster_user.last_read_message_id');
}])->get();
But this gives me a total count of all the messages in stead of the ones with an id higher than x.
If I hardcode an id, like this:
$clusters = Auth::user()->clusters()->withCount(['messages' => function ($query) {
$query->where('messages.id', '>', '3');
}])->get();
Then I get the correct unread message count.
So can somebody tell me how to use the pivot variable 'last_read_message_id' of the user()->cluster() relationship inside the withCount() callback function with the following in mind:
I'ts crucial to use as little queries as possible.
The unread message count must be a part of the cluster() collection because I'm returning a ClusterResource later on, like so:
return ClusterResource::collection($clusters);
which includes the unread message count.
class ClusterResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
return [
'name' => $this->name,
'unread_messages_count' => $this->whenPivotLoaded('cluster_user', $this->messages_count)
];
}
}
Thnx!
Found the answer due to a comment of #cbaconnier.
Placing DB::raw('cluster_user.last_read_message_id') on the spot is working. I't not neat, but it works.
Full example:
$clusters = Auth::user()
->clusters()
->withCount(['messages' => function ($query) {
$query->where('messages.id', '>', DB::raw('cluster_user.last_read_message_id'));
}])
->get();
Good question! I think you should be able to do use the withPivot method on your relationship and then use the pivot attribute in your query.
For example, in your Cluster model where you define the cluster_user relationship, do:
function cluster_user() {
return $this->belongsToMany('App\User')
->withPivot('last_read_message_id');
}
And then in your query you could use whereColumn to compare the columns. Something like this:
$clusters = Auth::user()
->clusters()
->withCount(['messages' => function ($query) {
$query->whereColumn('messages.id', '>',
'pivot.last_read_message_id');
}])
->get();
Search for "Retrieving Intermediate Table Columns" on the Eloquent relationships documentation for more information.
Related
Good day. I have this relationship problem in laravel
I have three models
User
State
School
I have established the following relationships between them
a. School - User (A user can enroll in many schools)
In User Model
public function schools(){
return $this->belongsToMany('App\Models\School', 'school_user', 'user_id', 'school_id');
}
b. A school can have many users
In School Model
public function users(){
return $this->belongsToMany('App\Models\User', 'school_user', 'school_id', 'user_id');
}
c. A school belongs to a state (i.e c an be found in a state)
In School Model
public function states(){
return $this->belongsTo('App\Models\State');
}
d. A state has many schools
In State Model
public function schools(){
return $this->hasMany('App\Models\School');
}
Now, I know how to query models with immediate relationship. For example, I can get the users associated with a school and vice-versa. I can also get the schools in a state.
My Question
How do I get all users associated with schools in a given state (using a query)? Assuming all we have is just the name of a state.
And probably get all schools the users are associated with and the date(time) in which there were associated with the schools?
This is what I have done. The second query is not giving me an answer
public function details(){
//Get all schools associated with a state
$schools = State::with('schools')->where('id',1)->first();
foreach($schools->schools as $data){
//Get all users associated with the schools
$users = School::with('users')->where('id',$data->id);
dd($users);
}
}
Apart from the fact that this approach is not probably right, I am not getting any answer. Is there a query or method that can solve this?
Thanks.
You should see the related models in the relations array attribute
$school= App\Models\School::with('User', 'State')->get();
Using a whereHas you can get all users that belong to schools with the given state_id.
$state = State::find(1);
$users = User::whereHas('schools', function ($query) use ($state) {
$query->where('state_id', $state->id);
})->get();
Querying Relationship Existence
If you need even more power, you may use the whereHas and orWhereHas
methods to define additional query constraints on your has queries,
such as inspecting the content of a comment:
use Illuminate\Database\Eloquent\Builder;
// Retrieve posts with at least one comment containing words like code%...
$posts = Post::whereHas('comments', function (Builder $query) {
$query->where('content', 'like', 'code%');
})->get();
// Retrieve posts with at least ten comments containing words like code%...
$posts = Post::whereHas('comments', function (Builder $query) {
$query->where('content', 'like', 'code%');
}, '>=', 10)->get();
I have two models: Game and Game_Assignment. Game_Assignment tells whose job it is to play a game.
I am trying to count the number of Game_Assignment's that a user has their id on that also have a specific value on the Game model that it relates to. I'll just get into the Models/the code
Game Model Relationship:
public function assignments() {
return $this->hasMany('App\Models\Game_Assignment', 'game_id');
}
Game_Assignment Relationship:
public function game() {
return $this->belongsTo('App\Models\Game', 'game_id');
}
Where things are going wrong (in a queue job, if that makes a difference)
$gamesDue = Game_Assignment::where('statistician_id', $statistician->id)->game->where('stats_done', '!=', 'yes')->count();
I have also tried the following two things, neither worked:
$gamesDue = Game_Assignment::where('statistician_id', $statistician->id)->game()->where('stats_done', '!=', 'yes')->count();
and...
$gamesDue = Game_Assignment::where('statistician_id', $defaultStatistician->id)->with(['games' => function($query) {
$query->where('stats_done', '!=', 'yes');
}])->count();
None of these work, and the first one I showed threw an error:
Property [game] does not exist on the Eloquent builder instance.
Anyone have an idea of where I am going wrong? I am using this link as my reference https://laravel.com/docs/5.8/eloquent-relationships#eager-loading
When using the query builder of your Game_Assignment model, you cannot simply switch context to the query builder of Game. You can only call ->game() or ->game after you retrieved one or many model instances of Game_Assignment with first() or get().
So, in your particular case, you were looking for whereHas('game', $callback) (where $callback is a function that applies constraints on the foreign table) in order to add a constraint on the foreign table:
use Illuminate\Database\Eloquent\Builder;
$gamesDue = Game_Assignment::query()
->where('statistician_id', $statistician->id)
->whereHas('game', function (Builder $query) {
$query->where('stats_done', '!=', 'yes');
})
->count();
Side note: a column (stats_done) that seems to hold a boolean value (yes/no) should be of boolean type and not string/varchar.
Maybe dublicate, but my search hasn't any success. What we have:
Laravel 5.2, three tables
user_tariff_hours_history
id
user_tariff_id
...
user_tariff
id
user_id
...
users
id
...
I want to get user through user_tariff for current user_tariff_hours_history
As official Laravel Docs said I need to have getter with the hasManyThrough method in my user_tariff_hours_history's model https://laravel.com/docs/5.5/eloquent-relationships#has-many-through
But result of the query with that getter didn't expected.
I get whole users table, not a user for current user_tariff_hours_history
getter:
public function getUsers() {
return $this->hasManyThrough('App\SARegistration',
'App\SAUser_tariff',
'user_id', //FK on SARegistration (users)
'id', //Local Key of ?
'user_tariff_id'); //FK on SAUser_tariff (user_tariffs)
}
Note: in docs there are 6 params for the hasManyThrough, but Laravel said it has only 5 params, than I was trying to use diffirent combo of params
Define hasManyThrough relationship called userTariffHoursHistory in User model and use whereHas():
User::whereHas('userTariffHoursHistory', function($q) use($userTariffHoursHistoryId) {
$q->where('id', $userTariffHoursHistoryId);
})->first();
Or define two hasMany() relationships (userTariffs in User model and userTariffHoursHistory in UserTariff model) and then use nested whereHas():
User::whereHas('userTariffs', function($q) use($userTariffHoursHistoryId) {
$q->whereHas('userTariffHoursHistory', function($q) use($userTariffHoursHistoryId) {
$q->where('id', $userTariffHoursHistoryId);
});
})->first();
In my Database, I have:
tops Table
posts Table
tops_has_posts Table.
When I retrieve a top on my tops table I also retrieve the posts in relation with the top.
But what if I want to retrieve these posts in a certain order ?
So I add a range field in my pivot table tops_has_posts and I my trying to order by the result using Eloquent but it doesn't work.
I try this :
$top->articles()->whereHas('articles', function($q) {
$q->orderBy('range', 'ASC');
})->get()->toArray();
And this :
$top->articles()->orderBy('range', 'ASC')->get()->toArray();
Both were desperate attempts.
Thank you in advance.
There are 2 ways - one with specifying the table.field, other using Eloquent alias pivot_field if you use withPivot('field'):
// if you use withPivot
public function articles()
{
return $this->belongsToMany('Article', 'tops_has_posts')->withPivot('range');
}
// then: (with not whereHas)
$top = Top::with(['articles' => function ($q) {
$q->orderBy('pivot_range', 'asc');
}])->first(); // or get() or whatever
This will work, because Eloquent aliases all fields provided in withPivot as pivot_field_name.
Now, generic solution:
$top = Top::with(['articles' => function ($q) {
$q->orderBy('tops_has_posts.range', 'asc');
}])->first(); // or get() or whatever
// or:
$top = Top::first();
$articles = $top->articles()->orderBy('tops_has_posts.range', 'asc')->get();
This will order the related query.
Note: Don't make your life hard with naming things this way. posts are not necessarily articles, I would use either one or the other name, unless there is really need for this.
For Laravel 8.17.2+ you can use ::orderByPivot().
https://github.com/laravel/framework/releases/tag/v8.17.2
In Laravel 5.6+ (not sure about older versions) it's convenient to use this:
public function articles()
{
return $this->belongsToMany('Article', 'tops_has_posts')->withPivot('range')->orderBy('tops_has_posts.range');
}
In this case, whenever you will call articles, they will be sorted automaticaly by range property.
In Laravel 5.4 I have the following relation that works fine in Set model which belongsToMany of Job model:
public function jobs()
{
return $this->belongsToMany(Job::class, 'eqtype_jobs')
->withPivot(['created_at','updated_at','id'])
->orderBy('pivot_created_at','desc');
}
The above relation returns all jobs that the specified Set has been joined ordered by the pivot table's (eqtype_jobs) field created_at DESC.
The SQL printout of $set->jobs()->paginate(20) Looks like the following:
select
`jobs`.*, `eqtype_jobs`.`set_id` as `pivot_set_id`,
`eqtype_jobs`.`job_id` as `pivot_job_id`,
`eqtype_jobs`.`created_at` as `pivot_created_at`,
`eqtype_jobs`.`updated_at` as `pivot_updated_at`,
`eqtype_jobs`.`id` as `pivot_id`
from `jobs`
inner join `eqtype_jobs` on `jobs`.`id` = `eqtype_jobs`.`job_id`
where `eqtype_jobs`.`set_id` = 56
order by `pivot_created_at` desc
limit 20
offset 0
in your blade try this:
$top->articles()->orderBy('pivot_range','asc')->get();
If you print out the SQL query of belongsToMany relationship, you will find that the column names of pivot tables are using the pivot_ prefix as a new alias.
For example, created_at, updated_at in pivot table have got pivot_created_at, pivot_updated_at aliases. So the orderBy method should use these aliases instead.
Here is an example of how you can do that.
class User {
...
public function posts(): BelongsToMany {
return $this->belongsToMany(
Post::class,
'post_user',
'user_id',
'post_id')
->withTimestamps()
->latest('pivot_created_at');
}
...
}
You can use orderBy instead of using latest method if you prefer. In the above example, post_user is pivot table, and you can see that the column name for ordering is now pivot_created_at or pivot_updated_at.
you can use this:
public function keywords() {
return $this->morphToMany(\App\Models\Keyword::class, "keywordable")->withPivot('order');
}
public function getKeywordOrderAttribute() {
return $this->keywords()->first()->pivot->order;
}
and append keyword attribiute to model after geting and use sortby
$courses->get()->append('keyword_order')->sortBy('keyword_order');
In the documentation of Eloquent it is said that I can pass the keys of a desired relationship to hasManyThrough.
Lets say I have Models named Country, User, Post. A Country model might have many Posts through a Users model. That said I simply could call:
$this->hasManyThrough('Post', 'User', 'country_id', 'user_id');
This is fine so far! But how can I get these posts only for the user with the id of 3 ?
Can anybody help here?
So here it goes:
models: Country has many User has many Post
This allows us to use hasManyThrough like in your question:
// Country model
public function posts()
{
return $this->hasManyThrough('Post', 'User', 'country_id', 'user_id');
}
You want to get posts of a given user for this relation, so:
$country = Country::first();
$country->load(['posts' => function ($q) {
$q->where('user_id', '=', 3);
}]);
// or
$country->load(['posts' => function ($q) {
$q->has('user', function ($q) {
$q->where('users.id', '=', 3);
});
})
$country->posts; // collection of posts related to user with id 3
BUT it will be easier, more readable and more eloquent if you use this instead:
(since it has nothing to do with country when you are looking for the posts of user with id 3)
// User model
public function posts()
{
return $this->hasMany('Post');
}
// then
$user = User::find(3);
// lazy load
$user->load('posts');
// or use dynamic property
$user->posts; // it will load the posts automatically
// or eager load
$user = User::with('posts')->find(3);
$user->posts; // collection of posts for given user
To sum up: hasManyThrough is a way to get nested relation directly, ie. all the posts for given country, but rather not to search for specific through model.
$user_id = 3;
$country = Country::find($country_id);
$country->posts()->where('users.id', '=', $user_id)->get();
$this->hasManyThrough('Post', 'User', 'country_id', 'user_id')->where(column,x);
What happen here is you get the collection in return you can put any condition you want at the end.