-
Notifications
You must be signed in to change notification settings - Fork 0
Speed up eloquent without hydration
This feature is included in laravel-crud-wizard-free 4.2.0 and in the demo page for clients resource.
Many developers choose to not use Eloquent (model/active record) from Laravel or Lumen but instead, they use the Query Builder.
The main reason is that the response times are smaller. Eloquent hydrates the data that comes from the Query Builder and does additional stuff like casting, appending, eager loading and so on.
When you don't need all this extra stuff and you just want to get the raw results from DB as response, but you still want to be able to AUTO FILTER the data using Eloquent not just the Query Builder (just like laravel-crud-wizard-free does), you can bypass the Eloquent hydration process with some simple steps:
Create Eloquent Builder macros for alternatives to paginate, simplePaginate and cursorPaginate methods:
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
...
EloquentBuilder::macro(
'getUnHydrated',
/**
* @throws \Throwable
*/
function (array|string $columns = ['*']): \Illuminate\Support\Collection {
/** @var EloquentBuilder $this */
return $this->toBase()->get($columns);
}
);
EloquentBuilder::macro(
'paginateUnHydrated',
/**
* @throws \Throwable
*/
function (
int|null|\Closure $perPage = null,
array|string $columns = ['*'],
string $pageName = 'page',
?int $page = null,
int|null|\Closure $total = null,
): LengthAwarePaginator {
/** @var EloquentBuilder $this */
$page = $page ?: Paginator::resolveCurrentPage($pageName);
$total = value($total) ?? $this->toBase()->getCountForPagination();
$perPage = value($perPage, $total) ?: $this->model->getPerPage();
$results = $total
? $this->forPage($page, $perPage)->getUnHydrated($columns)
: $this->model->newCollection();
return $this->paginator($results, $total, $perPage, $page, [
'path' => Paginator::resolveCurrentPath(),
'pageName' => $pageName,
]);
}
);
EloquentBuilder::macro(
'simplePaginateUnHydrated',
/**
* @throws \Throwable
*/
function (
?int $perPage = null,
array $columns = ['*'],
string $pageName = 'page',
?int $page = null
): \Illuminate\Contracts\Pagination\Paginator {
/** @var EloquentBuilder $this */
$page = $page ?: Paginator::resolveCurrentPage($pageName);
$perPage = $perPage ?: $this->model->getPerPage();
// Next we will set the limit and offset for this query so that when we get the
// results we get the proper section of results. Then, we'll create the full
// paginator instances for these results with the given page and per page.
$this->skip(($page - 1) * $perPage)->take($perPage + 1);
return $this->simplePaginator($this->getUnHydrated($columns), $perPage, $page, [
'path' => Paginator::resolveCurrentPath(),
'pageName' => $pageName,
]);
}
);
EloquentBuilder::macro(
'cursorPaginateUnHydrated',
/**
* @throws \Throwable
*/
function (
?int $perPage = null,
array|string $columns = ['*'],
string $cursorName = 'cursor',
Cursor|string|null $cursor = null
): CursorPaginator {
/** @var EloquentBuilder $this */
$perPage = $perPage ?: $this->model->getPerPage();
return $this->paginateUsingCursorUnHydrated($perPage, $columns, $cursorName, $cursor);
}
);
// the last one is done in an analog way but it is too long to be copy pasted into this article
// EloquentBuilder::macro(
// 'paginateUsingCursorUnHydrated',
// /**
// * @throws \Throwable
// */
// function (
// int $perPage,
// array|string $columns = ['*'],
// string $cursorName = 'cursor',
// Cursor|string|null $cursor = null
// ): CursorPaginator {
// // copy paste here from \Illuminate\Database\Concerns\BuildsQueries::paginateUsingCursor
//
// return $this->cursorPaginator($this->getUnHydrated($columns), $perPage, $cursor, [
// 'path' => Paginator::resolveCurrentPath(),
// 'cursorName' => $cursorName,
// 'parameters' => $orders->pluck('column')->toArray(),
// ]);
// }
// );
// NOTE:
// Place them in the boot function of a service provider
// (that DOES NOT implement Illuminate\Contracts\Support\DeferrableProvider
// to avoid situations when the macro is not found)
// and make sure it is registered.or overwrite them in a custom Builder that you set in your model if you want to avoid the macros:
/**
* @inheritDoc
*/
public function newEloquentBuilder($query): CustomEloquentBuilder
{
return new CustomEloquentBuilder($query);
}If you chose the macros, overwrite the getPaginator method in your controller to use them, otherwise skip this step:
/**
* @throws \Throwable
*/
protected function getPaginator(
Builder | Relation $builder,
array $allRequest
): LengthAwarePaginator | Paginator | CursorPaginator {
$model = $builder instanceof Relation ? $builder->getRelated() : $builder->getModel();
$limit = \max(1, (int)($allRequest['limit'] ?? $model->getPerPage()));
if (isset($allRequest['cursor'])) {
return $builder->cursorPaginateUnHydrated(
$limit,
['*'],
'cursor',
$allRequest['cursor']
);
}
return $builder->{$this->simplePaginate ? 'simplePaginateUnHydrated' : 'paginateUnHydrated'}(
$limit,
['*'],
'page',
\max((int)($allRequest['page'] ?? 1), 1)
);
}List a resource and check the result (refer to the documentation for details).
NOTE:
- the model will be used ONLY for building the query that will be executed
- the relations will not be part of the response
- it is advisable to only do this in a micro-service or service that retrieves data for an internal project to not expose publicly your db structure but, laravel-crud-wizard-decorator-free can be used for decorating the db columns if you want to expose the response publicly while sacrificing some extra time generated by the PHP processing.
- for non API situations you can use $eloquentBuilder->toBase()->get($columns);