Reputation: 143
We have a Livewire component linked to a view, intended to display a data-table with more than 100,000 records, each one with many (up to 300) child records displayed, from several relationships (mostly one-to- many and many-to-many). That heavy query, to get the Eloquent collection (paginated in chunks of 15), now takes about 15-20 secs to complete, after a long optimization and debugging work. The view is also intended to allow CRUD operations, by means of activating modal windows. The main problem we need to solve is the big delay to show the add or edit modal window. At that point, we don't need to run the data-table query again, since the underlying records haven't changed yet. The main queries should run on the initial load only, or after record adding or updating, when perhaps the paginated data-table has changed.
This is an excerpt from the component, showing the relevant code:
public function render()
{
//******************** SECTION A ***************
//This is the eloquent collection we use to generate the data-table in the view
//$q is a public property, binded to a input box, for searching purposes. $sortBy and $sortAsc are used to order the data-table.
$fonos = Fono::select('id','cod','titulo','sello_cod','ritmo_cod','propiedad_cod')
->with(['propiedad:id,cod,propiedad','sello:id,cod,sello','ritmo:id,cod,ritmo','artistas:id,cod,artista','autores:id,cod,autor','operadores:id,cod,operador','isrcs:id,isrc']) //Eager loading relationships to reduce queries (only required fields)
->when( $this->q, function($query) {
return $query->where(function( $query) {
$query->where('titulo', 'like', '%'.$this->q . '%')
->orWhere('cod', 'like', '%' . $this->q . '%')
->orWhere('isrc', 'like', '%' . $this->q . '%')
->orWhere('famoso_por', 'like', '%' . $this->q . '%')
->orWhereHas('sello', function ( $query) {
$query->where('sello', 'like', '%' . $this->q . '%');})
->orWhereHas('autores', function ( $query) {
$query->where('cod', 'like', '%' . $this->q . '%');})
->orWhereHas('autores', function ( $query) {
$query->where('autor', 'like', '%' . $this->q . '%');})
->orWhereHas('artistas', function ( $query) {
$query->where('cod', 'like', '%' . $this->q . '%');})
->orWhereHas('artistas', function ( $query) {
$query->where('artista', 'like', '%' . $this->q . '%');})
->orWhereHas('operadores', function ( $query) {
$query->where('cod', 'like', '%' . $this->q . '%');})
->orWhereHas('operadores', function ( $query) {
$query->where('operador', 'like', '%' . $this->q . '%');})
->orWhereHas('isrcs', function ( $query) {
$query->where('isrc', 'like', '%' . $this->q . '%');})
;
});
})
->orderBy($this->sortBy, $this->sortAsc ? 'ASC' : 'DESC')->paginate(15);
//******************** SECTION B ***************
//Here we load other Eloquent models (some with thousands of records), that are used to populate several selects (drop-down lists), that are visible when the modal window is activated to add or edit a record.
$tipos_fonos = TipoFono::select('cod','tipo_fono')->get();
$sellos = Sello::select('cod','sello')->get();
$ritmos = Ritmo::select('cod','ritmo')->get();
$generos = Genero::select('cod','genero')->get();
$idiomas = Idioma::select('id','idioma')->get();
$propiedades = Propiedad::select('cod','propiedad')->get();
$territorios = Territorio::select('cod','territorio')->get();
$clases_artistas = ClaseArtista::select('id','clase_artista')->get();
$artistasLista = Artista::select('cod','artista')->get();
$papelesLista = PapelArtista::select('id','papel_artista')->get();
$autoresLista = AutorFono::select('cod','autor')->get();
$operadoresLista = Operador::select('cod','operador')->get();
$tipos_autores = TipoAutor::select('id','cod','tipo_autor')->get();
//******************** SECTION C ***************
//Finally, we pass the objects to the view
return view('livewire.admin.fonos', [
'fonos' => $fonos,
'tipos_fonos' => $tipos_fonos,
'sellos' => $sellos,
'ritmos' => $ritmos,
'generos' => $generos,
'idiomas' => $idiomas,
'propiedades' => $propiedades,
'territorios' => $territorios,
'clases_artistas' => $clases_artistas,
'artistasLista' => $artistasLista,
'papelesLista' => $papelesLista,
'tipos_autores' => $tipos_autores,
'autoresLista' => $autoresLista,
'operadoresLista' => $operadoresLista,
]);
}
Clearly, we need a way to avoid unnecessary queries, but we found important limitations to accomplish that goal. For instance:
We are trying to store in memory the Eloquent models (collections) that are used to populate select elements, on the initial load, since that data won't change during the lifecycle. So, we moved those queries to the mount method, something like:
public function mount()
{
// This runs once, like a constructor class
//Load all models for select drop-downs
//all the variables are declared as public properties and also included in the $rules array (i.e. 'lw_generos.*.cod' => 'nullable')
$this->lw_tipos_fonos = TipoFono::select('cod','tipo_fono')->get();
$this->lw_sellos = Sello::select('cod','sello')->get();
$this->lw_ritmos = Ritmo::select('cod','ritmo')->get();
$this->lw_generos = Genero::select('cod','genero')->get();
$this->lw_idiomas = Idioma::select('id','idioma')->get();
$this->lw_propiedades = Propiedad::select('cod','propiedad')->get();
$this->lw_territorios = Territorio::select('cod','territorio')->get();
$this->lw_clases_artistas = ClaseArtista::select('id','clase_artista')->get();
$this->lw_artistasLista = Artista::select('cod','artista')->get();
$this->lw_papelesLista = PapelArtista::select('id','papel_artista')->get();
$this->lw_autoresLista = AutorFono::select('cod','autor')->get();
$this->lw_operadoresLista = Operador::select('cod','operador')->get();
$this->lw_tipos_autores = TipoAutor::select('id','cod','tipo_autor')->get();
}
So, to be consistent with this modification, we change the section C of the render method:
return view('livewire.admin.fonos', [
'fonos' => $fonos,
'tipos_fonos' => $this->lw_tipos_fonos,
'sellos' => $this->lw_sellos,
'ritmos' => $this->lw_ritmos,
'generos' => $this->lw_generos,
'idiomas' => $this->lw_idiomas,
'propiedades' => $this->lw_propiedades,
'territorios' => $this->lw_territorios,
'clases_artistas' => $this->lw_clases_artistas,
'artistasLista' => $this->lw_artistasLista,
'papelesLista' => $this->lw_papelesLista,
'autoresLista' => $this->lw_autoresLista,
'operadoresLista' => $this->lw_operadoresLista,
'tipos_autores' => $this->lw_tipos_autores,
]);
The problem with this approach is that it generates an error:
Symfony\Component\ErrorHandler\Error\FatalError Maximum execution time of 30 seconds exceeded Symfony\Component\ErrorHandler\Error\FatalError::__construct vendor/laravel/framework/src/Illuminate/Collections/Arr.php:115
NOTE: We tried to use the public variables for the Eloquent collections directly in the view, without passing them explicitly, but the result is the same. We also thought about declare the properties as private or protected, but the data won't persist (and the Livewire documentation states that private or protected properties don't survive component updates).
We are trying to avoid unnecessary queries, that occur during view render. The $fonos Eloquent collection should be updated only when data changes (modifications that are done in the modal form), or when the search input is used. In other circumstances, the collection should be stored as an object in memory, ready to be passed to the view. But there are obstacles to achieve that goal: First, the eloquent paginated collections can't be stored as Livewire properties, at this time (only JavaScript compatible data allowed). Second, even if we could assign the collection to the public property, we would get the same error that was described in the preceding point.
We are using Laravel 8.28.1 and Livewire 2.3.18.
Upvotes: 2
Views: 2186
Reputation: 143
We solved this by using Livewire computed properties and caching the results of queries. For instance:
public function getBancosProperty(){
/* Caching */
$expire = Carbon::now()->addMinutes(10);
$bancos = Cache::remember('bancos', $expire, function() {
return Banco::select('cod','banco')->orderBy('banco')->get();
});
return $bancos;
}
Hope this helps others.
Regards.
Upvotes: 1