Fast entity Doctrine hydrator
Asked Answered
M

2

17

I'm looking at improving the speed of doctrine hydration. I've previously been using HYDRATE_OBJECT but can see that in many instances, that can be quite heavy to work with.

I'm aware that the fastest option available is HYDRATE_ARRAY but then I give away a lot of benefits of working with entity objects. In instances where there's business logic in an entity method, that's going to be repeated for however that's handled by arrays.

So what I'm after is a cheaper object hydrator. I'm happy to make some concessions and loose some functionality in the name of speed. For instance if it ended up being read only, that'd be ok. Equally, if lazy loading wasn't a thing, that would be ok too.

Does this sort of thing exist or am I asking too much?

Monomer answered 9/10, 2015 at 10:0 Comment(0)
F
19

If you want faster ObjectHydrator without losing the ability to work with objects then you will have to create your own custom hydrator.

To do so you have to do following steps:

  1. Create your own Hydrator class which extends Doctrine\ORM\Internal\Hydration\AbstractHydrator. In my case I am extending ArrayHydrator as it saves me trouble of mapping aliases to object variables:

    use Doctrine\ORM\Internal\Hydration\ArrayHydrator;
    use Doctrine\ORM\Mapping\ClassMetadataInfo;
    use PDO;
    
    class Hydrator extends ArrayHydrator
    {
        const HYDRATE_SIMPLE_OBJECT = 55;
    
        protected function hydrateAllData()
        {
            $entityClassName = reset($this->_rsm->aliasMap);
            $entity = new $entityClassName();
            $entities = [];
            foreach (parent::hydrateAllData() as $data) {
                $entities[] = $this->hydrateEntity(clone $entity, $data);
            }
    
            return $entities;
        }
    
        protected function hydrateEntity(AbstractEntity $entity, array $data)
        {
            $classMetaData = $this->getClassMetadata(get_class($entity));
            foreach ($data as $fieldName => $value) {
                if ($classMetaData->hasAssociation($fieldName)) {
                    $associationData = $classMetaData->getAssociationMapping($fieldName);
                    switch ($associationData['type']) {
                        case ClassMetadataInfo::ONE_TO_ONE:
                        case ClassMetadataInfo::MANY_TO_ONE:
                            $data[$fieldName] = $this->hydrateEntity(new $associationData['targetEntity'](), $value);
                            break;
                        case ClassMetadataInfo::MANY_TO_MANY:
                        case ClassMetadataInfo::ONE_TO_MANY:
                            $entities = [];
                            $targetEntity = new $associationData['targetEntity']();
                            foreach ($value as $associatedEntityData) {
                                $entities[] = $this->hydrateEntity(clone $targetEntity, $associatedEntityData);
                            }
                            $data[$fieldName] = $entities;
                            break;
                        default:
                            throw new \RuntimeException('Unsupported association type');
                    }
                }
            }
            $entity->populate($data);
    
            return $entity;
        }
    }
    
  2. Register hydrator in Doctrine configuration:

    $config = new \Doctrine\ORM\Configuration()
    $config->addCustomHydrationMode(Hydrator::HYDRATE_SIMPLE_OBJECT, Hydrator::class);
    
  3. Create AbstractEntity with method for populating the entity. In my sample I am using already created setter methods in the entity to populate it:

    abstract class AbstractEntity
    {
        public function populate(Array $data)
        {
            foreach ($data as $field => $value) {
                $setter = 'set' . ucfirst($field);
                if (method_exists($this, $setter)) {
                    $this->{$setter}($value);
                }
            }
        }
    }
    

After those three steps you can pass HYDRATE_SIMPLE_OBJECT instead of HYDRATE_OBJECT to getResult query method. Keep in mind this implementation was not heavily tested but should work even with nested mappings for more advanced functionality you will have to improve Hydrator::hydrateAllData() and unless you implement connection to EntityManager you will lose the ability to easily save / update entities, while on the other hand because these objects are just mere simple objects, you will be able to serialize and cache them.

Performance test

Test code:

$hydrators = [
    'HYDRATE_OBJECT'        => \Doctrine\ORM\AbstractQuery::HYDRATE_OBJECT,
    'HYDRATE_ARRAY'         => \Doctrine\ORM\AbstractQuery::HYDRATE_ARRAY,
    'HYDRATE_SIMPLE_OBJECT' => Hydrator::HYDRATE_SIMPLE_OBJECT,
];

$queryBuilder = $repository->createQueryBuilder('u');
foreach ($hydrators as $name => $hydrator) {
    $start = microtime(true);
    $queryBuilder->getQuery()->getResult($hydrator);
    $end = microtime(true);
    printf('%s => %s <br/>', $name, $end - $start);
}

Result based on 940 records with 20~ columns each:

HYDRATE_OBJECT => 0.57511210441589
HYDRATE_ARRAY => 0.19534111022949
HYDRATE_SIMPLE_OBJECT => 0.37919402122498
Foreman answered 22/1, 2016 at 0:12 Comment(9)
Thank you Marcin for your answer. I'm gonig to award you the bounty as you've provided by far the best answer, but I'm not going to mark it as correct in the vain hope that someone might write one that can deal with ManyToMany/OneToMany/ManyToOne relationships.Monomer
Thanks @RobForrest I have modified my answer to include support for associations. I did not heavy test it but I did test it with ManyToOne and OneToMany with nested OneToOne and it worked just fine.Foreman
Wowser!!! Thanks for that. I'm looking forward to giving this a go, I'll get back to you with how I get on.Monomer
Couple of things, at the end of hydrateEntity() you call $entity->setFromArray($data); did you mean $entity->populate($data); ?. I also had to add use Doctrine\ORM\Mapping\ClassMetadataInfo; at the very top.Monomer
Other than those things, it performs really nicely. I'll be using it in anger over the coming months and will report back with how it works out in a real life setting. Thank you so much.Monomer
Yes you are right, thanks for pointing those out! I have some obsolete code which uses different naming that's why it was different.Foreman
Hi @MarcinNecsordSzulc, can you please specify what is the location of abstract class? - I am creating hydrator in MyBundle/Hydrator/ClassHydrator - In config.yml, I have configured it like below: doctrine: orm: hydrators: ListHydrator: MyBundle\Hydrator\SimpleHydrator In MyBundle/Entity/AbstractEntity is created. But I am getting errors, can you please tell why?Passionate
Hard for me to say without knowing the error. AbstractEntity is my own class so there's no need for specific path as long as your paths are includedForeman
Thanks a lot for this, it's very helpful. I have noticed some issues when trying to hydrate collections of objects which also have associations. I managed to get it working with associations and collections by modifying the Hydrator class like this: - line 12 becomes: $entity = new $entityClassName(); - line 15 becomes: $entities[] = $this->hydrateEntity((new $entity()), $data);Valeda
B
9

You might be looking for a way for Doctrine to hydrate DTO's (Data Transfer Object). These are not real entities, but simple read-only objects meant to pass data around.

Since Doctrine 2.4 it has native support for such hydration using the NEW operator in DQL.

When you have class like this:

class CustomerDTO
{
    private $name;
    private $email;
    private $city;

    public function __construct($name, $email, $city)
    {
        $this->name  = $name;
        $this->email = $email;
        $this->city  = $city;
    }

    // getters ...
}

You can use SQL like this:

$query     = $em->createQuery('SELECT NEW CustomerDTO(c.name, e.email, a.city) FROM Customer c JOIN c.email e JOIN c.address a');
$customers = $query->getResult();

$customers will then contain an array of CustomerDTO objects.

You can find it here in the documentation.

Bohunk answered 22/1, 2016 at 15:51 Comment(2)
Thanks Jasper, I don't think that DTO's are quite the right answer here, I'm keen to reuse the entity classes that already exist rather than create a new flock of classes to work with.Monomer
No worries! I'll keep it here as a reminder for people with similar questions, for whom it might be a valid option :)Bohunk

© 2022 - 2024 — McMap. All rights reserved.