How to express Larvel subqueries to use within a whereIn - laravel

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

Related

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();

Query builder - pivot table laravel

How do I get only users from database where they have no role assigned?
User belongs to many roles, the exact relation is:
public function roles() {
return $this->belongsToMany(Role::class, 'user_has_roles', 'user_id', 'role_id');
}
My problem is, that I do only work with Query builder, not with the model so I do not want to use relation.
EDIT:
I am working with migrations, so please no solutions when using model.
You can use doesntHave, I believe this uses QueryBuilder only.
User::doesntHave('roles')->get();
Reference: https://laravel.com/docs/5.6/eloquent-relationships
UPDATE
Using raw SQL query, you can do this.
Basically what this tries to do is to get all users where its id is not found in the user_has_roles table (meaning it doesn't have any relations)
SELECT *
FROM users
WHERE id NOT IN (SELECT user_id FROM user_has_roles)
I think you can convert this into a JOIN or something. I'm not that expert in raw SQL though.
UPDATE Trying Query Builder
$usersWithoutRoles = DB::table('users')
->whereNotIn(function ($query) {
$query->select(DB::raw('user_id'))
->from('user_has_roles')
->whereRaw('user_has_roles.user_id = users.id');
})
->get();
Reference: SQL - find records from one table which don't exist in another
You can try other alternatives there.

Use Laravel 5.3 Query Builder to replicate Eloquent ORM data structure for Sub Query

I am trying to replicate the result set that I get when using Eloquent ORM, except with Laravel Query Builder. Basically using this code I can get the packs to appear nested within the products so that when I loop them on the view I can further loop the packs within each products. Seems pretty basic right (see result set below).
$get_posts_for_product = Product::where('active', 1)
->with('packs')
->get()->toArray();
I have tried a few ways using Query Builder to get this to work but it joins the packs inline as I thought it would.
What is the best way to get this same Array structure using Query Builder, I am aware that the result set is a different type of array and that is fine but for my project it must be done using Query Builder at this point.
Thanks.
I would say, that is why you have Eloquent: you don't have to worry about how to have those relationships together.
However incase you really want to achieve the same result I will demo this using two tables users and messages:
1st method:
Retrieve the users and transform it by querying the database for relationships:
$result = DB::table('users')->get()->transform(function ($user){
$user->messages = DB::table('messages')->where('user_id', $user->id)->get();
return $user;
});
Downside: Having many users means a lot of db query on messages table.
Upside: less codes to write
2nd method:
Retrieve both tables using all the ids of user to query the messages:
$users = DB::table('users')->get();
$messages = DB::table('messages')->whereIn('user_id', $users->pluck('id')->toArray())->get();
$result = $users->transform(function ($user) use ($messages){
$user->messages = $messages->where('user_id', $user->id)->values();
return $user;
});
Downside: The need to still transform it.
Upside: Less database trips. i.e two queries only.
3rd method
Looks like the second except that you can group messages by 'user_id' then you do no extra filter when transforming users result:
$user = DB::table('users')->get();
$messages = DB::table('messages')->whereIn('user_id', $user->pluck('id')->toArray())
->get()
->groupBy('user_id');
$result = $user->transform(function ($user) use ($messages){
$user->messages = $messages[$user->id];
return $user;
});
Downside: Same with two.
Upside: no extra filter when transforming users.
Other method
Join on both users and messages when querying then transform the response, or simply use it as it is.
PS: Eloquent uses query builder.
The answer is open for update.

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