Laravel (Lumen) Eloquent querying with WHERE on a relation (Multi-level) - mysql

I have the following DB tables:
Purchase
-id
-workplace_id
Workplace
-id
-client_id
(and obviously some more fields, but for the example these are all the needed ones).
I would like to make a query like this:
SELECT * FROM
purchase
INNER JOIN workplace ON (purchase.workplace_id = workplace.id)
WHERE
(workplace.client_id = 1)
I'm trying to make this work with the Eloquent models, but I can't figure out how to filter on a joined table.
I tried:
$purchases = Purchase::query()
-> workplace()
-> where('client_id', '=', Auth::user() -> client_id)
-> get();
But apparently workplace() is undefined for some reason.
My Purchase.php model file looks like this:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Purchase extends Model
{
public function workplace(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(Workplace::class);
}
}
Any pointers on how to make this simple select work?
Thanks!
====EDIT=====
I found a possible solution:
$purchases = Purchase::with('workplace')
-> whereHas('workplace', function($q) {
return $q -> where('client_id', '=', Auth::user() -> client_id);
})
-> get();
But this generates an SQL that seems more complicated and is probably also slower:
select * from `purchases` where exists (select * from `workplaces` where `purchases`.`workplace_id` =
`workplaces`.`id` and `client_id` = ? and `workplaces`.`deleted_at` is null)
So I'm still looking for better alternatives

If you want to do a join, you need to build it with the join method, you can't use a relationship. See here for the docs:
https://laravel.com/docs/9.x/queries#joins
So you should be able to do something like this:
$purchases = Purchase::query()
->join('workplace', 'purchase.workplace_id', '=', 'workplace.id')
->where('workplace.client_id', '=', Auth::user()->client_id)
->get();
The generated SQL that you show in your edit is a sub-query, and you may still want to consider that. Sure, sub-queries are often slower than joins, but unless you're dealing with a massive dataset the performance difference might be negligible, and it allows you to use native Eloquent relationships.
See here for a discussion on this: https://stackoverflow.com/a/2577188/660694

Related

Add result of subquery to Eloquent query in Laravel 9

In Laravel 9 I am trying to add the result of a subquery to a query(for lack of better wording) and I am stuck. More concretely, I am trying to load all products and at the same time add information about whether the current user has bought that product.
Why do I want to do this?
I am currently loading all products, then loading all bought products, then comparing the 2 to determine if the user has bought a product, but that means extra queries which I would like to avoid. Pretend for the sake of this question that pagination doesn't exist(because when paginating the impact of those multiple queries is far diminished).
There is a many to many relationship between the 2 tables users and products, so these relationships are defined on the models:
public function products()
{
return $this->belongsToMany(Product::class);
}
and
public function users()
{
return $this->belongsToMany(User::class);
}
What I have tried so far:
I created a model for the join table and tried to use selectRaw to add the extra 'column' I want. This throws a SQL syntax error and I couldn't fix it.
$products = Product::query()
->select('id', 'name')
->selectRaw("ProductUser::where('user_id',$user->id)->where('product_id','products.id')->exists() as is_bought_by_auth_user")
->get();
I tried to use addSelect but that also didn't work.
$products = Product::query()
->select('id', 'name')
->addSelect(['is_bought_by_auth_user' => ProductUser::select('product_id')->where('user_id',$user?->id)->where('product_id','product.id')->first()])
->get();
I don't even need a select, I actually just need ProductUser::where('user_id',$user?->id)->where('product_id','product.id')->exists() but I don't know a method like addSelect for that.
The ProductUser table is defined fine btw, tried ProductUser::where('user_id',$user?->id)->where('product_id','product.id')->exists() with hardcoded product id and that worked as expected.
I tried to create a method on the product model hasBeenBoughtByAuthUser in which I wanted to check if Auth::user() bought the product but Auth wasn't recognized for some reason(and I thought it's not really nice to use Auth in the model anyway so didn't dig super deep with this approach).
$products = Product::query()
->select('id', 'name')
->addSelect(\DB::raw("(EXISTS (SELECT * FROM product_user WHERE product_users.product_id = product.id AND product_users.user_id = " . $user->id . ")) as is_bought_by_auth_user"))
->simplePaginate(40);
For all attempts $user=$request->user().
I don't know if I am missing something easy here but any hints in the right direction would be appreciated(would prefer not to use https://laravel.com/docs/9.x/eloquent-resources but if there is no other option I will try that as well).
Thanks for reading!
This should do,
$id = auth()->user()->id;
$products = Product::select(
'id',
'name',
DB::raw(
'(CASE WHEN EXISTS (
SELECT 1
FROM product_users
WHERE product_users.product_id = products.id
AND product_users.user_id = '.$id.'
) THEN "yes" ELSE "no" END) AS purchased'
)
);
return $products->paginate(10);
the collection will have purchased data which either have yes or no value
EDIT
If you want eloquent way you can try using withExists or withCount
i.e.
withExists the purchased field will have boolean value
$products = Product::select('id', 'name')->withExists(['users as purchased' => function($query) {
$query->where('user_id', auth()->user()->id);
}]);
withCount the purchased field will have count of found relationship rows
$products = Product::select('id', 'name')->withCount(['users as purchased' => function($query) {
$query->where('user_id', auth()->user()->id);
}]);

Filter With() in Query Scope

Controller
$r = \App\User::whereIn('id', $user_ids)->withPosts($category_id)->get();
User model
public function scopeWithPosts($query, $category_id)
{
return $query->with('posts')->where('category_id', $category_id);
}
I have been at this for too many hours now.
I am trying to use with() along with an query scope to add an extra filter to the relationship.
However it gives me the error " category_id not existing in users table"? What am I missing?
Laravel 6
The problem you are experiencing is that you are expecting the with('posts') function to return a query that is relative to the Posts ORM model. It won't, it will still return a reference to the original query. What you will find is that the with function returns $this, so you'll always get the original query.
What you are attempting is a SQL query to find the User, followed by another SQL query to get all the Post records of that user, with those posts filtered by category. So
SELECT * FROM Users WHERE id=?;
SELECT * FROM Posts WHERE user_id = ? AND category_id = ?
To do that in the Eloquent relationship, you need to subquery, like so:
return $query->with(['posts' => function ($q) use ($category_id) {
$q->where('category_id', $category_id);
}]);
Please comment if you need further info and I'll edit my answer.

Fastest way to order table by related table column

I have 2 tables comments and images
I'm trying to order the comments that have an image first and then comments without images.
I used this query below to solve this issue but it's very slow on large data it takes about 12 seconds
$data = Comment::withCount([
'images' => function ($query) use ($shop)
{
$query->where('shop_name', $shop);
},
])->where('shop_name', $shop)->orderBy('images_count', 'desc')->paginate(10);
How can I improve the performance or is there any other way to get similar results in faster way ?
The problem lies in how Laravel makes withCount() work - it will generate something like this:
SELECT `comments`.*,
(SELECT Count(*)
FROM `images`
WHERE `comment_id`.`id` = `comments`.`id`
AND `images`.`shop_name` = 'your shop') AS `images_count`
FROM `comments`
WHERE `shop_name` = 'your_shop'
ORDER BY `images_count`
This will force MySQL to execute count() subquery for every comment of specified shop.
What you need to do here is to make this correlated subquery (that executes for every row) into an independent query (that executes only once) and then utilize joins to let MySQL pair it all up:
$imagesCountQuery = DB::table('comments')
->selectRaw('comments.id AS comment_id, COUNT(comments.id) AS images_count')
->join('images', 'images.comment_id', '=', 'comments.id') /* !!! */
->where('images.shop_name', '=', $shop)
->groupBy('comments.id');
$data = Comment::joinSub($imagesCountQuery, 'images_count_sub', function ($join) {
$join->on('comments.id', '=', 'images_count_sub'.'comment_id'); /* !!! */
})->where('shop_name', $shop)->orderBy('images_count_sub.images_count', 'desc')->paginate(10);
!!! - this lines should be modified to represent your comment's images relation. In this example I just assumed it was hasMany relation since you didn't point that out in your question.
you need to create a hasMany relation in Comment model.
class Comment extends \Eloquent {
public function images()
{
return $this->hasMany('Images', 'comment_id');
}
}
Now you can use below query to fetch records.
$comments = Comments::with('images')->get()->sortBy(function($comment)
{
return $comment->images->count();
});

convert query from mysql to php laravel?

My query is the following:
select employe.id, employe.nam, users.name, users.name_user
from employe
left join users
on users.name = employe.id
it is a query to two tables: employe, users.
How can I pass it to my controller? Am I new to laravel..
I assume the user to employee is a One to One relation.
Did you setup the relation in both models?
If so you can do the following in your controller:
$employees = Employee::with('user')->all();
This will load all employees and the related user.
Question is the users.name a foreign key on the employee.id?
Thats a bit strange, i recommend using id's on both models (autoIncrement).
Laravel use a MVC pattern, a good practice is use a Model for your employe table
I recomend you to use an Eloquen Model, so, your query will look like this:
Employe::select('employe.id', 'employe.nam', 'users.name', 'users.name_user')
->leftJoin('users', 'users.name', 'employes.id')
->get();
You can easily maintain this kind of query with Eloquent relationships.
Add this method on your employee model
public function user(){
return $this->hasOne('App\User','name','id');
}
Add this method on your user model
public function Employee(){
return $this->belongsTo('App\Employee','id','name');
}
Add line on your controller
$employees = Employee::with('user')->all();

Laravel - Eloquent - Filter based on latest HasMany relation

I have this two models, Leads and Status.
class Lead extends Model
{
public function statuses() {
return $this->hasMany('App\LeadStatus', 'lead_id', 'id')
->orderBy('created_at', 'DESC');
}
public function activeStatus() {
return $this->hasOne('App\LeadStatus', 'lead_id', 'id')
->latest();
}
}
class LeadStatus extends Model
{
protected $fillable = ['status', 'lead_id'];
}
This works fine, now I'm trying to get all Leads based on the 'status' of the last LeadStatus.
I've tried a few combinations with no success.
if ($search['status']) {
$builder = $builder
->whereHas('statuses', function($q) use ($search){
$q = $q->latest()->limit(1);
$q->where('status', $search['status']);
});
}
if ($search['status']) {
$builder = $builder
->whereHas('status', function($q) use ($search){
$q = $q->latest()->Where('status', $search['status']);
});
}
Has anybody done this with Eloquent? Do I need to write some raw SQL queries?
EDIT 1: I'll try to explain again :D
In my database, the status of a lead is not a 1 to 1 relation. That is because I want to have a historic list of all the statuses which a Lead has had.
That means that when a Lead is created, the first LeadStatus is created with the status of 'new' and the current date.
If a salesman comes in, he can change the status of the lead, but this DOES NOT update the previous LeadStatus, instead it creates a new related LeadStatus with the current date and status of 'open'.
This way I can see that a Lead was created on 05/05/2018 and that it changed to the status 'open' on 07/05/2018.
Now I'm trying to write a query using eloquent, which only takes in count the LATEST status related to a Lead.
In the previous example, if I filter by Lead with status 'new', this Lead should not appear as it has a status of 'open' by now.
Hope this helps
Try this:
Lead::select('leads.*')
->join('lead_statuses', 'leads.id', 'lead_statuses.lead_id')
->where('lead_statuses.status', $search['status'])
->where('created_at', function($query) {
$query->selectRaw('max(created_at)')
->from('lead_statuses')
->whereColumn('lead_id', 'leads.id');
})->get();
A solution using the primary key (by Borjante):
$builder->where('lead_statuses.id', function($query) {
$query->select('id')
->from('lead_statuses')
->whereColumn('lead_id', 'leads.id')
->orderBy('created_at', 'desc')
->limit(1);
});
I had this same problem and posted my solution here but I think it's worth re-posting as it improves on the re-usability. It's the same idea as the accepted answer but avoids using joins, which can cause issues if you want to eager load relations or use it in a scope.
The first step involves adding a macro to the query Builder in the AppServiceProvider.
use Illuminate\Database\Query\Builder;
Builder::macro('whereLatestRelation', function ($table, $parentRelatedColumn)
{
return $this->where($table . '.id', function ($sub) use ($table, $parentRelatedColumn) {
$sub->select('id')
->from($table . ' AS other')
->whereColumn('other.' . $parentRelatedColumn, $table . '.' . $parentRelatedColumn)
->latest()
->take(1);
});
});
This basically makes the sub-query part of the accepted answer more generic, allowing you to specify the join table and the column they join on. It also uses the latest() function to avoid referencing the created_at column directly. It assumes the other column is an 'id' column, so it can be improved further. To use this you'd then be able to do:
$status = $search['status'];
Lead::whereHas('statuses', function ($q) use ($status) {
$q->where('status', $userId)
->whereLatestRelation((new LeadStatus)->getTable(), 'lead_id');
});
It's the same logic as the accepted answer, but a bit easier to re-use. It will, however, be a little slower, but that should be worth the re-usability.
If I understand it correctly you need / want to get all Leads with a specific status.
So you probably should do something like this:
// In your Modal
public function getLeadById($statusId)
{
return Lead::where('status', $statusId)->get();
// you could of course extend this and do something like this:
// return Lead::where('status', $statusId)->limit()....->get();
}
Basically I am doing a where and returning every lead with a specific id.
You can then use this function in your controller like this:
Lead::getLeadById(1)