Laravel join with conditions on 2 tables - laravel

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.

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

How to fetch two related objects in Laravel (Eloquent) with one SQL query

I am trying to get two related objects in Laravel using eager loading as per documentation.
https://laravel.com/docs/5.4/eloquent-relationships#eager-loading
My models are:
class Lead extends Model {
public function session() {
return $this->hasOne('App\LeadSession');
}
}
class LeadSession extends Model {
public function lead() {
return $this->belongsTo('App\Lead');
}
}
I want to get both objects with one SQL query. Basically I want to execute:
select * from lead_sessions as s
inner join lead as l
on l.id = s.lead_id
where s.token = '$token';
and then be able to access both the LeadSession and Lead objects. Here is the php code I am trying:
$lead = Lead::with(['session' => function ($q) use ($token) {
$q->where('token','=',$token);
}])->firstOrFail();
print($lead->session->id);
I have also tried:
$lead = Lead::whereHas('session', function($q) use ($token) {
$q->where('token','=',$token);
})->firstOrFail();
print($lead->session->id);
and
$session = LeadSession::with('lead')->where('token',$token)->firstOrFail();
print($session->lead->id);
In all three cases I get two queries executed, one for the leads table, and another for the lead_sessions table.
Is such a thing possible in Eloquent? In my view it should be a standard ORM operation, but for some reason I am struggling a whole day with it.
I don't want to use the Query Builder because I want to use the Eloquent objects and their functions afterwards.
I am coming from Python and Django and I want to replicate the behavior of select_related function in Django.
Try this and see if it makes more than one query
$session = LeadSession::join('leads', 'leads.id', '=', 'lead_sessions.lead_id')
->where('token',$token)
->firstOrFail();
I hope it only runs a single query. I didnt test this. Not sure if you have to add a select() to pick the columns. But yeah, try this first.
Updates
Just adding how to use both session and lead data. Try a select and specify the data you need. The reason being that if both tables have similar columns like 'id', one of them will be overwritten. So you have to alias your select like
$session = LeadSession::join('leads', 'leads.id', '=', 'lead_sessions.lead_id')
->where('token',$token)
->select(
'lead_sessions.*',
'leads.id as lead_id',
'leads.name',
'leads.more_stuff'
)
->firstOrFail();
Now all this data belongs to $session variable. For testing you were doing
print($lead->session->id);
//becomes
print($session->lead_id); //we aliased this in the query

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.

Resources