From 4adbee0c3a8fe8c12aa76371bffc084a713b5910 Mon Sep 17 00:00:00 2001 From: DDynamic Date: Sun, 10 Jan 2021 13:28:07 -0600 Subject: [PATCH] Add optimized selects --- src/Pagination/PaginateDirective.php | 11 +++ src/Schema/Directives/AllDirective.php | 14 ++- src/Schema/Directives/RelationDirective.php | 27 ++++-- src/Select/SelectDirective.php | 23 +++++ src/Select/SelectHelper.php | 99 +++++++++++++++++++++ src/lighthouse.php | 13 +++ tests/DBTestCase.php | 7 +- 7 files changed, 184 insertions(+), 10 deletions(-) create mode 100644 src/Select/SelectDirective.php create mode 100644 src/Select/SelectHelper.php diff --git a/src/Pagination/PaginateDirective.php b/src/Pagination/PaginateDirective.php index 168888405..00872543a 100644 --- a/src/Pagination/PaginateDirective.php +++ b/src/Pagination/PaginateDirective.php @@ -9,6 +9,7 @@ use Nuwave\Lighthouse\Schema\AST\DocumentAST; use Nuwave\Lighthouse\Schema\Directives\BaseDirective; use Nuwave\Lighthouse\Schema\Values\FieldValue; +use Nuwave\Lighthouse\Select\SelectHelper; use Nuwave\Lighthouse\Support\Contracts\FieldManipulator; use Nuwave\Lighthouse\Support\Contracts\FieldResolver; use Nuwave\Lighthouse\Support\Contracts\GraphQLContext; @@ -103,6 +104,16 @@ function ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) $this->directiveArgValue('scopes', []) ); + if (config('lighthouse.optimized_selects')) { + $fieldSelection = $resolveInfo->getFieldSelection(4); + + if (in_array('data', $fieldSelection) || in_array('edges', $fieldSelection)) { + $fieldSelection = array_keys(in_array($fieldSelection, 'data') ? $fieldSelection['data'] : $fieldSelection['edges']['node']); + $selectColumns = SelectHelper::getSelectColumns($this->definitionNode, $fieldSelection, $this->getModelClass()); + $query = $query->select($selectColumns); + } + } + return PaginationArgs ::extractArgs($args, $this->paginationType(), $this->paginateMaxCount()) ->applyToBuilder($query); diff --git a/src/Schema/Directives/AllDirective.php b/src/Schema/Directives/AllDirective.php index c5b44b7d4..afc2a6828 100644 --- a/src/Schema/Directives/AllDirective.php +++ b/src/Schema/Directives/AllDirective.php @@ -5,6 +5,7 @@ use GraphQL\Type\Definition\ResolveInfo; use Illuminate\Database\Eloquent\Collection; use Nuwave\Lighthouse\Schema\Values\FieldValue; +use Nuwave\Lighthouse\Select\SelectHelper; use Nuwave\Lighthouse\Support\Contracts\FieldResolver; use Nuwave\Lighthouse\Support\Contracts\GraphQLContext; @@ -35,13 +36,20 @@ public function resolveField(FieldValue $fieldValue): FieldValue { return $fieldValue->setResolver( function ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo): Collection { - return $resolveInfo + $builder = $resolveInfo ->argumentSet ->enhanceBuilder( $this->getModelClass()::query(), $this->directiveArgValue('scopes', []) - ) - ->get(); + ); + + if (config('lighthouse.optimized_selects')) { + $fieldSelection = array_keys($resolveInfo->getFieldSelection(1)); + $selectColumns = SelectHelper::getSelectColumns($this->definitionNode, $fieldSelection, $this->getModelClass()); + $builder = $builder->select($selectColumns); + } + + return $builder->get(); } ); } diff --git a/src/Schema/Directives/RelationDirective.php b/src/Schema/Directives/RelationDirective.php index e411cb352..ded64c4df 100644 --- a/src/Schema/Directives/RelationDirective.php +++ b/src/Schema/Directives/RelationDirective.php @@ -16,8 +16,10 @@ use Nuwave\Lighthouse\Pagination\PaginationType; use Nuwave\Lighthouse\Schema\AST\DocumentAST; use Nuwave\Lighthouse\Schema\Values\FieldValue; +use Nuwave\Lighthouse\Select\SelectHelper; use Nuwave\Lighthouse\Support\Contracts\FieldResolver; use Nuwave\Lighthouse\Support\Contracts\GraphQLContext; +use Illuminate\Support\Facades\DB; abstract class RelationDirective extends BaseDirective implements FieldResolver { @@ -26,8 +28,7 @@ public function resolveField(FieldValue $value): FieldValue $value->setResolver( function (Model $parent, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) { $relationName = $this->directiveArgValue('relation', $this->nodeName()); - - $decorateBuilder = $this->makeBuilderDecorator($resolveInfo); + $decorateBuilder = $this->makeBuilderDecorator($resolveInfo, $parent, $relationName); $paginationArgs = $this->paginationArgs($args); if (config('lighthouse.batchload_relations')) { @@ -70,15 +71,29 @@ function (Model $parent, array $args, GraphQLContext $context, ResolveInfo $reso return $value; } - protected function makeBuilderDecorator(ResolveInfo $resolveInfo): Closure - { - return function ($builder) use ($resolveInfo) { - $resolveInfo + protected function makeBuilderDecorator(ResolveInfo $resolveInfo, Model $parent, string $relationName): Closure + { + return function ($builder) use ($resolveInfo, $parent, $relationName) { + $builderDecorator = $resolveInfo ->argumentSet ->enhanceBuilder( $builder, $this->directiveArgValue('scopes', []) ); + + if (config('lighthouse.optimized_selects')) { + $fieldSelection = array_keys($resolveInfo->getFieldSelection(1)); + $selectColumns = SelectHelper::getSelectColumns($this->definitionNode, $fieldSelection, get_class($builderDecorator->getRelated())); + $foreignKeyName = $parent->{$relationName}()->getForeignKeyName(); + + if (! in_array($foreignKeyName, $selectColumns)) { + array_push($selectColumns, $foreignKeyName); + } + + $builderDecorator->select($selectColumns); + + // at some point, the builder is "infected" with a "with" clause causing it to select the relation or something, idk + } }; } diff --git a/src/Select/SelectDirective.php b/src/Select/SelectDirective.php new file mode 100644 index 000000000..7bdbcc9f7 --- /dev/null +++ b/src/Select/SelectDirective.php @@ -0,0 +1,23 @@ +documentAST(); + + $type = $documentAST->types[$returnTypeName]; + // error_log(print_r($type, true)); + + if ($type instanceof UnionTypeDefinitionNode) { + $type = $documentAST->types[ASTHelper::getUnderlyingTypeName($type->types[0])]; + } + + + /** @var iterable<\GraphQL\Language\AST\FieldDefinitionNode> $fieldDefinitions */ + $fieldDefinitions = $type->fields; + + $model = new $modelName; + + $selectColumns = []; + + foreach ($fieldSelection as $field) { + $fieldDefinition = ASTHelper::firstByName($fieldDefinitions, $field); + + if ($fieldDefinition) { + $directivesRequiringLocalKey = ['hasOne', 'hasMany', 'count']; + $directivesRequiringForeignKey = ['belongsTo', 'belongsToMany', 'morphTo']; + $directivesRequiringKeys = array_merge($directivesRequiringLocalKey, $directivesRequiringForeignKey); + + foreach ($directivesRequiringKeys as $directiveType) { + if (ASTHelper::hasDirective($fieldDefinition, $directiveType)) { + $directive = ASTHelper::directiveDefinition($fieldDefinition, $directiveType); + + if (in_array($directiveType, $directivesRequiringLocalKey)) { + $relationName = ASTHelper::directiveArgValue($directive, 'relation', $field); + + if (method_exists($model, $relationName)) { + array_push($selectColumns, $model->{$relationName}()->getLocalKeyName()); + } + } + + if (in_array($directiveType, $directivesRequiringForeignKey)) { + $relationName = ASTHelper::directiveArgValue($directive, 'relation', $field); + + if (method_exists($model, $relationName)) { + array_push($selectColumns, $model->{$relationName}()->getForeignKeyName()); + } + } + + continue 2; + } + } + + if (ASTHelper::hasDirective($fieldDefinition, 'select')) { + // append selected columns in select directive to seletion + $directive = ASTHelper::directiveDefinition($fieldDefinition, 'select'); + + if ($directive) { + $selectFields = ASTHelper::directiveArgValue($directive, 'columns') ?? []; + $selectColumns = array_merge($selectColumns, $selectFields); + } + } elseif (ASTHelper::hasDirective($fieldDefinition, 'rename')) { + // append renamed attribute to selection + $directive = ASTHelper::directiveDefinition($fieldDefinition, 'rename'); + + if ($directive) { + $renamedAttribute = ASTHelper::directiveArgValue($directive, 'attribute'); + array_push($selectColumns, $renamedAttribute); + } + } else { + // fallback to selecting the field name + array_push($selectColumns, $field); + } + } + } + + return array_unique($selectColumns); + } +} diff --git a/src/lighthouse.php b/src/lighthouse.php index 56cfc62c9..741ae3be8 100644 --- a/src/lighthouse.php +++ b/src/lighthouse.php @@ -295,6 +295,19 @@ 'batchload_relations' => true, + /* + |-------------------------------------------------------------------------- + | Optimized Selects + |-------------------------------------------------------------------------- + | + | If set to true, Eloquent will only select the columns neccessary to resolve + | a query. You must use the select directive to resolve advanced field dependencies + | on other columns. + | + */ + + 'optimized_selects' => true, + /* |-------------------------------------------------------------------------- | GraphQL Subscriptions diff --git a/tests/DBTestCase.php b/tests/DBTestCase.php index 156c35f8d..6e456b8bf 100644 --- a/tests/DBTestCase.php +++ b/tests/DBTestCase.php @@ -3,6 +3,7 @@ namespace Tests; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Event; abstract class DBTestCase extends TestCase { @@ -39,11 +40,15 @@ protected function getEnvironmentSetUp($app): void { parent::getEnvironmentSetUp($app); + /*Event::listen('Illuminate\Database\Events\QueryExecuted', function ($query) { + error_log($query->sql . ' - ' . serialize($query->bindings)); + });*/ + $app['config']->set('database.default', 'mysql'); $app['config']->set('database.connections.mysql', [ 'driver' => 'mysql', 'database' => env('LIGHTHOUSE_TEST_DB_DATABASE', 'test'), - 'host' => env('LIGHTHOUSE_TEST_DB_HOST', 'mysql'), + 'host' => env('LIGHTHOUSE_TEST_DB_HOST', 'localhost'), 'username' => env('LIGHTHOUSE_TEST_DB_USERNAME', 'root'), 'password' => env('LIGHTHOUSE_TEST_DB_PASSWORD', ''), ]);