Find total with only the first iteration from relationship - php

In this scenario I have a Ticket model and a TicketReply model.
I can grab all replies to a ticket with $ticket->replies.
Considering status 1 or 3 of a ticket or reply means its state is open/unresolved, this is how I currently find the total of tickets open the Eloquent way.
$tickets = Ticket::all();
$tickets_open = 0;
foreach($tickets as $t)
{
$tickets_open++;
if(($t->replies()->first()->status == 2) || ($t->status == 2))
{
$tickets_open--;
}
}
return $tickets_open;
Is there a more efficient way of doing this with Eloquent?
Since as it is a query will run for each iteration.
If there is not, I can convert $tickets to an array and iterate it.
Update:
$t->replies()->first()->status was causing the 1+N with or without eager loading.
I changed to $t->replies->first()->status and 1+N is gone. A rookie mistake I believe.

You can use Eager Loading to reduce the number of queries.
$tickets = Ticket::with('replies')->get();
Doing this, instead of running 1+N queries, you will end up running two SQL queries:
select * from Tickets
select * from TicketReplies where ticket_id in (1, 2, 3, 4, 5, ...)

Though it's a bit confusing to me but I believe you may try something like this:
$openTickets = Reply::where('status', 2)->count();
Also maybe something like this:
$tickets = Ticket::whereHas('replies', function($query) {
$query->where('status', 2);
})->with('replies')->get();
Then count:
$t = 0;
$tickets->fetch('replies')->each(function($i) use (&$t){
$t += count($i);
});
dd($t); // total
I could be wrong but I guessed it could be done like this.
P/S:Please let me know the update.

Related

check to remove expired posts laravel

i have a post model that will be boosted by user for some certain time and now i have a crone job to be lunched every 15 mins to run a special script to see if post is boosted or finished .
but the problem is that every time it loops all the posts and it takes some certain amount of time i wanted to know if there is any alternative and better ways to do this ?? here is my code below :
$boosted = POST::whereIn('status_id', [4, 5, 6])->where('boosted',1)->get();
foreach ($boosted as $index => $item) {
if ($item->boosted_until < Carbon::now()) {
$wish = Wish::find($item->id);
$wish->boosted = 0;
$wish->save();
}
Following my comments on your question, you can have something like but accurate:
If you have it set as relations
Wish::whereHas('posts', function($query){
$query->where('boosted_until', '<', Carbon::now());
})->where('boosted', 1)->update(['boosted' => 0]);
I think the problem is. It's a huge amount of data that can cause execution time error. I suggest you to use queues in laravel https://laravel.com/docs/6.x/queues to run it on the background. You can chunk them also into pieces. For example you are about to loop a 100k row of datas. It's best to divide them into pieces so the queue job will work smoothly.
Thankfully, we can use eager loading to reduce this operation to just 2 queries. When querying, you may specify which relationships should be eager loaded using the with method:
I assume you already have hasMany relationship between Post and Wish models
// App\Post Model
public function wishes() {
return $this->hasMany('App\Wish');
}
// Controller
//...
$boosted = POST::with('wishes')->whereIn('status_id', [4, 5, 6])->where('boosted',1)->get();
$wishesIDsToUpdate = [];
foreach ($boosted as $index => $item) {
if ($item->boosted_until < Carbon::now()) {
foreach($item->wishes as $wish) {
$wishesIDsToUpdate[] = $wish->id;
}
}
}
if(!empty($wishesIDsToUpdate)) {
Wish::whereIn('id', $wishesIDsToUpdate)->update(['boosted' => 0]);
}

Laravel hasMany relation count

There are many Categories(Category.php) on my index site, which has many Boards(Board.php).
A Board can have many Topics(Topic.php), and a Topic can have many Replies (Reply.php).
I want to fetch the number of replies of the current board, possibly with eager loading.
I have a way, but it executes too many queries. I create a post_count attribute, then I iterate over every topic and gather number of replies. Is there a way to select all replies from the current board?
public function getPostCountAttribute()
{
$count = 0;
foreach ($this->topics()->withCount('replies')->get() as $topic) {
$count += $topic->replies_count;
}
return $count;
}
Found a nice way. Even though, I'll leave this question open, if anyone finds a better way to do it.
I declared a hasManyThrough relationship, and the called the count():
Board.php:
public function replies()
{
return $this->hasManyThrough(Reply::class, Topic::class);
}
And then called:
$board->replies->count()
30 queries executed (45 before).
Still, I would like to find an eager way to load the data, getting the query number down to 2 or 3 queries.
In addition to your reply you can do something like this.
public function repliesCount() {
return $this->replies()->selectRaw('board_id, count(*) as aggregate')
->groupBy('board_id');
}
and to use this use as below
$board->repliesCount()
Please note that you have to change query according to you.
Try it
Change
$count = 0;
foreach ($this->topics()->withCount('replies')->get() as $topic) {
$count += $topic->replies_count;
}
To
$topic_ids = $this -> topics -> pluck('id');
$reply_count = Reply::whereIn('id',$topic_ids) -> count();

Refactoring Laravel function to reduce number of database queries

I wondered if anyone could offer me some advice on refactoring the below function to reduce the number of database queries? Alternatively, there may be a completely different way of achieving this using Laravel.
I am trying to calculate the P&L for a Job, which is made up of Products, which are made up of Components:
public function jobProfitAndLoss($id)
{
$products_in_job = DB::table('job_product')
->where('job_id', $id)
->get();
$total_price = 0.0000;
$total_cost = 0.0000;
foreach ($products_in_job as $row) {
$total_price = $total_price + ($row->quantity)*($row->price);
$product_id = $row->product_id;
$components_in_product = DB::table('components')
->where('product_id', $product_id)
->get();
foreach ($components_in_product as $component) {
$total_cost = $total_cost + ($component->cost)*($row->quantity);
}
}
return $total_price-$total_cost;
}
Products have Components -
https://www.dropbox.com/s/ncnij8dnh99sb9v/Screenshot%202016-04-09%2015.22.26.png?dl=0
Components belong to Products -
https://www.dropbox.com/s/3dx6u30gbod2rv4/Screenshot%202016-04-09%2015.23.43.png?dl=0
Jobs have many Products -
https://www.dropbox.com/s/q179t0knd7y8z4k/Screenshot%202016-04-09%2015.24.11.png?dl=0
You will see here that there are some of the same queries being executed multiple times, which I am not sure how to avoid in this situation -
https://www.dropbox.com/s/xonbtx9cdqvq1wd/Screenshot%202016-04-09%2015.33.07.png?dl=0
Any help is much appreciated.
Edit: It seems that you aren't using models. If you have not done so, create models for your database entries. You will need to use the protected $table attribute for job_product as Eloquent may not be able to automatically convert your class name into the correct table name.
First of all, set up relations if you have not done so. For example, under Job.php, include the Products relation:
public function products() {
return $this->hasMany(App\Products::class); // Assuming App is the namespace
}
Now, instead of using a Fluent query for $components_in_product, you are able to directly do $components_in_product = $products_in_job->products;. However, this still leads to N+1 queries.
As a result, take a look at this: https://laravel.com/docs/5.2/eloquent-relationships#eager-loading
$books = App\Book::with('author')->get();
foreach ($books as $book) {
echo $book->author->name;
}
For this operation, only two queries will be executed:
select * from books
select * from authors where id in (1, 2, 3, 4, 5, ...)
Therefore, change $products_in_job to an Eloquent query and add ->with('products') to the query.

Get number of rows in Laravel's Eloquent before using "take" and "skip"

I want to query my Laravel model using eloquent for results that may need to match some where clauses, then take and skip predefined numbers.
This isn't a problem in itself, but I also need to know the number of rows that were found in the query before reducing the result set with take and skip - so the original number of matches which could be every row in the table if no where clauses are used or a few if either is used.
What I want to do could be accomplished by making the query twice, with the first omitting "->take($iDisplayLength)->skip($iDisplayStart)" at the end and counting that, but that just seems messy.
Any thoughts?
$contacts = Contact::where(function($query) use ($request)
{
if (!empty($request['firstname'])) {
$query->where(function($query) use ($request)
{
$query->where('firstname', 'LIKE', "%{$request['firstname']}%");
});
}
if (!empty($request['lastname'])) {
$query->where(function($query) use ($request)
{
$query->where('lastname', 'LIKE', "%{$request['lastname']}%");
});
}
})
->take($iDisplayLength)->skip($iDisplayStart)->get();
$iTotalRecords = count($contacts);
You can use count then get on the same query.
And by the way, your whole query is a bit over complicated. It results in something like this:
select * from `contacts` where ((`firstname` like ?) and (`lastname` like ?)) limit X, Y
Closure in where is used to make a query like this for example:
select * from table where (X or Y) and (A or B);
So to sum up you need this:
$query = Contact::query();
if (!empty($request['firstname'])) {
$query->where('firstname', 'like', "%{$request['firstname']}%");
}
if (!empty($request['lastname'])) {
$query->where('lastname', 'like', "%{$request['lastname']}%");
}
$count = $query->count();
$contacts = $query->take($iDisplayLength)->skip(iDisplayStart)->get();
The Collection class offers a splice and a count method which you can take advantage of.
First you would want to get the collection..
$collection = $query->get();
Then you can get the count and splice it.
$count = $collection->count();
$records = $collection->splice($iDisplayStart, $iDisplayLength);
This might be hard on performance because you are querying for the entire collection each time rather than putting a limit on the query, so it might be beneficial to cache the collection if this page is going to be hit often. At the same time though, this will hit the database only once, so it's a bit of a trade off.

Counting total distant relationships in Laravel (eager loading?)

I'm having issues getting a proper count total with my Laravel model.
Model Structure
User
Item
ItemLike
A user can have multiple Items, and each of these Items can have multiple ItemLikes (when a user 'likes' the item).
I can easily get the individual ItemLike counts when using an Item model:
return $this->itemLikes()->count();
But I can't figure out how to get the total # of ItemLike's a User has across all the Item's he owns.
EXAMPLE
User A has 3 Items. Each Item has 5 ItemLike's, for a grand total of 15.
I tried using eager loading on the User model like this:
return $this->items()->with('itemlikes')->get()->count();
But that returns 3 (the # of Items)
These are the queries it ran, which appears like the second query is the one I want, yet every way I try it I still get 3 instead of 15
select * from `items` where `items`.`user_id` = '1000'
select * from `item_likes` where `item_likes`.`item_id` in ('1000', '1001', '1002')
After suggestions from others I found 2 solutions to get the result.
Using whereIn:
$itemViewCount = ItemView::
whereIn('item_views.item_id', $this->items()->lists('id'))
->count();
return $itemViewCount;
2 queries for a total of 410μs
Using join:
$itemViewCount = $this->items()
->join('item_views', 'item_views.item_id', '=', 'items.id')
->count();
return $itemViewCount;
2 queries for a total of 600μs
Isn't it just a case of creating a method that would return the number of items for the model. e.g.:
#UserModel
public function nbLikes()
{
$nbLikes = 0;
foreach($this->items() as $item) {
$nbLikes += $item->itemLikes()->count();
}
return $nbLikes;
}
And then User::nbLikes() should return the piece of data you are looking for?
try this:
$query="select count(il.id) from item_likes il,item itm where il.item_id=itm.id and tm.user_id=1000";

Categories