Быстрое учение Hydrator

Я смотрю на улучшение скорости увлажнения доктрины. Я ранее использовал HYDRATE_OBJECT, но вижу, что во многих случаях это может быть довольно тяжело для работы.

Я знаю, что самый быстрый доступный вариант - HYDRATE_ARRAY, но затем я отдаю много преимуществ от работы с объектами сущности. В случаях, когда бизнес-логика в методе сущности, которая будет повторяться, однако, обрабатывается массивами.

Итак, что мне нужно, это более дешевый гидратор объекта. Я рад сделать некоторые уступки и потерять некоторые функции во имя скорости. Например, если в конечном итоге это будет только чтение, все будет в порядке. Точно так же, если ленивая загрузка не была чем-то, все было бы хорошо.

Есть ли такая вещь или я слишком много прошу?

Ответ 1

Если вы хотите быстрее ObjectHydrator, не теряя возможности работать с объектами, вам нужно будет создать свой собственный гидратор.

Для этого вам необходимо выполнить следующие действия:

  • Создайте свой собственный класс Hydrator, который расширяет Doctrine\ORM\Internal\Hydration\AbstractHydrator. В моем случае я расширяю ArrayHydrator, поскольку это избавляет меня от проблем с отображением псевдонимов в переменных объекта:

    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;
        }
    }
    
  • Зарегистрируйте гидратор в конфигурации Doctrine:

    $config = new \Doctrine\ORM\Configuration()
    $config->addCustomHydrationMode(Hydrator::HYDRATE_SIMPLE_OBJECT, Hydrator::class);
    
  • Создайте AbstractEntity с помощью метода для заполнения объекта. В моем примере я использую уже созданные методы setter в сущности для его заполнения:

    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);
                }
            }
        }
    }
    

После этих трех шагов вы можете передать HYDRATE_SIMPLE_OBJECT вместо HYDRATE_OBJECT в getResult метод запроса. Имейте в виду, что эта реализация не была сильно протестирована, но должна работать даже с вложенными сопоставлениями для более сложных функций, вам придется улучшить Hydrator::hydrateAllData(), и если вы не реализуете подключение к EntityManager, вы потеряете возможность легко сохранять/обновлять объекты, тогда как с другой стороны, потому что эти объекты - всего лишь простые объекты, вы сможете сериализовать и кэшировать их.

Тест производительности

Тестовый код:

$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);
}

Результат основан на 940 записях по 20 ~ столбцам:

HYDRATE_OBJECT => 0.57511210441589
HYDRATE_ARRAY => 0.19534111022949
HYDRATE_SIMPLE_OBJECT => 0.37919402122498

Ответ 2

Возможно, вы ищете способ доктрины для гидратации DTO (Объект передачи данных). Это не реальные сущности, а простые объекты только для чтения, предназначенные для передачи данных.

Так как Doctrine 2.4 имеет встроенную поддержку такой гидратации, используя оператор NEW в DQL.

Если у вас есть класс следующим образом:

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

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

    // getters ...
}

Вы можете использовать SQL следующим образом:

$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 будет содержать массив объектов CustomerDTO.

Вы можете найти его здесь, в документации.