Filter With() in Query Scope - mysql

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.

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

I want to show posts and all of their comments in laravel

I am trying to show all posts of mine and my friends and also wanna show the comments on that posts
here is my controller
$user = Auth::user();
$friend_ids = $user->friends()->pluck('friend_id')->toArray();
$posts=PostModel::whereIn('users_id',$friend_ids)
->orWhere('users_id',Auth::user()->id)
->leftJoin('users as p_user','posts.users_id','=','p_user.id')
->leftJoin('post_comments','posts.id','=','post_comments.post_id')
->leftJoin('users as c_user','post_comments.friend_id','=','c_user.id')
-select('posts.caption','posts.image','posts.created_at','p_user.name','p_user.user_img as user_image','posts.id','c_user.user_img as commenter_img','post_comments.comment')
->get();
but the issue is that whenever any post have more than one comments it create more than one post and show one comment on any post , hope so you understand my question if not then I return my data here is the result
[{"id":5,"caption":"5thpost","image":"s1.jpg","name":"roger","user_image":"roger.jpg","commenter_img":"alex.jpg","comment":"nice one"},
{"id":5,"caption":"5thpost","image":"s1.jpg","name":"alex","user_image":"alex.jpg","commenter_img":"sufi.jpg","comment":"wow"}]
here you can see the id 5 is repeating I want to show all comments of id 5
You can go a step further and eager load from friends
$friends = $user->friends()->with(['posts.comments'])->get()
and you can chain on extra functions inside the with statement if required!
Likely you would want to add a between dates for the posts function for instance like:
$friends = $user->friends()->with(['posts' => function($q) use ($start, $end){
return $q->whereBetween('created_at', [$start, $end]);
},'posts.comments'])->get()
you can get the posts with $friends->posts and the comments with $friends->posts->comments and all the data you want will already be loaded and it stops N + 1 queries!
In Friends Model:
public function posts()
{
return $this->hasMany(Post::class);
}
In Post Model:
public function comments()
{
return $this->hasMany(Comments::class);
}
Don't use joins, use Model Relationships. Then you can eager-load related records like:
$posts = $postModel->with('comments')->where...
The result is that each Post Model within the Collection would have a nested attribute called 'comments', the name of the method within the Model that describes the relationship. And this 'comments' attribute would contain an Eloquent\Collection of Comment Model records.

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

Using count subquery within CakePHP ORM

I'm looking through the CakePHP docs and I can't see anywhere that explains how you execute a subquery in the MySQL statement. I would essentially like to count the number of credits each user has as a field, but at the moment it is counting the credits cumulatively for all users into one field:
$this->Users->find()
->contain(['Plans','Products'])
->contain('Credits', function ($q) {
return $q->select(['count' => $q->func()->count('*')]);
})->group(['Users.id']);
The query I'm trying to create is more like :
SELECT *, (SELECT COUNT(*) FROM credits Credits WHERE Credits.user_id = Users.id) as credit_count FROM users Users group by Users.id ASC
Contained hasMany associations will always be retrieved in a separate query, and that is where the conditions will be applied that you are defining in the contain callback (check Sql Log tab of DebugKit).
To get the results that you are looking for requires to either join in the association, and counting the Credits in the same query, something like this:
$this->Users
->find()
->select(function(\Cake\ORM\Query $q) {
return ['count' => $q->func()->count('Credits.id')]
})
->select($this->Users)
->contain(['Plans','Products'])
->leftJoinWith('Credits')
->group(['Users.id']);
or to explicitly use a subquery, which works by simply creating a regular query object, and passing it to wherever the query builder accepts expression objects, for example as the value in your select list:
$subquery = $this->Users->Credits
->find()
->select(function(\Cake\ORM\Query $q) {
return ['count' => $q->func()->count('Credits.id')]
})
->where(function (\Cake\Database\Expression\QueryExpression $exp) {
return $exp->equalFields('Credits.user_id', 'Users.id'):
});
$this->Users
->find()
->select(['count' => $subquery])
->select($this->Users)
->contain(['Plans','Products']);
See also
Cookbook > Database Access & ORM > Query Builder > Selecting Specific Fields
Cookbook > Database Access & ORM > Query Builder > Advanced Conditions

Eloquent Query, possible Join

I have three models: driver, designation and dpsObject, with the following replationships:
driver->hasMany(dpsObject)
driver->belongsTo(Designation)
designation->hasMany(Driver)
dpsObject->belongsTo(Driver)
I'm trying to write a query to return a list of dpsObject records that correspond to the values of three user inputs, which are: a date range(From and To) holding the values of an EntryDate field in the dpsObject and a Designation input, holding the value of a Designation_name field in the Designation object.
Currently this is my Query:
$dps = dpsObject::where([['entryDate', '>=', $from],
['entryDate', '<=', $to]]);
$from and $to hold the request values gotten from the form user's submit.
I need to complete the query to capture the Designation name of a driver that that has dpsObject records. The challenge is that the designation_name field does not exist on the dpsObject model but only on the driver and designation models. This is how I want to maintain the database model. I think I need to be using a join or something similar, but I'm not sure how to go about it.
What is the best way to write such a query?
Kind regards
You can use nested whereHas():
$dpsObjects = dpsObject::whereBetween('entryDate', [$from, $to])
->whereHas('driver', function ($q) use($designationName) {
$q->whereHas('designation', function ($q) use($designationName) {
$q->where('designation_name', $designationName);
});
})
->get();
Here, designation and driver are belongsTo() relationships.