Гидратор Fast Entity Doctrine

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

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

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

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


person Rob Forrest    schedule 09.10.2015    source источник


Ответы (2)


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

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

  1. Создайте свой собственный класс 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;
        }
    }
    
  2. Зарегистрируйте гидратор в конфигурации Doctrine:

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

    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
person Marcin Necsord Szulc    schedule 22.01.2016
comment
Спасибо Марцин за ответ. Я собираюсь наградить вас наградой за то, что вы предоставили лучший ответ, но я не собираюсь отмечать его как правильный в тщетной надежде, что кто-то может написать ответ, который может иметь дело с отношениями ManyToMany/OneToMany/ManyToOne. . - person Rob Forrest; 28.01.2016
comment
Спасибо @RobForrest. Я изменил свой ответ, включив поддержку ассоциаций. Я не сильно тестировал его, но я проверил его с ManyToOne и OneToMany с вложенным OneToOne, и он работал нормально. - person Marcin Necsord Szulc; 28.01.2016
comment
Ваусер!!! Спасибо за это. Я с нетерпением жду возможности попробовать это, я вернусь к вам с тем, как я получаю. - person Rob Forrest; 29.01.2016
comment
Пара вещей, в конце hydrateEntity() вы называете $entity->setFromArray($data); вы имели в виду $entity->populate($data); ?. Мне также пришлось добавить use Doctrine\ORM\Mapping\ClassMetadataInfo; в самом верху. - person Rob Forrest; 29.01.2016
comment
Помимо этих вещей, он работает очень хорошо. Я буду использовать его в гневе в ближайшие месяцы и доложу, как это работает в реальных условиях. Большое спасибо. - person Rob Forrest; 29.01.2016
comment
Да, вы правы, спасибо, что указали на них! У меня есть устаревший код, который использует другое имя, поэтому оно было другим. - person Marcin Necsord Szulc; 29.01.2016
comment
Привет @MarcinNecsordSzulc, не могли бы вы указать, где находится абстрактный класс? - Я создаю гидратор в MyBundle/Hydrator/ClassHydrator - В config.yml я настроил его, как показано ниже: доктрина: orm: Hydrators: ListHydrator: MyBundle\Hydrator\SimpleHydrator В MyBundle/Entity/AbstractEntity создается. Но я получаю ошибки, не могли бы вы сказать, почему? - person anujeet; 29.07.2019
comment
Мне сложно сказать, не зная ошибки. AbstractEntity - это мой собственный класс, поэтому нет необходимости указывать конкретный путь, если включены ваши пути - person Marcin Necsord Szulc; 31.07.2019

Возможно, вы ищете способ для Doctrine гидратировать 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 объектов.

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

person Jasper N. Brouwer    schedule 22.01.2016
comment
Спасибо, Джаспер, я не думаю, что DTO - правильный ответ здесь, я стремлюсь повторно использовать уже существующие классы сущностей, а не создавать новую группу классов для работы. - person Rob Forrest; 29.01.2016
comment
Не волнуйтесь! Я сохраню это здесь как напоминание для людей с похожими вопросами, для которых это может быть подходящим вариантом :) - person Jasper N. Brouwer; 01.02.2016