Eloquent - where has pivot users orWhere has none - php

I'm trying to write a function that will get all "buckets" that are assigned to the auth'd user and/or buckets that have NO USERS assigned.
Relations and such, work as they should. Unless I'm missing something?
How can I get all buckets user is assigned too - and also include buckets where no users (including the auth user) are assigned.
Buckets user is assigned to
Buckets where NO users have been assigned. i.e. pivot table contains no rows for bucket, etc.
My issue very likely stems from the orWhere query...
$buckets = Team::currentTeam()->buckets()->with('user')->whereHas('user', function($query) {
$query->where('user_id', Auth::user()->id)
->orWhere('user_id', function() {
$query->count();
}, '<', 0);
})->get();

Didn't tested this but I think this should work. You're looking to remove that orWhere query and add orHas('user', '=', 0).
$buckets = Team::currentTeam()->buckets()->with('user')->whereHas('user', function($query) {
$query->where('user_id', Auth::user()->id);
})->orHas('user', '=', 0)->get();

Another possible solution you might consider is using a left join.
Heads-up: this might not be accurate as I don't know your db schema.
Team::currentTeam()
->buckets()
->leftJoin('users', 'users.bucket_id', '=', 'buckets.id')
->where(function($query) {
$query->where('users.id', $user_id)
->orWhereNull('user.id');
});
Please also check this blog post
AND-OR-AND + brackets with Eloquent
https://laraveldaily.com/and-or-and-brackets-with-eloquent/

Related

Laravel how to get only one record per user

In my laravel-application, I want to display all users/candidates, which have taken an education. Now it might happen, that a user/candidate has taken more than one education, so in this case, just the latest education should be displayed.
I've come to this:
$users = User::whereHas('roles', function ($q) {
$q->where('slug', 'candidate');
}
)->whereHas('educations')
->join('user_educations', 'user_educations.user_id', '=', 'users.id')
->join('educations', 'user_educations.education_id', '=', 'educations.id')
->join('education_levels', 'user_educations.education_level_id', '=', 'education_levels.id')
->select('users.id', 'users.name', 'users.surname', 'users.status', 'users.city', 'users.zipcode', 'users.birthday', 'users.avatar', 'educations.title as education', 'education_levels.title as education_level')
->where('user_educations.user_id', '=', 'users.id') // HERE IT FAILS - returns null
->first();
return response(['success' => true, "users" => $users], 200);
when I leave out the where('user_educations.user_id', '=', 'users.id')-clause, and do get() instead of first(), I get all users/candidates with educations, and also sometimes the same user multiple times, depending on how many educations he has taken.
how can I fix this?
The distinct method allows you to force the query to return distinct results, you can use it like this;
$users = DB::table('users')->groupBy('user_id')->distinct()->get();
If you want to take just the latest record, you should use "orderBy" to order your records, than pick a record with something like this :
$users = DB::table('users')->groupBy('user_id')->orderBy('created_at','desc')->distinct()->get();
You can look here for more detailed information about query builders in Laravel

How to compare related count with own column in Laravel Eloquent?

Assume we have an agents table with a quota column and a many-to-many relationship to tickets. With Laravel Eloquent ORM, how can I select only agents having less or equal number of 'tickets' than their 'quota'?
Eager-loading objects must be avoided.
class Agent extends Model {
public function tickets()
{
return $this->belongsToMany(Ticket::class, 'agent_tickets')->using(AgentTicket::class);
}
public function scopeQuotaReached($query)
{
// Does not work. withCount is an aggregate.
return $query->withCount('tickets')->where('tickets_count', '<=', 'quota');
// Does not work. Tries to compare against the string "quota".
return $query->has('tickets', '<=', 'quota');
}
}
Is there a more eloquent (pun intended) way to solve this than using a DB::raw() query with joining and grouping and counting manually?
EDIT
Works:
$query->withCount('tickets')->having('tickets_count', '<=', DB::raw('quota'))->get();
Works:
$query->withCount('tickets')->having('tickets_count', '<=', DB::raw('quota'))->exists();
Breaks: (throws)
$query->withCount('tickets')->having('tickets_count', '<=', DB::raw('quota'))->count();
RELATED
https://github.com/laravel/framework/issues/14492
Issue is closed, links to #9307, I have posted there. Will follow up.
Derived columns like tickets_count can only be accessed in the HAVING clause.
Since there is no havingColumn() method, you'll have to use a raw expression:
$query->withCount('tickets')->having('tickets_count', '<=', DB::raw('quota'));
At a database level I don't know how to achieve this, but you could do it at a Collection level.
// Get users
$agents = Agent::withCount('tickets')->get();
// filter
$good_agents = $agents->filter(function ($agent, $key) {
return $agent->tickets_count >= $agent->quota;
})
->all();
Of course you can inline it:
$good_agents = Agent
::withCount('tickets')
->get()
->filter(function ($agent, $key) {
return $agent->tickets_count >= $agent->quota;
})
->all();

Laravel 5.3 Constraining Eager Loads not working

I have two model User and Profile in one to one relationship.
I want to retrieve all user where profile.status == TRUE using following code.
$users = User::with(['profile' => function ($query) {
$query->where('status', TRUE);
}])->get();
dd(count($users)); //50
I have 50 users and only among of them only 3 has status == TRUE. But always it display 50.
You are getting 50 users because you are applying condition to profile. dd($user->profile) you will get only the records of the profile whose status is true.
Use whereHas():
$users = User::whereHas('profile', function ($query) {
$query->where('status', TRUE);
})->get();
dd(count($users));
If you want to make it work with single Query, you can use Query Builder join like
\DB::table('users')->join('profile', function ($join){
$join->on('users.id', '=', 'profile.user_id')->where('profile.status', '=',TRUE);
})->get();
You said you're having N+1 problem, so you need to use both whereHas() and with() like this to get users with profiles and to solve N+1 problem:
$users = User::whereHas('profile', function ($query) {
$query->where('status', TRUE);
})
->with('profile')
->get();

Laravel OrderBy Nested Collection

I'm using a Roles package (similar to entrust). I'm trying to sort my User::all() query on roles.id or roles.name
The following is all working
User::with('roles');
This returns a Collection, with a Roles relation that also is a collection.. Like this:
I'm trying to get all users, but ordered by their role ID.
I tried the following without success
maybe because 'roles' returns a collection? And not the first role?
return App\User::with(['roles' => function($query) {
$query->orderBy('roles.id', 'asc');
}])->get();
And this
return App\User::with('roles')->orderBy('roles.id','DESC')->get();
None of them are working. I'm stuck! Can someone point me in the right direction please?
You can take the help of joins like this:
App\User::join('roles', 'users.role_id', '=', 'roles.id')
->orderBy('roles.id', 'desc')
->get();
Hope this helps!
You can make accessor which contains role id or name that you want to sort by.
Assume that the accessor name is roleCode. Then App\User::all()->sortBy('roleCode') will work.
Here's the dirty trick using collections. There might be a better way to achieve this(using Paginator class, I guess). This solution is definitely a disaster for huge tables.
$roles = Role::with('users')->orderBy('id', 'DESC')->get();
$sortedByRoleId = collect();
$roles->each(function ($role) use($sorted) {
$sortedByRoleId->push($role->users);
});
$sortedByRoleId = $sortedByRoleId->flatten()->keyBy('id');
You can sort your relations by using the query builder:
notice the difference with your own example: I don't set roles.id but just id
$users = App\User::with(['roles' => function ($query) {
$query->orderBy('id', 'desc');
}])->get();
See the Official Laravel Docs on Constraining Eager Loading
f you want to order the result based on nested relation column, you must use a chain of joins:
$values = User::query()->leftJoin('model_has_roles', function ($join)
{
$join>on('model_has_roles.model_id', '=', 'users.id')
->where('model_has_roles.model_type', '=', 'app\Models\User');})
->leftJoin('roles', 'roles.id', '=', 'model_has_roles.role_id')
->orderBy('roles.id')->get();
please note that if you want to order by multiple columns you could add 'orderBy' clause as much as you want:
->orderBy('roles.name', 'DESC')->orderby('teams.roles', 'ASC') //... ext
check my answer here:
https://stackoverflow.com/a/61194625/10573560

Laravel Eloquent maximize efficiency but keep elegant structure

Im working in a sensitive section of my app and i need to make sure to minimize the number of querys. I can easily do this with a multiple joins. The question is: is there a way to do this with beauty?
Elequent relationships are a good place to start but most of the time it requires multiple query.
The eager loading method used in this article looks alot better but still requires at least 2 querys and uses a whereIn statement instead of a join.
Article Example Of Eager Loading:
$users = User::with('posts')->get();
foreach($users as $user)
{
echo $user->posts->title;
}
Using Eager Loading, Laravel would actually be running the following
select * from users
select * from posts where user_id in (1, 2, 3, 4, 5, ...)
My current solution is to use laravel scopes in a way not intented.
public static function scopeUser($query) // join users table and user_ranks
{
return $query->join('users', 'users.id', '=', 'posts.user_id')
->join('user_ranks', 'users.rank_id', '=', 'user_ranks.id');
}
public static function scopeGroup($query,$group_id) // join feeds,group_feeds (pivot) and groups tables
{
return $query->join('feeds', 'feeds.id', '=', 'posts.feed_id')
->join('group_feed', 'feeds.id', '=', 'group_feed.feed_id')
->join('groups', 'groups.id', '=', 'group_feed.group_id')
->where("groups.id","=",$group_id);
}
The resulting query looks like this:
$posts = Post::take($limit)
->skip($offset)
->user() // scropeUser
->group($widget->group_id) // scropeGroup
->whereRaw('user_ranks.view_limit > users.widget_load_total')
->groupBy('users.id')
->orderBy('posts.widget_loads', 'ASC')
->select(
'posts.id AS p_id',
'posts.title AS p_title',
'posts.slug AS p_slug',
'posts.link AS p_link',
'posts.created_on AS p_create_on',
'posts.description AS p_description',
'posts.content AS p_content',
'users.id AS u_id',
'users.last_name AS u_last_name',
'users.first_name AS u_first_name',
'users.image AS u_image',
'users.slug AS u_slug',
'users.rank_id AS u_rank',
'user_ranks.name AS u_rank_name',
'user_ranks.view_limit AS u_view_limit'
)
->get();
Because of column name collisions i then need a huge select statement. This works and produces a single query, but its far from sexy!
Is there a better way to deal with big joined querys?
You could try to actually add the selects with aliases in the scope.
Note: This is totally untested
public static function scopeUser($query) // join users table and user_ranks
{
foreach(Schema::getColumnListing('users') as $column){
$query->addSelect('users.'.$related_column.' AS u_'.$column);
}
$query->addSelect('user_ranks.name AS u_rank_name')
->addSelect('user_ranks.view_limit AS u_view_limit');
$query->join('users', 'users.id', '=', 'posts.user_id')
->join('user_ranks', 'users.rank_id', '=', 'user_ranks.id');
return $query;
}
There is also no need to alias the post columns with a p_ prefix... But if you really want to, add another scope that does that and use addSelect()

Categories