Search code examples
phpsymfonyserializationdoctrine-ormapi-platform.com

Bad Performance when using a output DTO with doctrine entities with a set of relations


API Platform version(s) affected:

/srv/api # composer show | grep api-platform
api-platform/core                              v2.6.8           Build a fully-featured hypermedia or GraphQL API in minutes!

Description
To define the response of our API endpoints, we have used attributes on the generated Doctrine entity such as:

/**
 * @ORM\Table(name = "products")
 * @ORM\Entity(repositoryClass=ProductRepository::class)
 */
#[ApiResource(
    collectionOperations: [
        'get' => [
            'path' => '/products',
        ],
    ],
    itemOperations: [
        'get' => [
            'path' => '/products/{id}',
        ],
    ],
    normalizationContext: [
        'groups' => [
            'product:read',
        ],
    ],
    output: ProductOutput::class,
)]
class Product {
    .... // properties and getters+setters
}

The Product entity has a 1:n relation to the Variant entity which is also a ApiResource with an different endpoint /variants. The Variant entity has several relations to other entities and some values of all entities are translatable with https://github.com/doctrine-extensions/DoctrineExtensions/blob/main/doc/translatable.md.

The performance was as expected => good enough.


Later on, it was required to "enrich" the response of /products and /variants with some data, which was not mapped in relations between Product <> additional-data | Variant <> additional-data, so we decided to use Outputs DTO with DataTransformers, as documented in the API-Platform docs.

The DataTransformer's method transform puts the data into the DTO by using therespective getters of the entities, e. g.:

$output                  = new ProductOutput();
$output->id              = $object->getId();
$output->category        = null !== $object->getCategory() ?
    $this->iriConverter->getIriFromItem($object->getCategory()) :
    '';
$output->identifierValue = $object->getIdentifierValue();
$output->manufacturer    = $object->getManufacturer();
$output->variants        = $object->getVariants();

The $object is a Product entity, in this case.

The DTO contains only public properties, such as

/**
 * @var Collection<int, Variant>
 */
#[Groups(['product:read'])]
public Collection $variants;

and the Groups attributes, which are also defined in the normalizationContext of the ApiResource attribute in the Product entity above.

After that, we found the performance had drastically deteriorated: A request to the /products endpoint which "lists" 30 products with the related variants needs around 25 seconds.

After analyzing, we determined the following:

  1. without DTO: Doctrine runs one single query with a lot of joins to retrieve all the related data from the database.
  2. with DTO: Doctrine runs in sum 3.155 single queries to get the data.
  3. by default, API-Platform uses Eager-Fetching (see https://api-platform.com/docs/core/performance/#force-eager), but it seems so that will be ignored if the getters of a entity are used in the DTO.
  4. the serialization process needs the most time. That is maybe (also) a Symfony issue.

In a try to reduce the Doctrine queries, we created a DataProvider to fetch the related data. This actually worked, as using the DataProvider reduced the number of queries to +/- 50, but the serialization process also needed around 25s. So the cause of the performance problem does not seem to be the lazy-loading of doctrine, which is now done.

The question is: Why is using a DTO so much slower how would it be it possible to get performance back to an acceptable level?


Solution

  • It was not possible to improve performance when using a DTO for this data-structure. Instead of a DTO and data transformer we used a Doctrine Event Listener (postLoad) to set the value which is a unmapped property (Doctrine/Symfony: Entity with non-mapped property) now.