Eager Load Relation, When Relation Doesn't Exist - laravel

So, I have a Product model which belongsTo('App\Tax'). A Product may or may not be associated with a Tax (the tax_id fk in Product could be null).
The query below works fine as long as I have at least one Product associated with a Tax:
\App\Product::with("tax")->get()
It starts throwing the following exception, when none of the Product's have associated Taxes:
Illuminate\Database\QueryException with message 'SQLSTATE[22P02]: Invalid text
representation: 7 ERROR: invalid input syntax for uuid: "0" (SQL: select *
from "taxes" where "taxes"."id" in (0))'
While this exception is quite understandable, my question is how do I avoid this situation?
Note: The tax.id column is of postgres uuid type.
EDIT 1
I should explicitly mention that I'm trying to write a query to fetch all Products, belonging to a particular Organization, whether they're associated with a Tax or not.
The query above, works just fine as long as there is at least one Product associated with a Tax. It throws QueryException if all the products (1 or more), are not associated with any tax.
EDIT 2
So, I got a working solution, that doesn't seem quite right. I'm still open to a better solution. Here's how I got it to work:
// First just fetch all the products using whatever criterion
$products = $query
->orderBy ($params["orderBy"], $params["sortDir"])
->skip ($params["start"])
->take ($params["length"])
->get();
// Then, load the relation, if there's at least one product having
// that relation.
// Solution requires one additional aggregate query per relation
// than a regular with() clause would have generated
$relations = array('purchase_tax', 'sale_tax', 'category');
foreach($relations as $relation) {
$count = $org->products()
->whereHas($relation, function($query){
$query->whereNotNull('id');
})
->count();
if ($count) {
$products->load($relation);
}
}
This gives me all products, even if none of them have the associated relations.

You can go many ways.
Use has \App\Product::has('tax')->whith('tax')->get(); It will get product that have at least one Tax
based on condition for relationship
\App\Product::whereHas('tax', function ($query) {
$query->where('content', 'like', 'foo%');
})->whith('tax')->get();
\App\Product::with(['tax' => function ($query) {
$query->whereNotNull('product_id');
}])->get(); With but subquery where product_id is not null;
$products = \App\Product::get();
if ($products->tax()->count() > 0) {
$products->load('tax');
}
Lazy Load Count the product tax and if greater then 0 lazy load them
Hope it helps!

I do not like to answer my own questions, but since I've not received any answers, I'm posting one myself, for the benefit of whomsoever it may concern.
I got a working solution, that doesn't seem quite right. I'm still open to a better solution. Here's how I got it to work:
// First just fetch all the products using whatever criterion
$products = $query
->orderBy ($params["orderBy"], $params["sortDir"])
->skip ($params["start"])
->take ($params["length"])
->get();
// Then, load the relation, if there's at least one product having
// that relation.
// Solution requires one additional aggregate query per relation
// than a regular with() clause would have generated
$relations = array('purchase_tax', 'sale_tax', 'category');
foreach($relations as $relation) {
$count = $org->products()
->whereHas($relation, function($query){
$query->whereNotNull('id');
})
->count();
if ($count) {
$products->load($relation);
}
}
This gives me all products, even if none of them have the associated relations.

Related

How to express Larvel subqueries to use within a whereIn

I'm sorry for asking somthing so simple, but I can't get the docs (https://laravel.com/docs/9.x/queries#subquery-where-clauses)
I'm writting something like a social network functionality, so I have my messages table and my users table, there's another pivot table where I store the users the user follows, and they work pretty well.
I want to represent the following SQL in Laravel's ORM
SELECT * FROM mensajes
WHERE user_id=1
OR user_id IN (SELECT seguido_id FROM seguidos WHERE user_id=1)
The idea is I'll get the user's posts, and also the posts from the users that the user follows.
My following solution works, but I feel it's quite dirty, and should be solved with a Subquery
// this relation returns the users the user is following, ans works correctly
$seguidos = auth()->user()->seguidos;
// I store in an array the ids of the followed users
$seg = [];
foreach ($seguidos as $s) {
array_push($seg, $s->id);
}
array_push($seg, auth()->user()->id);
// Then I retrieve all the messages from the users ids (including self user)
$this->mensajes = Mensaje::whereIn('user_id', $seg)
->orderBy('created_at', 'desc')
->get();
I'd like to change everything to use subqueries, but I don't get it
$this->mensajes = Mensaje::where('user_id', auth()->user()->id)
->orWhereIn('user_id', function($query) {
// ... what goes here?
// $query = auth()->user()->seguidos->select('id');
// ???? This doesn't work, of course
}
->orderBy('created_at', 'desc')
->get();
You can simply construct the raw query as you have done with the SQL.
->orWhereIn('user_id', function($query) {
$query->select('seguido_id')
->from('seguidos')
->where('seguidos.user_id', auth()->user()->id);
});
But normally sub queries have a relationship between the primary SQL query with the sub query, you don't have this here and instead of doing one query with a sub query, you can quite simply write it as two queries. If you are not calling this multiple times in a single request, one vs two queries is insignificant performance optimization in my opinion.
Mensaje::where('user_id', auth()->user()->id)
->orWhereIn(
'user_id',
Seguidos::where('user_id', auth()->user()->id)->pluck('seguido_id'),
);

Laravel Eloquent optimize request with many relationships

I have actually a DB structure with a dashboard_users table with several morphedByMany relationship with retailers, brands, private_groups tables, etc (there are several models which are morphed to this relation).
These relationships are handled through a roleables table.
So my dashboardUser model has these functions :
public function retailer()
{
return $this->morphedByMany(Retailer::class, 'roleable')
->withPivot('role_id')
->withTimestamps();
}
public function brand()
{
return $this->morphedByMany(Brand::class, 'roleable')
->withPivot('role_id')
->withTimestamps();
}
public function privateGroup()
{
return $this->morphedByMany(PrivateGroup::class, 'roleable')
->withPivot('role_id')
->withTimestamps();
}
Some dashboardUsers can have a relation with one or many Retailer, but also one or many Brand, or with some PrivateGroup. There is no rule here, a DashboardUser can have only one relation but also a hundred.
In my dashboard I have a screen where I need to manage all these relations and show which user has which relation.
Actually to retrieve all my DashboardUsers I'm making this simple Eloquent request :
$aUsersList = DashboardUser::with(['brand', 'retailerBrand', 'retailer', 'retailerGroup', 'privateGroup'])
->get()
->unique('uuid')
->all();
which is working fine... except that now that I have around 3k rows on each table the request takes more than 10 seconds to be executed.
I'm wondering how I could optimize this request to receive the same results.
I'm thinking of making several leftJoins this way but I'm stuck with 2 problems:
$aUsersList = DashboardUser::select("dashboard_users.*",
"retailers.uuid", "retailers.name", "retailers.postal", "roleables.role_id",
"brands.uuid", "brands.name",
"private_groups.uuid", "private_groups.name"
)
->leftJoin('roleables', function($query){
$query->on("dashboard_users.uuid", 'roleables.dashboard_user_uuid');
})
->leftJoin('retailers', function($query){
$query->on("retailers.uuid", 'roleables.roleable_id');
})
->leftJoin('brands', function($query){
$query->on("brands.uuid", 'roleables.roleable_id')
;
})
->leftJoin('private_groups', function($query){
$query->on("private_groups.uuid", 'roleables.roleable_id')
;
})
->get()
->all()
;
This request is much more quicker to be executed but it's not returning all the data (it seems to return only dashboardUsers with retailer relationship).
And also I fear I'll be loosing my relations this way (all the fields are aggregated in a single object). So is there a way to rehydrate my model with it's relations?
Last but not least I'm using Lumen V6 (so pretty close to Laravel 6).
Thank you for pushing me in the right direction.

Wrong ID returned from local Eloquent scope

In my Laravel project I've got a model (and an underlying table) for lessons. Now I'm trying to write a local scope for returning all lessons that have been finished by a particular user. The definition of "finished" is that there exists a row in a table named "lesson_results" with this lesson's ID and the users ID.
My scope currently looks like this:
public function scopeFinished($query, User $user)
{
return $query->join('lesson_results', function($join) use($user)
{
$join->on('lesson_results.lesson_id', '=', 'lessons.id')
->where("user_id", $user->id);
});
}
This kinda works. When I do a Lesson::finished($user)->get() I get out the correct lessons for that user. However the lessons in the collection returned all have the wrong ID's! The ID's I see are the ID's from the lesson_results table. So when I check $lesson->id from one of the returned items I don't get the ID from that lesson, but the ID from the corresponding row in the lesson_results table.
I've checked in mysql and the full query sent from Laravel is the following
select * from `lessons` inner join `lesson_results` on `lesson_results`.`lesson_id` = `lessons`.`id` and `user_id` = 53
This query DO return two columns named id (the one from the lessons table and the one from the lesson_results table) and it seems Laravel is using the wrong one for the result returned.
I don't know if I'm going about this the wrong way or if it's a bug somewhere?
This is on Laravel 7.6.1.
edit: Ok, I think I actually solved it now. Not really sure though if it's a real solution or just a workaround. I added a select() call so the return row now is
return $query->select('lessons.*')->join('lesson_results', function($join) use($user)
...which makes it only return the stuff from the lessons table. But should that really be needed?
One of the same column names will be covered by the other.
Solution 1:
Specify the table with the column, and alias the other table's column if it has same column name.
Lesson::finished($user)->select('lessons.*', 'lesson_results.id AS lesson_result_id', 'lesson_results.column1', 'lesson_results.column2',...)->get();
Solution 2:
Or you can use Eloquent-Builder eager-loading whereHas,(Assuming you have build the relationship between model Lesson and model LessonResult)
public function scopeFinished($query, User $user)
{
return $query->whereHas('lessonResults', function($query) use($user)
{
$query->where("user_id", $user->id);
});
}
So you can get lesson like this:
Lesson::finished($user)->get();

Laravel - Unique user list in related model

My question is regarding ensuring a unique array of users in a related model using Eloquent's query builder.
One feature of an app I am working on displays a list of active conversations with other users, text messenger style. Each conversation should preview the most recent message received. Since conversations can be carried out between multiple users, a conversation between you and John should be different from a conversation between you, John, and Jane.
I've set up two relevant models: Message and User. Message is related to User in the following way:
public function sent_to() {
return $this->belongsToMany('App\User', 'message_users');
}
I am trying to return a list of unique conversations in my message controller like so:
$show = \App\Message::where('team_id', $team_id)
->where('user_id', Auth::user()->id)
->with(['sent_to' => function($query) {
$query->distinct();
}])
->with('user')
->orderBy('created_at', 'desc')->skip($offset)->take($limit)->get();
return $show;
The ->with(['sent_to'... section is where I'm a bit lost. Of course, the intent is that I get a list of unique addressees; however, I get all results. Any help is appreciated!
Update using jedrzej.kurylo's suggestion
jedrzej.kurylo suggested the following:
$show = \App\Message::where('team_id', $team_id)
->where('user_id', Auth::user()->id)
->with(['sent_to', function($query){
$query->groupBy('id');
}])
->with('user')
->orderBy('created_at', 'desc')->skip($offset)->take($limit)
->get();
return $show;
This yields the following error:
Since this is a many-to-many relationship (User linked to Message via 'message_user' table), 'id' actually refers to the id of the pivot table. What I would actually like is to get the pivot table's 'user_id'. Changing to ->groupBy('user_id') (a value on the pivot table) yields the following error: "mb_strpos() expects parameter 1 to be string, object given." Which is a radically different error.
I'm working on a work-around, and will update with it when I get it working--but it will require a couple more explicit queries. I feel like this is should be possible!
Key is in your error message "not compatible with sql_mode=only_full_group_by"
If you disable this mode your group by should work as expected.
See this question for 2 ways to disable this mode
Disable ONLY_FULL_GROUP_BY
And mysql docs for more info on the setting.
https://dev.mysql.com/doc/refman/8.0/en/sql-mode.html#sqlmode_only_full_group_by
Try grouping related results by their IDs:
->with(['sent_to' => function($query) {
$query->groupBy('id');
}])

Laravel collection eager loading based on nested relation field value

I'm trying to return a Laravel collection object with relations where the resulting collection is based on a nested criteria (ie. a nested model's field value). So it would look something like this:
User -> Category -> Post -> Comments where comment.active == 1
In this case, I want the result to include all of a specific user's categories => posts => comments, where the comment is active. If it is active, it would be nested in the proper hierarchy (Category->Post->Comment). If the comment is not active, any related post and potentially category (if there are no other posts with active comments) should not show up in the collection at all.
I've tried eager loading through with(), load() and filter() with no luck. They will continue to load the relations with empty comment relations. Looking for guidance as to where to research: joins? filters? advanced wheres with nesting?
One attempt:
$user->categories->filter(function($category) {
return $category->isActive();
});
In my model I have all the relationships setup appropriately, and in addition to that I have setup isActive() as follows:
// Category model
public function isActive() {
$active = $this->posts->filter(function($post) {
return $post->isActive();
}
}
// Post model
public function isActive() {
return (boolean) $this->comments()->where('active', 1)->count();
}
This works as expected, but it also includes eagerly loaded nested relationships where comments have an active field of 0. Obviously I'm doing this the wrong way but would appreciate any direction.
Another attempt:
User::with(['categories.posts.comments' => function($q) {
$q->where('active', 1);
}])->find(1);
Unfortunately, this also loads relations (categories and posts) that have no active comments. Replacing the relations with 'categories.posts.isActive' does not work either.
Still confusing because you didn't provide enough code but you may try something like this to get all the users with nested categories.posts.comments without any condition:
$users = User::with('categories.posts.comments')->get();
But it'll give you every thing even when you don't have any comments but to add condition you may try something like this:
// It should return all user models with `categories - posts - active comments`
$users = User::with('categories.posts.activeComments')->get();
Post model:
public function activeComments() {
return $this->hasMany('Comment')->where('active', 1);
}
You may also add more filters using constraints like:
$users = User::with(array('categories.posts.activeComments' => function($query){
$query->whereNull('comments.deleted_at');
}))->get();
But I'm not sure about it, don't know enough about your relationships, so just gave you an idea.

Resources