Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Keep the query and pagination capabilities of relation directives when disabling batch loading #1083

Merged
merged 8 commits into from
Dec 13, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Enable multiple queries in a single request by clearing `BatchLoader` instances
after executing each query https://github.com/nuwave/lighthouse/pull/1030
- Keep the query and pagination capabilities of relation directives when disabling batch loading https://github.com/nuwave/lighthouse/pull/1083

## [4.7.1](https://github.com/nuwave/lighthouse/compare/v4.7.0...v4.7.1)

Expand Down
4 changes: 2 additions & 2 deletions src/Execution/Arguments/ArgumentSet.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,11 @@ public function rename(): self
/**
* Apply ArgBuilderDirectives and scopes to the builder.
*
* @param \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $builder
* @param \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Relations\Relation $builder
* @param string[] $scopes
* @param \Closure $directiveFilter
*
* @return \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder
* @return \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Relations\Relation
*/
public function enhanceBuilder($builder, array $scopes, Closure $directiveFilter = null)
{
Expand Down
18 changes: 15 additions & 3 deletions src/Execution/DataLoader/BatchLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,9 @@

use GraphQL\Deferred;
use Illuminate\Support\Collection;
use Nuwave\Lighthouse\Support\Traits\HandlesCompositeKey;

abstract class BatchLoader
{
use HandlesCompositeKey;

/**
* Active BatchLoader instances.
*
Expand Down Expand Up @@ -140,4 +137,19 @@ function ($key) use ($metaInfo): Deferred {
* @return array<mixed, mixed>
*/
abstract public function resolve(): array;

/**
* Build a key out of one or more given keys, supporting composite keys.
*
* E.g.: $primaryKey = ['key1', 'key2'];.
*
* @param mixed $key
* @return string
*/
protected function buildKey($key): string
{
return is_array($key)
? implode('___', $key)
: $key;
}
}
183 changes: 47 additions & 136 deletions src/Execution/DataLoader/ModelRelationFetcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,12 @@
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Nuwave\Lighthouse\Support\Traits\HandlesCompositeKey;
use Nuwave\Lighthouse\Pagination\PaginationArgs;
use ReflectionClass;
use ReflectionMethod;

class ModelRelationFetcher
{
use HandlesCompositeKey;

/**
* The parent models that relations should be loaded for.
*
Expand All @@ -33,109 +31,72 @@ class ModelRelationFetcher
protected $relations;

/**
* @param mixed $models The parent models that relations should be loaded for
* @param \Illuminate\Database\Eloquent\Collection $models The parent models that relations should be loaded for
* @param mixed[] $relations The relations to be loaded. Same format as the `with` method in Eloquent builder.
* @return void
*/
public function __construct($models, array $relations)
{
$this->setModels($models);
$this->setRelations($relations);
}

/**
* Set the relations to be loaded.
*
* @param array $relations
* @return $this
*/
public function setRelations(array $relations): self
public function __construct(EloquentCollection $models, array $relations)
{
$this->models = $models;
// Parse and set the relations.
$this->relations = $this->newModelQuery()
->with($relations)
->getEagerLoads();

return $this;
}

/**
* Return a fresh instance of a query builder for the underlying model.
* Load all relations for the model, but constrain the query to the current page.
*
* @return \Illuminate\Database\Eloquent\Builder
* @param \Nuwave\Lighthouse\Pagination\PaginationArgs $paginationArgs
* @return \Illuminate\Database\Eloquent\Collection
*/
protected function newModelQuery(): EloquentBuilder
public function loadRelationsForPage(PaginationArgs $paginationArgs): EloquentCollection
{
return $this->models()
->first()
->newModelQuery();
}
foreach ($this->relations as $name => $constraints) {
$this->loadRelationForPage($paginationArgs, $name, $constraints);
}

/**
* Get all the underlying models.
*
* @return \Illuminate\Database\Eloquent\Collection<\Illuminate\Database\Eloquent\Model>
*/
public function models(): EloquentCollection
{
return $this->models;
}

/**
* Set one or more Model instances as an EloquentCollection.
*
* @param mixed $models
* @return $this
*/
protected function setModels($models): self
{
$this->models = $models instanceof EloquentCollection
? $models
: new EloquentCollection($models);

return $this;
}

/**
* Load all the relations of all the models.
* Reload the models to get the `{relation}_count` attributes of models set.
*
* @return $this
* @return \Illuminate\Database\Eloquent\Collection
*/
public function loadRelations(): self
public function reloadModelsWithRelationCount(): EloquentCollection
{
$this->models->load($this->relations);

return $this;
}
$ids = $this->models->modelKeys();

/**
* Load all relations for the model, but constrain the query to the current page.
*
* @param int $perPage
* @param int $page
* @return $this
*/
public function loadRelationsForPage(int $perPage, int $page = 1): self
{
foreach ($this->relations as $name => $constraints) {
$this->loadRelationForPage($perPage, $page, $name, $constraints);
}
$this->models = $this
->newModelQuery()
->withCount($this->relations)
->whereKey($ids)
->get()
->filter(function (Model $model) use ($ids): bool {
// We might have gotten some models that we did not have before
// so we filter them out
return in_array(
$model->getKey(),
$ids,
true
);
});

return $this;
return $this->models;
}

/**
* Load only one page of relations of all the models.
*
* The relation will be converted to a `Paginator` instance.
*
* @param int $first
* @param int $page
* @param \Nuwave\Lighthouse\Pagination\PaginationArgs $paginationArgs
* @param string $relationName
* @param \Closure $relationConstraints
* @return $this
* @return void
*/
public function loadRelationForPage(int $first, int $page, string $relationName, Closure $relationConstraints): self
protected function loadRelationForPage(PaginationArgs $paginationArgs, string $relationName, Closure $relationConstraints): void
{
// Load the count of relations of models, this will be the `total` argument of `Paginator`.
// Be aware that this will reload all the models entirely with the count of their relations,
Expand All @@ -145,8 +106,8 @@ public function loadRelationForPage(int $first, int $page, string $relationName,
$relations = $this
->buildRelationsFromModels($relationName, $relationConstraints)
->map(
function (Relation $relation) use ($first, $page) {
return $relation->forPage($page, $first);
function (Relation $relation) use ($paginationArgs) {
return $relation->forPage($paginationArgs->page, $paginationArgs->first);
}
);

Expand All @@ -161,52 +122,19 @@ function (Relation $relation) use ($first, $page) {

$this->associateRelationModels($relationName, $relationModels);

$this->convertRelationToPaginator($first, $page, $relationName);

return $this;
}

/**
* Reload the models to get the `{relation}_count` attributes of models set.
*
* @return $this
*/
public function reloadModelsWithRelationCount(): self
{
/** @var \Illuminate\Database\Eloquent\Builder $query */
$query = $this->models()
->first()
->newQuery()
->withCount($this->relations);

$ids = $this->getModelIds();

$reloadedModels = $query
->whereKey($ids)
->get()
->filter(function (Model $model) use ($ids): bool {
return in_array(
$model->getKey(),
$ids,
true
);
});

return $this->setModels($reloadedModels);
$this->convertRelationToPaginator($paginationArgs, $relationName);
}

/**
* Extract the primary keys from the underlying models.
* Return a fresh instance of a query builder for the underlying model.
*
* @return mixed[]
* @return \Illuminate\Database\Eloquent\Builder
*/
protected function getModelIds(): array
protected function newModelQuery(): EloquentBuilder
{
return $this->models
->map(function (Model $model) {
return $model->getKey();
})
->all();
->first()
->newModelQuery();
}

/**
Expand Down Expand Up @@ -284,27 +212,11 @@ protected function loadDefaultWith(EloquentCollection $collection): self
* @param string $relationName
* @return string
*/
public function getRelationCountName(string $relationName): string
protected function getRelationCountName(string $relationName): string
{
return Str::snake("{$relationName}_count");
}

/**
* Get an associative array of relations, keyed by the models primary key.
*
* @param string $relationName
* @return mixed[]
*/
public function getRelationDictionary(string $relationName): array
{
return $this->models
->mapWithKeys(
function (Model $model) use ($relationName): array {
return [$this->buildKey($model->getKey()) => $model->getRelation($relationName)];
}
)->all();
}

/**
* Merge all the relation queries into a single query with UNION ALL.
*
Expand All @@ -326,14 +238,13 @@ function (EloquentBuilder $builder, Relation $relation) {
}

/**
* @param int $first
* @param int $page
* @param \Nuwave\Lighthouse\Pagination\PaginationArgs $paginationArgs
* @param string $relationName
* @return $this
*/
protected function convertRelationToPaginator(int $first, int $page, string $relationName): self
protected function convertRelationToPaginator(PaginationArgs $paginationArgs, string $relationName): self
{
$this->models->each(function (Model $model) use ($page, $first, $relationName): void {
$this->models->each(function (Model $model) use ($paginationArgs, $relationName): void {
$total = $model->getAttribute(
$this->getRelationCountName($relationName)
);
Expand All @@ -343,8 +254,8 @@ protected function convertRelationToPaginator(int $first, int $page, string $rel
[
'items' => $model->getRelation($relationName),
'total' => $total,
'perPage' => $first,
'currentPage' => $page,
'perPage' => $paginationArgs->first,
'currentPage' => $paginationArgs->page,
'options' => [],
]
);
Expand Down
Loading