Fetching a cached Eloquent collection of 10 items times out - laravel

I'm building a search functionality that returns large collections which are paginated using a LengthAwarePaginator. I'm trying to cache results using a key called $searchFilter_$query_$offsetPages for a single page of returned results (10 items). It goes into the cache just fine. However, it times out when I try to check using Cache::has($key) or fetch using Cache::get($key).
The same problem occurs in the browser as well as in artisan Tinker. Strangely, when I put a random set of 10 items into the cache in Tinker and fetch them back, everything works fine. I'm using Redis as the cache driver.
Here is my controller method:
public function search($filter, $query, $layout, Request $request) {
if($layout == "list-map") {
return view("list-map")->with(['filter' => $filter, 'query' => $query, 'layout' => 'list-map']);
} else {
$offsetPages = $request->input('page', 1) - 1;
$cacheKey = $filter . "_" . $query . "_" . $offsetPages;
if(Cache::has($cacheKey)) {
\Log::info("fetching results from cache");
$data = Cache::get($cacheKey);
$totalCt = $data[0];
$results = $data[1];
} else {
$results = $this->getResults($filter, $query);
$totalCt = $results->count();
$results = $results->slice($offsetPages, $this->resultsPerPage);
\Log::info("caching results");
Cache::put($cacheKey, [$totalCt, $results], 5);
}
$results = new LengthAwarePaginator($results,
$totalCt,
$this->resultsPerPage,
$request->input('page', 1),
['path' => LengthAwarePaginator::resolveCurrentPath()]
);
return view($layout)->with(['filter' => $filter, 'query' => $query, 'layout' => $layout, 'results' => $results]);
}
}

So, the issue was that many of the models in the collection returned from my getResults() method were obtained via relationship queries. When I would dd($results) on the single page of 10 results, I could see that there was a "relations" field on each model. Inside that array were thousands of recursively related models based on the relationship I originally queried. I was unable to find any information about an option to not eager load these related models. Instead I came up with a bit of a hacky workaround to fetch the models directly:
$results = $results->slice($offsetPages, $this->resultsPerPage);
//load models directly so they don't include related models.
$temp = new \Illuminate\Database\Eloquent\Collection;
foreach($results as $result) {
if(get_class($result) == "App\Doctor") {
$result = Doctor::find($result->id);
} else if(get_class($result == "App\Organization")) {
$result = Organization::find($result->id);
}
$temp->push($result);
}
$results = $temp;
\Log::info("caching results");
Cache::put($cacheKey, [$totalCt, $results], 5);
If anyone knows the best practice in this situation, please let me know. Thanks!
Edit:
I've found a better solution instead of the above workaround. If I query my relationships like this: $taxonomy->doctors()->get() rather than $taxonomy->doctors, it does not load in the huge recusive relations.

I dont really see why your code doesn't work. The only potential problems I see are the cache keys, which could contain problematic characters, as well as the way you check for a cached value. As you are using Cache::has($key) before Cache::get($key), you could end up with a race condition where the first call returns true and the latter null because the cached value timed out just between the two calls.
I tried to address both issues in the following snippet:
public function search($filter, $query, $layout, Request $request)
{
if($layout == "list-map") {
return view("list-map")->with(['filter' => $filter, 'query' => $query, 'layout' => 'list-map']);
} else {
$offsetPages = $request->input('page', 1) - 1;
$cacheKey = md5("{$filter}_{$query}_{$offsetPages}");
$duration = 5; // todo: make this configurable or a constant
[$totalCount, $results] = Cache::remember($cacheKey, $duration, function () use ($filter, $query) {
$results = $this->getResults($filter, $query);
$totalCount = $results->count();
$filteredResults = $results->slice($offsetPages, $this->resultsPerPage);
return [$totalCount, $filteredResults];
});
$results = new LengthAwarePaginator($results,
$totalCount,
$this->resultsPerPage,
$request->input('page', 1),
['path' => LengthAwarePaginator::resolveCurrentPath()]
);
return view($layout)->with(compact('filter', 'query', 'layout', 'results'));
}
}
The inbuilt function Cache::remember() doesn't use Cache::has() under the hood. Instead, it will simply call Cache::get(). As this function will return null as default if no cache was hit, the function can easily determine if it has to execute the closure or not.
I also wrapped the $cacheKey in md5(), which gives a consistently valid key.
Looking at the following part of your code
$results = $this->getResults($filter, $query);
$totalCount = $results->count();
$filteredResults = $results->slice($offsetPages, $this->resultsPerPage);
I am quite sure the whole search could be improved (independently of the caching). Because it seems you are loading all results for a specific search into memory, even if you throw away most parts of it. There is certainly a better way to do this.

Related

Laravel 9 and Livewire 2 multiple storeAs methods throwing "Call to a member function storeAs() on null" error

I have 6 distinct images that a user needs to upload, so I have 6 different storeAs methods. When I have just one, everything works without fault, but 2 or more, I get a Call to a member function storeAs() on null error. Honesty, my code looks repetitive and dirty, so I'm not surprised.
public function createTentry($id)
{
$trial = Trial::find($id);
$image_plant_general = $this->image_plant_general->storeAs('/', $this->trial->id.'_'.$this->id.'_'.now()->timestamp.'.'.$this->image_plant_general->getClientOriginalExtension(), 'trial-entry-photos');
$image_plant_closeup = $this->image_plant_closeup->storeAs('/', $this->trial->id.'_'.$this->id.'_'.now()->timestamp.'.'.$this->image_plant_closeup->getClientOriginalExtension(), 'trial-entry-photos');
$image_fruit_in_plant = $this->image_fruit_in_plant->storeAs('/', $this->trial->id.'_'.$this->id.'_'.now()->timestamp.'.'.$this->image_fruit_in_plant->getClientOriginalExtension(), 'trial-entry-photos');
$image_fruit_in_plant_closeup = $this->image_fruit_in_plant_closeup->storeAs('/', $this->trial->id.'_'.$this->id.'_'.now()->timestamp.'.'.$this->image_fruit_in_plant_closeup->getClientOriginalExtension(), 'trial-entry-photos');
$image_fruit_in_harvest_single = $this->image_fruit_in_harvest_single->storeAs('/', $this->trial->id.'_'.$this->id.'_'.now()->timestamp.'.'.$this->image_fruit_in_harvest_single->getClientOriginalExtension(), 'trial-entry-photos');
$image_fruit_in_harvest_group = $this->image_fruit_in_harvest_group->storeAs('/', $this->trial->id.'_'.$this->id.'_'.now()->timestamp.'.'.$this->image_fruit_in_harvest_group->getClientOriginalExtension(), 'trial-entry-photos');
Tentry::create([
...
'image_plant_general' => $image_plant_general,
'image_plant_closeup' => $image_plant_closeup,
'image_fruit_in_plant' => $image_fruit_in_plant,
'image_fruit_in_plant_closeup' => $image_fruit_in_plant_closeup,
'image_fruit_in_harvest_single' => $image_fruit_in_harvest_single,
'image_fruit_in_harvest_group' => $image_fruit_in_harvest_group,
]);
return redirect()->route('trial.show', [$trial->evaluation_id, $trial->id]);
}
I would recommend that you use an array and a loop, rather than checking individual properties. Makes for cleaner code, and easier to check the same thing over and over.
Another thing, I added model-route-binding, so that the Trial model is injected as a parameter directly without having to look it up explicitly.
public function createTentry(Trial $trial)
{
$prefix = $this->trial->id.'_'.$this->id.'_'.now()->timestamp.'.';
$images = [
'image_plant_general',
'image_plant_closeup',
'image_fruit_in_plant',
'image_fruit_in_plant_closeup',
'image_fruit_in_harvest_single',
'image_fruit_in_harvest_group'
];
$data = [
// Other Tentry-fields here
];
foreach ($images as $image) {
$data[$image] = $this->{$image}
? $this->{$image}->storeAs('/', $prefix.$this->image_plant_general->getClientOriginalExtension(), 'trial-entry-photos')
: null;
}
Tentry::create($data);
return redirect()->route('trial.show', [$trial->evaluation_id, $trial->id]);
}

Laravel: Cache with pagination clear issue

I have laravel (7.x) application. I recently added the cache functionality for the performance boost. After implementing the cache functionality, I was having trouble with the pagination while loading the data in grid format, so I googled for the solution and found this Pagination with cache in Laravel.
Although, it did solve my problem. But, the case is that I have about 100 pages and due to the solution I found, each page has it's own cache. Now, if I create or update any record then it doesn't reflect in the grid because the data is loaded from the cache.
PostController.php:
...
$arraySearch = request()->all();
# calculating selected tab
$cache = (!empty(request()->inactive)) ? 'inactive' : 'active';
$cacheKey = strtoupper("{$this->controller}-index-{$cache}-{$arraySearch['page']}");
# caching the fetch data
$arrayModels = cache()->remember($cacheKey, 1440, function() use ($arraySearch) {
# models
$Post = new Post();
# returning
return [
'active' => $Post->_index(1, 'active', $arraySearch),
'inactive' => $Post->_index(0, 'inactive', $arraySearch),
];
});
...
Post.php:
public function _index($status = 1, $page = null, $arraySearch = null)
{
...
$Self = self::where('status', $status)
->orderBy('status', 'ASC')
->orderBy('title', 'ASC')
->paginate(10);
...
return $Self;
}
How do I clear all this cache to show the newly created or updated record to with the updated values.?
1. Store All pages under the same tag:
As seen on the documentation: https://laravel.com/docs/master/cache#storing-tagged-cache-items
You can use tags to group cached items.
$cacheTag = strtoupper("{$this->controller}-index-{$cache}");
$arrayModels = cache()->tags([$cacheTag])->remember($cacheKey, 1440, function() use ($arraySearch) {
...
2. Set an event listener on Post to clear the tag
You can run an Event listener on your Post update() or create() events.
https://laravel.com/docs/7.x/eloquent#events-using-closures
You can then clear the tag cache using
Cache::tags([$cacheTag])->flush();
I know this isn't the proper solution. But, until I find the proper way to do it, this is the option I am kind of stuck with.
PostController.php:
public function index()
{
...
$arraySearch = request()->all();
# calculating selected tab
$cache = (!empty(request()->inactive)) ? 'inactive' : 'active';
$cacheKey = strtoupper("{$this->controller}-index-{$cache}-{$arraySearch['page']}");
# caching the fetch data
$arrayModels = cache()->remember($cacheKey, 1440, function() use ($arraySearch) {
# models
$Post = new Post();
# returning
return [
'active' => $Post->_index(1, 'active', $arraySearch),
'inactive' => $Post->_index(0, 'inactive', $arraySearch),
];
});
...
}
public function store()
{
...
Artisan::call('cache:clear');
...
}
I'll post the proper solution when I find one. Till then I am using this one.
There is a method in Laravel Model class called booted (not boot, which is having a different purpose). This method runs every time something is "saved" (including "updated") or "deleted".
I have used this as following (in a Model; or a Trait, included in a Model):
protected static function booted(): void
{
$item = resolve(self::class);
static::saved(function () use ($item) {
$item->updateCaches();
});
static::deleted(function () use ($item) {
$item->updateCaches();
});
}
"updateCaches" is a method in the Trait (or in the Model), that can have the code to update the cache.

Is there any issue in each loop to break it and not create models?

Is there any problem in following code, I tried so hard but got no answer.
I want to create models in each iteration from loop in filtering dom.
$node->filter('div.panel-section.font-size-2.font-size-xxs-normal .row')->each(function (Crawler $nestedNode, $i) use (&$part) {
$model_category = ModelCategory::firstOrCreate(['name' => $nestedNode->filter('.bold.mb-2 a')->first()->text()]);
$part->modelCategories()->sync($model_category, false);
$nestedNode->filter('.col-xs-6.col-md-3')->each(function (Crawler $columns, $i) use (&$model_category) {
$model = Model::Create(['name' => $columns->filter('a')->first()->text()]);
$model->category()->associate($model_category);
$model->save();
});
});
I have one to many relation between Model and ModelCategory
I found issue after sometimes looking and some searching. It's because of sync syntax. sync function in eloquent relationship get array of ids as first argument but I passed an object to it. Below is the corrected piece of code:
$node->filter('div.panel-section.font-size-2.font-size-xxs-normal .row')->each(function (Crawler $nestedNode, $i) use (&$part) {
$model_category = ModelCategory::firstOrCreate(['name' => $nestedNode->filter('.bold.mb-2 a')->first()->text()]);
$part->modelCategories()->sync([$model_category->id], false);
$nestedNode->filter('.col-xs-6.col-md-3')->each(function (Crawler $columns, $i) use (&$model_category) {
$model = Model::Create(['name' => $columns->filter('a')->first()->text()]);
$model->category()->associate($model_category);
$model->save();
});
});

Laravel 5.4 Return as array not object

The following method is intended to return an array with another array, 'data' and an Object (The result of some eloquent query).
It is however returning an array with two objects in it; $data is somehow being converted to an object with multiple child-objects, rather than being an array of objects. It should be noted that a dd($data) before the return statement reveals that it is indeed an array of objects. I think that somehow the Laravel middleware that handles response is returning this as an object instead...
Any idea how to work around this?
public function getTestData($id) {
$participants = Participant::where('test_id', $id)->with('testRecords')->get();
$finalRecordValue = TestRecord::where('test_id', $id)->orderBy('created_at', 'desc')->first();
$data = [];
foreach ($participants as $participant) {
foreach ($participant->testRecords as $testRecord) {
if (!array_key_exists((int)$testRecord->capture_timestamp, $data)) {
$data[$testRecord->capture_timestamp] = (object)[
'category' => $testRecord->capture_timestamp,
'value' . "_" . $participant->id => $testRecord->score
];
} else {
$data[$testRecord->capture_timestamp]->{"value" . "_" . $participant->id} = $testRecord->score;
}
}
}
return [$data, Auth::user()->tests()->findOrFail($id)];
}
Try this before excuting return sentence or in it:
array_values($data);

Yii2 ActiveRecord cache

How to use ActiveRecotd cache for Yii 2? I did't find any examples in official docs. In Google I found 2 examples, first is:
$db = self::getDb();
$object = $db->cache(function ($db) use($id) {
return self::findOne($id);
});
But it doesn't work for Model, I tested with updated framework. Other example is:
$data = \Yii::$app->cache->get('some_var_' . $id);
if ($data === false)
{
$data = self::findOne($id);
\Yii::$app->cache->set('some_var_' . $id, $data, 60);
}
It's working fine, but it's not ActiveRecord caching it's data caching, So we haven't got ActiveRecord caching in Yii 2?
1) Use cache like that:
$db = Yii::$app->db;// or Category::getDb()
$result = $db->cache(function ($db) use ($id) {
return Category::find()->where(['id' => $id])->all();
}, CACHE_TIMEOUT);
2) If you may use query dependency, use like that:
$db = Yii::$app->db;// or Category::getDb()
$dep = new DbDependency();
$dep->sql = 'SELECT count(*) FROM category';
$result = $db->cache(function ($db) use ($id) {
return Category::find()->where(['id' => $id])->all();
}, CACHE_TIMEOUT, $dep);
I too am having trouble with this. Here's my workaround for the time being for a hasOne() relationship.
public function getGroup()
{
if(isset(static::$_getGroup[$this->id])) {
return static::$_getGroup[$this->id];
}
$Group = $this->hasOne(BillChargesGroup::className(), ['id' => 'group_id'])->one();
static::$_getGroup[$this->id] = $Group;
return $Group;
}
I only want to cache data for the current request, so this works. However because I'm using ->one(); it does not return the ActiveQuery object if we call $model->getGroup() (which I found is good for extending queries)
Unfortunately if I do return the ActiveQuery object, Yii2 does some "magic" on it and always does a SELECT * which I can't control.
Since 2.0.14 you can use the following shortcuts:
(new Query())->cache(7200)->all();
// and
User::find()->cache(7200)->all();
Source: https://www.yiiframework.com/doc/guide/2.0/en/caching-data

Resources