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?
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:
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;
}
}
Register hydrator in Doctrine configuration:
$config = new \Doctrine\ORM\Configuration()
$config->addCustomHydrationMode(Hydrator::HYDRATE_SIMPLE_OBJECT, Hydrator::class);
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.
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