Skip to content

Disable eager loading partial fetch by default #1069 #1100

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

Merged
merged 1 commit into from
May 18, 2017
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
31 changes: 21 additions & 10 deletions src/Bridge/Doctrine/Orm/Extension/EagerLoadingExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,17 @@ final class EagerLoadingExtension implements QueryCollectionExtensionInterface,
private $serializerContextBuilder;
private $requestStack;

public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, int $maxJoins = 30, bool $forceEager = true, RequestStack $requestStack = null, SerializerContextBuilderInterface $serializerContextBuilder = null)
/**
* @TODO move $fetchPartial after $forceEager (@soyuka) in 3.0
*/
public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, int $maxJoins = 30, bool $forceEager = true, RequestStack $requestStack = null, SerializerContextBuilderInterface $serializerContextBuilder = null, bool $fetchPartial = false)
{
$this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
$this->propertyMetadataFactory = $propertyMetadataFactory;
$this->resourceMetadataFactory = $resourceMetadataFactory;
$this->maxJoins = $maxJoins;
$this->forceEager = $forceEager;
$this->fetchPartial = $fetchPartial;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

name is fine @teohhanhui 😛 ?

$this->serializerContextBuilder = $serializerContextBuilder;
$this->requestStack = $requestStack;
}
Expand All @@ -67,10 +71,11 @@ public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGenerator
}

$forceEager = $this->shouldOperationForceEager($resourceClass, $options);
$fetchPartial = $this->shouldOperationFetchPartial($resourceClass, $options);

$groups = $this->getSerializerGroups($resourceClass, $options, 'normalization_context');

$this->joinRelations($queryBuilder, $queryNameGenerator, $resourceClass, $forceEager, $queryBuilder->getRootAliases()[0], $groups);
$this->joinRelations($queryBuilder, $queryNameGenerator, $resourceClass, $forceEager, $fetchPartial, $queryBuilder->getRootAliases()[0], $groups);
}

/**
Expand All @@ -86,6 +91,7 @@ public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterf
}

$forceEager = $this->shouldOperationForceEager($resourceClass, $options);
$fetchPartial = $this->shouldOperationFetchPartial($resourceClass, $options);

if (isset($context['groups'])) {
$groups = ['serializer_groups' => $context['groups']];
Expand All @@ -95,7 +101,7 @@ public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterf
$groups = $this->getSerializerGroups($resourceClass, $options, 'normalization_context');
}

$this->joinRelations($queryBuilder, $queryNameGenerator, $resourceClass, $forceEager, $queryBuilder->getRootAliases()[0], $groups);
$this->joinRelations($queryBuilder, $queryNameGenerator, $resourceClass, $forceEager, $fetchPartial, $queryBuilder->getRootAliases()[0], $groups);
}

/**
Expand All @@ -112,7 +118,7 @@ public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterf
*
* @throws RuntimeException when the max number of joins has been reached
*/
private function joinRelations(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, bool $forceEager, string $parentAlias, array $propertyMetadataOptions = [], bool $wasLeftJoin = false, int &$joinCount = 0)
private function joinRelations(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, bool $forceEager, bool $fetchPartial, string $parentAlias, array $propertyMetadataOptions = [], bool $wasLeftJoin = false, int &$joinCount = 0)
{
if ($joinCount > $this->maxJoins) {
throw new RuntimeException('The total number of joined relations has exceeded the specified maximum. Raise the limit if necessary.');
Expand Down Expand Up @@ -152,18 +158,23 @@ private function joinRelations(QueryBuilder $queryBuilder, QueryNameGeneratorInt
$queryBuilder->{$method}(sprintf('%s.%s', $parentAlias, $association), $associationAlias);
++$joinCount;

try {
$this->addSelect($queryBuilder, $mapping['targetEntity'], $associationAlias, $propertyMetadataOptions);
} catch (ResourceClassNotFoundException $resourceClassNotFoundException) {
continue;
if (true === $fetchPartial) {
try {
$this->addSelect($queryBuilder, $mapping['targetEntity'], $associationAlias, $propertyMetadataOptions);
} catch (ResourceClassNotFoundException $resourceClassNotFoundException) {
continue;
}
} else {
$queryBuilder->addSelect($associationAlias);
}

// Avoid recursion
if ($mapping['targetEntity'] === $resourceClass) {
$queryBuilder->addSelect($associationAlias);
continue;
}

$this->joinRelations($queryBuilder, $queryNameGenerator, $mapping['targetEntity'], $forceEager, $associationAlias, $propertyMetadataOptions, $method === 'leftJoin', $joinCount);
$this->joinRelations($queryBuilder, $queryNameGenerator, $mapping['targetEntity'], $forceEager, $fetchPartial, $associationAlias, $propertyMetadataOptions, $method === 'leftJoin', $joinCount);
}
}

Expand All @@ -184,7 +195,7 @@ private function addSelect(QueryBuilder $queryBuilder, string $entity, string $a
}

//the field test allows to add methods to a Resource which do not reflect real database fields
if (true === $targetClassMetadata->hasField($property) && true === $propertyMetadata->isReadable()) {
if (true === $targetClassMetadata->hasField($property) && (true === $propertyMetadata->getAttribute('fetchable') || true === $propertyMetadata->isReadable())) {
$select[] = $property;
}
}
Expand Down
37 changes: 33 additions & 4 deletions src/Bridge/Doctrine/Orm/Util/EagerLoadingTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
trait EagerLoadingTrait
{
private $forceEager;
private $fetchPartial;
private $resourceMetadataFactory;

/**
Expand All @@ -35,18 +36,46 @@ trait EagerLoadingTrait
* @return bool
*/
private function shouldOperationForceEager(string $resourceClass, array $options): bool
{
return $this->getBooleanOperationAttribute($resourceClass, $options, 'force_eager', $this->forceEager);
}

/**
* Checks if an operation has a `fetch_partial` attribute.
*
* @param string $resourceClass
* @param array $options
*
* @return bool
*/
private function shouldOperationFetchPartial(string $resourceClass, array $options): bool
{
return $this->getBooleanOperationAttribute($resourceClass, $options, 'fetch_partial', $this->fetchPartial);
}

/**
* Get the boolean attribute of an operation or the resource metadata.
*
* @param string $resourceClass
* @param array $options
* @param string $attributeName
* @param bool $default
*
* @return bool
*/
private function getBooleanOperationAttribute(string $resourceClass, array $options, string $attributeName, bool $default): bool
{
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);

if (isset($options['collection_operation_name'])) {
$forceEager = $resourceMetadata->getCollectionOperationAttribute($options['collection_operation_name'], 'force_eager', null, true);
$attribute = $resourceMetadata->getCollectionOperationAttribute($options['collection_operation_name'], $attributeName, null, true);
} elseif (isset($options['item_operation_name'])) {
$forceEager = $resourceMetadata->getItemOperationAttribute($options['item_operation_name'], 'force_eager', null, true);
$attribute = $resourceMetadata->getItemOperationAttribute($options['item_operation_name'], $attributeName, null, true);
} else {
$forceEager = $resourceMetadata->getAttribute('force_eager');
$attribute = $resourceMetadata->getAttribute($attributeName);
}

return is_bool($forceEager) ? $forceEager : (bool) $this->forceEager;
return is_bool($attribute) ? $attribute : $default;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ private function handleConfig(ContainerBuilder $container, array $config, array
$container->setParameter('api_platform.error_formats', $errorFormats);
$container->setParameter('api_platform.eager_loading.enabled', $config['eager_loading']['enabled']);
$container->setParameter('api_platform.eager_loading.max_joins', $config['eager_loading']['max_joins']);
$container->setParameter('api_platform.eager_loading.fetch_partial', $config['eager_loading']['fetch_partial']);
$container->setParameter('api_platform.eager_loading.force_eager', $config['eager_loading']['force_eager']);
$container->setParameter('api_platform.collection.order', $config['collection']['order']);
$container->setParameter('api_platform.collection.order_parameter_name', $config['collection']['order_parameter_name']);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public function getConfigTreeBuilder()
->addDefaultsIfNotSet()
->children()
->booleanNode('enabled')->defaultTrue()->info('To enable or disable eager loading')->end()
->booleanNode('fetch_partial')->defaultFalse()->info('Fetch only partial data according to serialization groups. If enabled, Doctrine ORM entities will not work as expected if any of the other fields are used.')->end()
->integerNode('max_joins')->defaultValue(30)->info('Max number of joined relations before EagerLoading throws a RuntimeException')->end()
->booleanNode('force_eager')->defaultTrue()->info('Force join on every relation. If disabled, it will only join relations having the EAGER fetch mode.')->end()
->end()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
<argument>%api_platform.eager_loading.force_eager%</argument>
<argument type="service" id="request_stack" />
<argument type="service" id="api_platform.serializer.context_builder" />
<argument>%api_platform.eager_loading.fetch_partial%</argument>

<tag name="api_platform.doctrine.orm.query_extension.item" priority="64" />
<tag name="api_platform.doctrine.orm.query_extension.collection" priority="64" />
Expand Down
17 changes: 17 additions & 0 deletions src/Metadata/Property/PropertyMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,23 @@ public function getAttributes()
return $this->attributes;
}

/**
* Gets an attribute.
*
* @param string $key
* @param mixed $defaultValue
*
* @return mixed
*/
public function getAttribute(string $key, $defaultValue = null)
{
if (isset($this->attributes[$key])) {
return $this->attributes[$key];
}

return $defaultValue;
}

/**
* Returns a new instance with the given attribute.
*
Expand Down
Loading