Traversing through collection and updating records using Eloquent - php

I am using eloquent to pull a list of App\Post's from my database and deleting their file contents from my server.
I am traversing through the list and processing a delete action, on success I want to store a value to the respective App\Post but I am getting this error:
Method save does not exist.
Here is my code:
$posts_to_purge = Post::where('post_date', '<', \Carbon\Carbon::now()->subDays(7)->toDateString())->get();
if(count($posts_to_purge) > 0)
{
foreach ($posts_to_purge as $post) {
$directory = storage_path() . '/' . $post->post_date;
$deleted_folder = File::deleteDirectory($directory);
if($deleted_folder)
{
// 'purged' is a column in my Post table
$post->purged = 1;
}
else
{
Log::error('Unable to delete \''.$directory.'\' directory');
}
} // end foreach loop
// Update collection with purged posts
$posts_to_purge->save();
}
A side question to this... is this the most efficient way to do this? As I only want to update SOME records in the collection.. should I create a separate collection and save that only?

Collections don't have really have the power to interact with the database in Laravel. You are able to define a custom collection class with a save() method however there is a simple way to do what you want.
$posts_to_purge = Post::where('post_date', '<', \Carbon\Carbon::now()->subDays(7)->toDateString())->get();
if(count($posts_to_purge) > 0)
{
foreach ($posts_to_purge as $post) {
$directory = storage_path() . '/' . $post->post_date;
$deleted_folder = File::deleteDirectory($directory);
if($deleted_folder)
{
// 'purged' is a column in my Post table
$post->purged = 1;
$post->save ();
}
else
{
Log::error('Unable to delete \''.$directory.'\' directory');
}
} // end foreach loop
// Update collection with purged posts
$posts_to_purge->save();
}
Yes this does do one query per updated model, however updates by primary key are very fast.
If you were super committed to using one query, you could simply save each updated Post's ID to an array and do something like Post::whereIn ('id', $updated_ids)->update(['purged' => 1]);

Related

Improve performance of multiple row insertions in Laravel

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.

How to find and merge two rows into one

I am new to Laravel so I am confused how to do this
I have a table called groups where I forget to use unique in validation . After Long time I found out there are multiple datas with same name for ex :
I have two datas with same name called Feminism but with different id
The group table have only name and description columns , but it has relation with products many to many I have other datas too which has same name I want to merge those datas into one . along with the relation to the product . Is is possible ?
Shall I have to check all the names manually like
Item::where('name','Feminism')->get();
and then merge those datas or we have some other relavant methods
You can refactor this code for your models and relations
foreach ($groups as $group) {
$duplicates = Group::where('name', $group->name)->where('id', '<>', $group->id)->get();
foreach ($duplicates as $duplicate) {
$mustBeUpdatedRelation = $duplicate->products;
$anotherRelation = $duplicate->anotherRelation;
foreach ($mustBeUpdatedRelation as $product) {
$product->categories()->detach($duplicate->id); // if product has pivot data you must get these and assign to new relation
//new relation
$product->categories()->attach($group->id);
}
$anotherRelation = $duplicate->anotherRelation;
foreach ($anotherRelation as $item) {
// like above
// detach duplicated
// attach unique
}
//finally you can delete or .... duplicated group
$duplicate->update([
'name' => "duplicated_$duplicate->name"
]);
}
}
You may use the following
$items = App\Item::with('products')->get();
$tempArr = [];
foreach ($items as $key => $item) {
if(isset($tempArr[$item->name])) {
$merged = $tempArr[$item->name]['products']->merge($item->products);
unset($tempArr[$item->name]['products']);
$tempArr[$item->name]['products'] = $merged;
} else {
$tempArr[$item->name] = $item;
}
}
foreach ($tempArr as $item) {
$item->products()->sync($item->products->pluck('id'));
}
$idsToKeep = array_column($tempArr, 'id');
App\Item::whereNotIn('id', $idsToKeep )->delete();
Use onDelete cascade when defining your pivot table migration.
This takes care of deleting the model’s relations for you:
e.g.
$table->foreign(’item_id’)
->references(’id’)->on(’items’)
->onDelete(’cascade’);

Insert only unique values into db table

I'm using vinkla/instagram composer package in my Laravel to fetch Instagram posts in my app.
Since Instagram allows the package to call their API 200 times per hour I'm trying to save post links into database table. And this package fetches 20 latest posts and its attributes.
Here I'm trying to compare the link of posts fetched by vinkla/instagram composer package and already saved posts and insert only unique into to table.
Below is the code snippet:
$instagram = new Instagram('access-token');
$posts = $instagram->media(); //gets 20 latest insta posts of a user
$fromDBs = Insta::orderBy('id', 'desc')->take(20)->get(); //get last 20 rows from table
foreach( $posts as $post)
{
foreach( $fromDBs as $fromDB)
{
if($post->images->low_resolution->url != $fromDB->link)
{
$create = new Insta;
$create->link = $post->images->low_resolution->url;
$create->save();
}
}
}
With the above stated code the new links are inserted x10 times.
What would be the correct way to insert unique link once only.
There are firstOrCreate or firstOrNew helper functions on eloquent, so you can create it only if it does not exist in order to prevent duplicates. So instead of your check you can check this code:
foreach( $posts as $post)
{
Insta::firstOrCreate(['link' => $post->images->low_resolution->url]);
}
...
foreach( $posts as $post)
{
foreach( $fromDBs as $fromDB)
{
if($post->images->low_resolution->url != $fromDB->link)
{
$create = Insta::firstOrNew(['link' => $post->images->low_resolution->url]);
if(! $create->id ) $create->save();
}
}
}

Laravel trying to check if the user has relationship with post

I have posts and these posts can be saved by users to read later. I created this relation and I can save or delete them easily. The problem is I can't check if the post is saved or not in frontend. Now I wrote some code to handle this but it doesn't seem to work. here is my controller code:
$articleFlag = 1;
$userID = Auth::User()->id;
if (count($bestarticles) > 0) {
foreach ($bestarticles as $bestarticle) {
$saveddata = DB::table('savearticle')->where('user_id', $userID && 'article_id', $bestarticle);
if (count($saveddata) > 0) {
$articleFlag = 1;
} else {
$articleFlag = 2;
}
} //foeach endes here
} //first if endes here
and than I pass the $articleFlag to the view than checking it's value with an if
But the problem is, no matter what I do if (count($bestarticles) > 0) returns true and I get value 1 in view.
Does anybody have any idea what I might be missing?
Here is my user controller relationshio:
function savedarticle(){
return $this->belongsToMany('App\User', 'savearticle', 'user_id',
'article_id');
}
and here goes the functions that i use for saving and deleting:
function savethearticle(Article $article){
$this->savedarticle()->syncWithoutDetaching([$article->id]);
}
function removethearticle(Article $article){
$this->savedarticle()->detach([$article->id]);
}
But there is nothing you need to worry about. I'm able to delete and add.
Or is there another way to check for existing relationship in view or a better way to check it in controller and pass into view?
I am using Laravel 5.4.
It looks as though you have a Collection of Article models, and you're trying to determine whether it is related to the User or not.
If that's the case, I would suggest eager loading the User relation when you originally query the Article models. This has the advantage of using one query to load the relationship, rather than one per Article.
$userId = Auth::id();
$articles = Article::with(['savedarticle' => function ($query) use ($userId) {
return $query->where('user_id' => $userId);
}])->get();
With this Collection, because we have loaded specifically the currently authenticated User, you can then proceed knowing that if the savedarticle relation has a count of 1, that the User relation exists.
foreach ($articles as $article) {
if ($article->savedarticle->count()) {
// User has already saved article
} else {
// User has not saved article
}
}
Should you not be passing the id of bestarticle in the Where clause? Also, it requires a ->get() to actually fire the request off to the database and run the query.
$saveddata = DB::table('savearticle')->where('user_id', $userID && 'article_id', $bestarticle);
Should be
$saveddata = DB::table('savearticle')->where('user_id', $userID && 'article_id', $bestarticle->id)->get();

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