Laravel - Unique user list in related model - laravel

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');
}])

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'),
);

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 complicated relationship

My laravel app structed like this:
People: Model & Controller
Pets: Model & Controller. Has many to many relationship with People.
Abiilities: Model & Controller.
People_pets: People pets. (pivot table with people_id and pet_id). Also has 4 columns of abbility1_id, abbility2_id, abbility3_id and abbility4_id.
Now.. I built an API method to return the user pets, and it looks like this:
public function pets()
{
return $this->belongsToMany(Pet::Class, 'user_pets')->withPivot(
'abbility1_id',
'abbility2_id',
'abbility3_id',
'abbility4_id'
)->orderBy('position','asc');
}
which makes $user->pets returns the user list of pets, with the pivot information of user_pets and the abbility ids.
The question
What I want to do is add a json object to this method result named "abbilities" and get the data of the abbilities there such as name and description from the Abbilities model/controller.
How I can add the abbilities information to my query out of just the ID's?
The output of the current API CALL:
Desired output: array of Abbilities inside every pet object with the detail of the attack_id's inside $user->pets->pivot->attack1_id
You could just use the query builder and orWhere to grab the data. I don't think you can access it through relationships with the way you have it set up.
I don't know how you access your pet ability ids, but I'm guessing it's like $pet->abbility1_id.
$abbilities = Abbilities::where('id', '=', $pet->abbility1_id)
->orWhere('id', '=', $pet->abbility2_id)
->orWhere('id', '=', $pet->abbility3_id)
->orWhere('id', '=', $pet->abbility4_id)
->get();

Eager Load Relation, When Relation Doesn't Exist

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.

Laravel join with conditions on 2 tables

In my application the users can create a task and assign it to a project that ca be either active or archived, or it can be created without a related project.
What I'm trying to do now is to fetch all the tasks created by company users that are either assigned to active projects or that have no project assigned.
I managed to complete the first part with the following code inside the Company model:
public function tasks()
{
return $this->hasMany('App\Project', 'company')
->join('tasks', function($join)
{
$join->on('projects.id', '=', 'tasks.project')
->where('projects.status', '=', Project::STATUS_ACTIVE);
});
}
This function fetches all the company tasks assigned to active projects, but I also want to fetch the tasks that are not assigned to any project (this is indicated by the project column in the tasks table set to 0).
The problem is that I don't know where to put the orWhere condition. I tried many changes but none of them worked.
Furthermore, there's one thing I don't get: why is the join function not something like the following?
$this->hasMany('App\Task', 'company')
->join('projects', ...
I am not sure if I understood really well your issue. If you could post your models and how the relationships are defined it would help a lot.
Anyway for navigating through queries I use the following "rule":
And then for retrieving the registers from table1 that are related to any register from table5 and filtered by a certain id value for table5.
public function navigation($table5ID){
return Table1::whereHas('table2',function($query) use ($table5ID){
$query->whereHas('table3',function($query2) use ($table5ID){
$query2->whereHas('table4',function($query3) use ($table5ID){
$query3->whereHas('table5',function($query4) use ($table5ID){
$query4->where('table5_id',$table5ID);
});
});
});
})->get();
}
I made a post on my personal blog some time ago. Hope this helps.
After struggling for some time I eventually found a solution to my problem.
My main issue was due to a misunderstanding of the Eloquent ORM and/or the Database Builder.
What I did to solve the issue is the following:
$companyTasksQuery = DB::table('tasks')
->where('company', '=', $this->id)
->where('project', '=', 0)
->where('status', '=', Task::STATUS_INCOMPLETE);
return $this->hasMany('App\Task', 'company')
->join('projects', function($join)
{
$join->on('projects.id', '=', 'tasks.project')
->where('projects.status', '=', Project::STATUS_ACTIVE);
})
->select('tasks.*')
->union($companyTasksQuery);
As you can see from the code above I kind of switched the join. Previously I was joining the projects table with the tasks but now I'm doing it the other way around and the ->select() fixed my issue (as you can see from my previous code I missed completely the select.
In order to add the other condition I simply added it as an union with another query that is selecting all the incompleted tasks that are not assigned to any project.

Resources