How to sum a colum from a related model efficiently on Laravel - php

Ok I got this table
affiliates_referral_clicks
id | affiliate_id | clicks | date
1 | 1 | 10 | 2021-07-14
2 | 1 | 2 | 2021-07-11
3 | 2 | 1 | 2021-07-11
4 | 2 | 14 | 2021-07-10
...
Of course my Model Affiliate has a relationship with referralClicks
Affiliate.php
public function referralClicks(){
return $this->hasMany(AffiliateReferralClick::class,'affiliate_id');
}
Now I want to bring all Affiliates with the SUM of all their clicks that have a date between a given date. I implemented it like this
$affiliate = Affiliate::with(['referralClicks' => function($query) use($params) {
$query->whereDate('date','>=', $params['dateFrom'])
->whereDate('date','<=', $params['dateTo'])
->select('clicks')
;
}])->get();
foreach ($affiliates as $affiliate){
$affiliate->totalClicks = $affiliate->referralClicks->sum('clicks');
}
this works fine, but since the affiliates_referral_clicks table is waaaay too big and the request ends up being too slow, I think if you do the query without using Eloquent's helpers you can get a much faster query.
So my question would be...how can I do the same I just did but with raw querys (or whatever the most efficient way is)? Im using a MySQL DB I hope you guys can help me!

Haven't tried that yet but that's how I'd solve this (if we assume, you only need the sum and nothing else from the relationship):
$affiliate = Affiliate::withSum(['referralClicks.clicks as totalClicks' => function($query) use($params) {
$query->whereDate('date','>=', $params['dateFrom'])
->whereDate('date','<=', $params['dateTo'])
->select('clicks')
;
}])->get();

Related

Laravel Query Optimization (SQL)

My user_level database structure is
| user_id | level |
| 3 | F |
| 4 | 13 |
| 21 | 2 |
| 24 | 2 |
| 33 | 3 |
| 34 | 12+ |
I have another table users
| id | school_id |
| 3 | 3 |
| 4 | 4 |
| 21 | 2 |
| 24 | 2 |
| 33 | 3 |
| 34 | 1 |
What I have to achieve is that, I will have to update the level of each user based on a certain predefined condition. However, my users table is really huge with thousands of records.
At one instance, I only update the user_level records for a particular school. Say for school_id = 3, I fetch all the users and their associated levels, and then increase the value of level by 1 for those users (F becomes 1, 12+ is deleted, and all other numbers are increased by 1).
When I use a loop to loop through the users, match their user_id and then update the record, it will be thousands of queries. That is slowing down the entire application as well as causing it to crash.
One ideal thing would be laravel transactions, but I have doubts if it optimises the time. I tested it in a simple query with around 6000 records, and it was working fine. But for some reason, it doesnt work that good with the records that I have.
Just looking some recommendation on any other query optimization techniques.
UPDATE
I implemented a solution, where I would group all the records based on the level (using laravel collections), and then I would only have to issue 13 update queries as compared to hundreds/thousands now.
$students = Users::where('school_id', 21)->get();
$groupedStudents = $students->groupBy('level');
foreach ($groupedStudents as $key => $value) :
$studentIDs = $value->pluck('id');
// condition to check and get the new value to update
// i have used switch cases to identify what the next level should be ($NexLevel)
UserLevel::whereIn('userId', $studentIDs)->update(["level" => $nextLevel]);
endforeach;
I am still looking for other possible options.
First defined a relationship in your model, like:
In UserLevel model:
public function user() {
return $this->belongsTo(\App\UserLevel::class);
}
And you can just update the level without 12+ level's query, only by one query, and delete all 12+ level by one query.
UserLevel::where('level', '<=', 12)->whereHas('user', function($user) {
$user->where('school_id', 3);
})->update(['level' => DB::raw("IF(level = 'F', 1, level+1)")]);
UserLevel::whereHas('user', function($user) {
$user->where('school_id', 3);
})->where('level', '>', 12)->delete();
If your datas is too huge. you can also use chunk to split them for reduce memory consumption.
like this:
UserLevel::where('level', '<=', 12)->whereHas('user', function($user) {
$user->where('school_id', 3);
})->chunk(5000, function($user_levels) {
$user_levels->update(['level' => DB::raw("IF(level = 'F', 1, level+1)")]);
});
UserLevel::whereHas('user', function($user) {
$user->where('school_id', 3);
})->where('level', '>', 12)->delete();

How to check with validate if a Value does exist per User that it is only once contained?

I am creating a coffee shop website because I want to learn Laravel and I like coffee.
I have a dashboard where the user can manage his coffee. He can manually select from all kind of coffee. With my approach, I detect 2 Major Problems which is connected to the question.
1. Problem
Let's say the User has added 2 coffees with the id of 1 and 2
id | user_id | selected_coffee | created_at | updated_at
----------------------------------------------------------------
1 | 1 | 1 | xxxxxxxxxx | xxxxxxxxxx
1 | 1 | 2 | xxxxxxxxxx | xxxxxxxxxx
If the User selects in these orders because he can select multiple. * Let's say he select the coffee id 1, 2 and the last one 3.*
Because the code detects that the first id is in the database, it is ignoring the other values.
2. Problem
Let's say the User1 has added 2 coffees with the id of 1 and 2
id | user_id | selected_coffee | created_at | updated_at
----------------------------------------------------------------
1 | 1 | 1 | xxxxxxxxxx | xxxxxxxxxx
1 | 1 | 2 | xxxxxxxxxx | xxxxxxxxxx
The User2 can select the coffee but it is not written in the Database which I need for display visually on the frontend
I know it is not well seen to add 2 Problems but my thought is if we can fix the number 1. The problem then the 2.Problem will disappear.
Here is my code which makes it unique:
public function store(Request $request)
{
app('App\Http\Controllers\ScoreController')->store($request);
$message = ['selected_coffee.unique' => 'Check Coffee ID already exist'];
$this->validate($request,[
'selected_coffee' => 'unique:manage_coffees',
],$message);
if($request->input('checkedDrink') != null){
foreach ($request->input('checkedDrink') as $selected_id) {
$manage_coffee = new ManageCoffee();
$manage_coffee->user_id = \Auth::user()->id;
$manage_coffee->selected_coffee = (int)$selected_id;
$manage_coffee->save();
}
}
return redirect('dashboard/coffee');
}
I found the validator in StackOverflow but it was not well explained. I think I must do a where clause in the ->validate but I don't know where I should put it.
Hopefully, you guys can help me.
Torsten
If I understood you correct you need exist rule but with additional condition like in example from official docs.
Something like that:
use Illuminate\Validation\Rule;
$this->validate($request,[
'selected_coffee' => [
Rule::exists('manage_coffees')->where(function ($query) {
$query->where('user_id', Auth::user()->id);
//you can specify any conditions you like
}),
],
],$message);

product filters with AND condition in eloquent laravel

I am working around with pivot table in Laravel Eloquent which is great but I am stuck in a point where I am unable to find the solution.
I have three tables shown below
## plant ##
id | name
1 | plant_1
2 | plant_2
3 | plant_3
seasons
id | season_name
1 | Summer
2 | Winter
3 | Mid-Summer
plant_to_season
id | plant_id | season_id
1 | 1 | 1
2 | 2 | 2
3 | 3 | 2
4 | 3 | 3
Now if I apply filters like
$filters = array(1,2,3);
\App\Plant::with(['seasons' => function($query) use ($filters) {
return $query->wherein('seasons.id', $filters);
}])->groupBy('id')->get();
This code returns me all three plants. But what I actually want is those plants which exactly have these 3 given filters.
I have searched everywhere from laravel documentation to Stackoverflow but there is no help regarding this.
This seems more tricky than expected, You should def use the whereHas method instead of with and then loop through your filters: Here is my suggestion on how to solve this, it works, but maybe there is a shorter solution:
$filters = array(1,2,3);
$outquery = \App\Plant::where('id','>',1);
foreach( $filters as $filter){
$outquery = $outquery->whereHas('seasons', function($query) use ($filter){
$query->where('id','=',$filter);
});
}
return $outquery->get();

Codeigniter - Getting Value from a Secondary Table

I have 3 tables in my DB: ‘workouts’, ‘exercises’ and ‘exercise_list’.
workouts: | id | datetime | misc1 | misc2 |
exercises: | id | ex_id | wo_id | weight | reps | wo_order |
exercise_list: | id | title |
So far I have generated a view which grabs details of a specific workout (myurl.com/workouts/view/<datetime>)
I have built a query that grabs the fields from ‘workouts’ and also it grabs any ‘exercises’ entries that correspond to that workout (by get_where using wo_id).
I build a view which lists the exercises for that workout, but I can only get as far as foreach’ing out the ‘id’ of the exercise. I need to somehow have a further query that grabs the ‘title’ of each exercise that is associated with that workout ‘id’.
So I currently have a table (html):
| Exercise | Weight | Reps |
| 1 | 50 | 8 | ...
I need ‘1’ to become the title of the exercise in ‘exercise_list’ with an ‘id’ of ‘1’.
My solution
May not be perfect but it works:
public function get_exercises($wo_id)
{
$this->db->select('exercises.wo_id,
exercises.weight,
exercises.reps,
exercise_list.title');
$this->db->from('exercises');
$this->db->join('exercise_list','exercises.ex_id= exercise_list.id');
$this->db->where('exercises.wo_id',$wo_id);
$q = $this->db->get();
$query = $q->result_array();
return $query;
}
Not sure about the bestway to do the last few lines. This is in my model, so I needed to return the array. I am going tobet there is a way to do it better than the last 3 lines.
You can use joins and select title from your exercise_list table
$this->db->select('w.*,el.title')
->from('workouts w')
->join('exercises e','w.id = e.wo_id')
->join('exercise_list el','el.id = e.ex_id')
->where('e.wo_id',$yourid)
->get()
->result();

Merge and Sort two Eloquent Collections?

I've two Collections and I want merge it to one variable (of course, with ordering by one collumn - created_at). How Can I do that?
My Controllers looks that:
$replies = Ticket::with('replies', 'replies.user')->find($id);
$logs = DB::table('logs_ticket')
->join('users', 'users.id', '=', 'mod_id')
->where('ticket_id', '=', $id)
->select('users.username', 'logs_ticket.created_at', 'action')
->get();
My Output looks for example:
Replies:
ID | ticket_id | username | message | created_at
1 | 1 | somebody | asdfghj | 2014-04-12 12:12:12
2 | 1 | somebody | qwertyi | 2014-04-14 12:11:10
Logs:
ID | ticket_id | username | action | created_at
1 | 1 | somebody | close | 2014-04-13 12:12:14
2 | 1 | somebody | open | 2014-04-14 14:15:10
And I want something like this:
ticket_id | table | username | message | created_at
1 |replies| somebody | asdfghj | 2014-04-12 12:12:12
1 | logs | somebody | close | 2014-04-13 12:12:14
1 | logs | somebody | open | 2014-04-14 11:15:10
1 |replies| somebody | qwertyi | 2014-04-14 12:11:10
EDIT:
My Ticket Model looks that:
<?php
class Ticket extends Eloquent {
protected $table = 'tickets';
public function replies() {
return $this->hasMany('TicketReply')->orderBy('ticketreplies.created_at', 'desc');
}
public function user()
{
return $this->belongsTo('User');
}
}
?>
A little workaround for your problem.
$posts = collect(Post::onlyTrashed()->get());
$comments = collect(Comment::onlyTrashed()->get());
$trash = $posts->merge($comments)->sortByDesc('deleted_at');
This way you can just merge them, even when there are duplicate ID's.
You're not going to be able to get exactly what you want easily.
In general, merging should be easy with a $collection->merge($otherCollection);, and sort with $collection->sort();. However, the merge won't work the way you want it to due to not having unique IDs, and the 'table' column that you want, you'll have to make happen manually.
Also they are actually both going to be collections of different types I think (the one being based on an Eloquent\Model will be Eloquent\Collection, and the other being a standard Collection), which may cause its own issues. As such, I'd suggest using DB::table() for both, and augmenting your results with columns you can control.
As for the code to achieve that, I'm not sure as I don't do a lot of low-level DB work in Laravel, so don't know the best way to create the queries. Either way, just because it's looking like starting to be a pain to manage this with two queries and some PHP merging, I'd suggest doing it all in one DB query. It'll actually look neater and arguably be more maintainable:
The SQL you'll need is something like this:
SELECT * FROM
(
SELECT
`r`.`ticket_id`,
'replies' AS `table`,
`u`.`username`,
`r`.`message`,
`r`.`created_at`
FROM `replies` AS `r`
LEFT JOIN `users` AS `u`
ON `r`.`user_id` = `u`.`id`
WHERE `r`.`ticket_id` = ?
) UNION (
SELECT
`l`.`ticket_id`,
'logs' AS `table`,
`u`.`username`,
`l`.`action` AS `message`,
`l`.`created_at`
FROM `logs` AS `l`
LEFT JOIN `users` AS `u`
ON `l`.`user_id` = `u`.`id`
WHERE `l`.ticket_id` = ?
)
ORDER BY `created_at` DESC
It's pretty self-explanatory: do the two queries, returning the same columns, UNION them and then sort that result set in MySQL. Hopefully it (or something similar, as I've had to guess your database structure) will work for you.
As for translating that into a Laravel DB::-style query, I guess that's up to you.

Categories