Improve performance of multiple row insertions in Laravel - php

I have a controller API method where I insert many rows (around 4000 - 8000), before inserting a new row I also check if a venue with the same ame was added already in the zone sothat's another Elouent call, my issue is I usually get timeout errors becuase the row inserting takes too much, I use set_time_limit(0) but this seems too hacky.
I think the key is the validation check I do before inserting a new row.
//Check if there is a venue with same name and in the same zone already added
$alreadyAdded = Venue::where('name', $venue['name'])->whereHas('address', function ($query) use ($address){
$query->where('provinceOrState' , $address['provinceOrState']);
})->orWhere('venueId',$venue['venueId'])->first();
Is there a way I can improve the performance of this method ? This is my complete method call:
public function uploadIntoDatabase(Request $request)
{
set_time_limit(0);
$count = 0;
foreach($request->input('venuesToUpload') as $index => $venue)
{
//Check if there is a venue with same name and in the same zone already added
$alreadyAdded = Venue::where('name', $venue['name'])->whereHas('address', function ($query) use ($address){
$query->where('provinceOrState' , $address['provinceOrState']);
})->orWhere('venueId',$venue['venueId'])->first();
if(!$alreadyAdded)
{
$newVenue = new Venue();
$newVenue->name = $venue['name'];
$newVenue->save();
$count++;
}
}
return response()->json([
'message' => $count.' new venues uploaded to database',
]);
}

use only one request to add the venues
$newVenues = [];
$count = 0;
foreach($request->input('venuesToUpload') as $index => $venue) {
//Check if there is a venue with same name and in the same zone already added
$alreadyAdded = Venue::where('name', $venue['name'])->whereHas('address', function ($query) use ($address){
$query->where('provinceOrState' , $address['provinceOrState']);
})->orWhere('venueId',$venue['venueId'])->count();
if(!$alreadyAdded) {
$newVenues [] = ['name' => $venur['name'];
}
}
if ($newVenues) {
$count = count($newVenues);
Venue::insert($newVenues);
}
As for the verification part, change the first to count cause you dont need to recover the data, just the information that it exists. And since you're verifying with both name and id, you can do some custom query that verifies all values in one query using a static table made from the request inputs and joining on the existing venues table where venues.id = null.

Related

How do I remove duplicate rows with same column values in Laravel?

I'm trying to make a artisan command in Laravel to remove all venues that have the same address and leave the one with the lowest ID number (so first created).
For this I need to check 3 fields: 'street', 'house_number', 'house_number_addition'
This is how far I've got:
$venues = Venue::select('street', 'house_number', 'house_number_addition', DB::raw('COUNT(*) as count'))
->groupBy('street', 'house_number', 'house_number_addition')
->having('count', '>', 1)
->get();
foreach ($venues as $venue) {
$this->comment("Removing venue: {$venue->street} {$venue->house_number} {$venue->house_number_addition}");
$venue->delete();
}
Only the delete is not working but is also not giving an error.
To be able to delete an item, Eloquent needs to know it's id. If you make sure your models' id is queried, you can call delete() without issues.
In your query, however, that won't work because you have a GROUP_BY statement, so SQL doesn't allow you to select the id column (see here).
The easiest solution here is to utilize Eloquent's Collection class to map over the models, something like:
$uniqueAddresses = [];
Venue::all()
->filter(function(Venue $venue) use (&$uniqueAddresses) {
$address = sprintf("%s.%s.%s",
$venue->street,
$venue->house_number,
$venue->house_number_addition);
if (in_array($address, $uniqueAddresses)) {
// address is a duplicate
return $venue;
}
$uniqueAddresses[] = $address;
})->map(function(Venue $venue) {
$venue->delete();
});
Or, to make your delete query a little more efficient (depending on how big your dataset is):
$uniqueAddresses = [];
$duplicates = [];
Venue::all()
->map(function(Venue $venue) use (&$uniqueAddresses, &$duplicates) {
$address = sprintf("%s.%s.%s",
$venue->street,
$venue->house_number,
$venue->house_number_addition);
if (in_array($address, $uniqueAddresses)) {
// address is a duplicate
$duplicates[] = $venue->id;
} else {
$uniqueAddresses[] = $address;
}
});
DB::table('venues')->whereIn('id', $duplicates)->delete();
Note: the last one will permanently delete your models; it doesn't work with Eloquent's SoftDeletes functionality.
You could, of course, also write a raw query to do all this.

Laravel, copy row to another table with eloquent relationship

I want to get the data form database table and create a new row in another table.
Which 1 PO have many PoProducts.
$_getPO = Order::find($id);
$_getPOProducts= OrderProducts::where('order_id', $id)->get();
$order_no = $_getPO->order_no;
$eta = $_getPO->eta;
$_Order = new DeliveryOrders();
$_Order->order_no = $order_no;
$_Order->eta = $eta;
$_Order->save();
$POProduct = array();
foreach($_getPOProducts as $i => $_getPOProduct)
{
$POProduct[] = new DeliveryOrderProducts();
$POProduct[] = $_getPOProduct->order_id;
$POProduct[] = $_getPOProduct->item_id;
$POProduct[] = $_getPOProduct->qty;
$POProduct->save();
}
But, this output an error.
Call to a member function save() on array
Please help me. Thanks.
If you wish to copy records from one table to another or just duplicate a record in the same table you could simply use the repliacate() method.
$user = User::findOrFail($id);
// replicate (duplicate) the data
$staff = $user->replicate();
// make into array for mass assign.
//make sure you activate $guarded in your Staff model
$staff = $staff->toArray();
Staff::firstOrCreate($staff);
Note: in case you're only duplicating on the same table replace Staff with User on this example.
You are trying to run the save method on the array but what you want is to use it on the array index instead.
Change your foreach to this and it should work (assuming columns are the same).
foreach($_getPOProducts as $i => $_getPOProduct)
{
$POProduct[$i] = new DeliveryOrderProducts();
$POProduct[$i]->order_id = $_getPOProduct->order_id;
$POProduct[$i]->item_id = $_getPOProduct->item_id;
$POProduct[$i]->qty = $_getPOProduct->qty;
$POProduct[$i]->save();
}
You can shorten this by using forceCreate.
foreach($_getPOProducts as $i => $_getPOProduct)
{
$POProduct[$i] = (new DeliveryOrderProducts())->forceCreate($_getPOProduct->only(['order_id', 'item_id', 'qty']));
}

Retrieving data from database using multiple ids - Laravel

I have list of clients in my database with with unique ids. I am trying to retrieve these customers using these unique ids like this .
I want to get each clients name and phone and so i can process a message to them respectively. But with my query, i am only getting only one client and not all of my clients.
PS: when i return $explode, i am able to get all the ids i have selected.
public function getCustomers(Request $request)
{
$ids = $request->ids;
$explode = explode(",",$ids);
if(request()->ajax())
{
$clients = Client::whereHas('product', function($find_clients)use($explode)
{
$find_clients->where('id',$explode);
})->get();
$get_customer_name = [];
$get_customer_phone = [];
foreach($clients as $key => $client)
{
$get_customer_name[] = $client->name;
$get_customer_phone [] = $client->phone;
return ['success' => $explode];
}
}
}
SMS query
$query = "?key=$api_keyto=$implode(',',$$get_customer_phone)&msg=Dear ".$implode(',',$$get_customer_name)."Thank you";
To find clients with an array of ids, try using whereIn instead of where.
Like so:
$find_clients->whereIn('id',$explode);
Try using whereIn instead of where
$find_clients->whereIn('id', $explode);

what is a good way to save an array data to mySQL in laravel

Its an online result marker. Once the user clicks save it gets the CA(Continious assessment) and it gets the Exam marks and the teachers remark for the particular student. I want to know how i can insert these fields into my database the right way.
return $request->all();
the above code returns the image below
$i = 0;
foreach($request->id as $id) {
$model = new Model;
$model->user_id = $id;
$model->ca_mark = $request->ca_mark[$i];
$model->exam_mark = $request->ca_mark[$i];
$model->remarks = $request->remarks[$i];
$model->save();
$i++;
}
Query Builder
$i = 0;
foreach($request->id as $id) {
DB::table('table')->insert([
'user_id' => $id,
'ca_mark' => $request->ca_mark[$i],
'exam_mark' => $request->ca_mark[$i],
'remarks' => $request->remarks[$i]
]);
$i++;
}
The above code has been written based on an assumption of the database table structure. In short, foreach on the ID's as this would be the student ID, and take the ca_mark, exam_mark and remark based on the key of the id.

Laravel - efficient way to check database parameters before insert

I have populate a form of which every text field generated is based on the database result. I simply name every text field using the id. Now when the form is filled, I use controller to save it. But prior to insert the database, I loop the Request::input() to check every item whether such entry is exist or not. I just wonder if there is efficient way to check every item in the loop to insert it into db. Here is my code
public function store(Request $request, $id, $inid)
{
$startOfDay = Carbon::now()->startOfDay();
$endOfDay = Carbon::now()->endOfDay();
$instruments = InstrumentReading::whereBetween('created_at', [$startOfDay, $endOfDay])
->where('iv_inid', '=', $inid)
->get();
foreach ($request->input() as $k => $v) {
$read = new InstrumentReading;
$read->iv_inid = $inid;
$read->iv_ipid = $k;
$read->iv_usid = Auth::user()->id;
$read->iv_reading = $v;
$read->save();
}
if ($instruments->count() > 0) {
//to filter the iv_ipid...
foreach($instruments as $instrument)
{
$instrument->iv_status = "VOID";
$instrument->save();
}
}
}
In words of efficent approach what you can do is to simple check / fetch ONLY all posible rows from the database, and the check in the loop if the row was already inserted. Also fetch only iv_ipid column, as we do not need all columns from the table to do our check. It will be faster to select only the column we need. You can use directly Fluent (Query Builder) over Eloquent to pull the data from database as it greatly increase the performance for a simple query like this.
public function store(Request $request, $id, $inid)
{
// Search only records with submitted iv_ipid, iv_inid and created today
$alreadyInserted = DB::table('instrument_readings')
->whereBetween('created_at', [
Carbon::now()->startOfDay(),
Carbon::now()->endOfDay()
])
// Get only records with submitted iv_ipid
->whereIn('iv_ipid', array_keys($request->input()))
// Get records with given iv_inid only
->where('iv_inid', $inid)
// For our check we need only one column,
// no need to select all of them, it will be fast
->select('iv_ipid')
// Get the records from DB
->lists('iv_ipid');
foreach ($request->input() as $k => $v) {
// Very simple check if iv_ipid is not in the array
// it does not exists in the database
if (!in_array($k, $alreadyInserted)) {
$read = new InstrumentReading;
$read->iv_inid = $inid;
$read->iv_ipid = $k;
$read->iv_usid = Auth::user()->id;
$read->iv_reading = $v;
$read->save();
} else {
//todo
}
}
This is the most efficent way suggested until now, because you fetch at once only the records you are interested in, not all records from today. Also you fetch only one column, the one that we need for out check. Eloquent ususlally give a lot of overheat on the perfomance, so in the suggested code I use directly Fluent, which will boost the speed this part of code is executed by ~ 20%.
Your mistake in the original code is that you are doing database call each time in a loop. When you need such a simple task as a check, never put database calls, queries etc. in a loop. It is an overkill. Instead select all needed data before the loop and then do your checks.
Now this is in case you only need to save new records to database. In case you want to manipulate each record in the loop, let's say you need to loop through each submited entry, get get the model or create it if it does not exists and then do something else with this model, the most efficent way then will be this one:
public function store(Request $request, $id, $inid)
{
foreach ($request->input() as $k => $v) {
// Here you search for match with given attributes
// If object in DB with this attributes exists
// It will be returned, otherwise new one will be constructed
// But yet not saved in DB
$model = InstrumentReading::firstOrNew([
'iv_inid' => $inid,
'iv_ipid' => $k,
'iv_usid' => Auth::user()->id
]);
// Check if it is existing DB row or a new instance
if (!$model->exists()) {
// If it is a new one set $v and save
$model->iv_reading = $v;
$model->save();
}
// Do something with the model here
.....
}
This way Laravel will check if model with the passed parameters already exist in database, and if so it will return it for you. If it does not exist, it will create new instance of it, so you can then set the $v and save to db. So you are good to go to do anything else with this model and you can be sure it exists in database after this point.
First approach (efficiency first)
Consider using a simple SQL INSERT IGNORE Query and use Fluent, i.e.:
Make a composite unique key containing:
iv_inid
iv_ipid
created_time, up to an hour granularity, this is important, because created_at might have a far greater granularity than your intended purpose, and might slow things down a bit.
Use DB, i.e.:
DB::query(
"INSERT IGNORE INTO $yourTable VALUES ( ... )"
);
Pros:
- Extremely fast, all the necessary checking is done on the DB Server
Cons:
- You cannot know which values triggered a duplicate value / unique key violation, as related errors are treated as warnings.
Second approach (convenience first)
Use firstOrFail, i.e.:
$startOfDay = Carbon::now()->startOfDay();
$endOfDay = Carbon::now()->endOfDay();
// ... for
try {
InstrumentReading::where('iv_inid', $inid)
->where('iv_ipid', $k)
->whereBetween('created_at', [$startOfDay, $endOfDay])
->firstOrFail();
continue;
} catch (ModelNotFoundException $e) {
$instrumentReading = InstrumentReading::create([
// your values
]);
}
// ... endfor
Pros:
- Easy to implement
Cons:
- Somewhat slower than simple queries
Your code will send request to database every time you need to check the value. Instead, search all value of this day then check the value. This approach will send request to database only one time.
$startOfDay = Carbon::now()->startOfDay();
$endOfDay = Carbon::now()->endOfDay();
// Search only this day
$instruments = InstrumentReading::whereBetween('created_at', [$startOfDay, $endOfDay])->get();
foreach($instruments as $instrument)
{
// Check the value
}

Categories