Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Fast entity Doctrine hydrator

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?

like image 551
Rob Forrest Avatar asked Oct 09 '15 10:10

Rob Forrest


2 Answers

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
like image 182
Marcin Necsord Szulc Avatar answered Oct 07 '22 17:10

Marcin Necsord Szulc


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.

like image 26
Jasper N. Brouwer Avatar answered Oct 07 '22 16:10

Jasper N. Brouwer