问题 快速实体主义水化器


我正在寻求提高学说水合的速度。我以前一直在使用 HYDRATE_OBJECT 但是可以看到,在许多情况下,这可能会非常繁重。

我知道最快的选择是 HYDRATE_ARRAY 但后来我放弃了使用实体对象的许多好处。在实体方法中存在业务逻辑的情况下,这将被重复,但是由数组处理。

所以我所追求的是更便宜的物体保湿剂。我很乐意以速度的名义做出一些让步并放松一些功能。例如,如果它最终只是被读取,那就没问题了。同样,如果延迟加载不是一件事,那也没关系。

这种事情是存在还是我要求太多?


10839
2017-10-09 10:00


起源



答案:


如果你想要更快 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 用于填充实体的方法。在我的示例中,我使用已在实体中创建的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

13
2018-01-22 00:12



谢谢Marcin的回答。我鼓励你奖励你,因为你提供了迄今为止最好的答案,但是我不打算将它标记为正确,因为徒劳地希望有人可以编写一个可以处理ManyToMany / OneToMany / ManyToOne关系的人。 - Rob Forrest
谢谢@RobForrest我修改了我的答案,包括对协会的支持。我没有对它进行大量测试,但我确实测试了它 ManyToOne 和 OneToMany 嵌套 OneToOne 它运作得很好。 - Marcin Necsord Szulc
Wowser!感谢那。我很期待这样做,我会回答你的情况。 - Rob Forrest
几件事,在结束时 hydrateEntity() 你打电话 $entity->setFromArray($data); 你的意思是 $entity->populate($data); ?。我也不得不补充一下 use Doctrine\ORM\Mapping\ClassMetadataInfo; 在最顶端。 - Rob Forrest
除了那些东西,它表现得非常好。我将在未来几个月内愤怒地使用它,并将报告它在现实环境中如何运作。非常感谢。 - Rob Forrest


答案:


如果你想要更快 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 用于填充实体的方法。在我的示例中,我使用已在实体中创建的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

13
2018-01-22 00:12



谢谢Marcin的回答。我鼓励你奖励你,因为你提供了迄今为止最好的答案,但是我不打算将它标记为正确,因为徒劳地希望有人可以编写一个可以处理ManyToMany / OneToMany / ManyToOne关系的人。 - Rob Forrest
谢谢@RobForrest我修改了我的答案,包括对协会的支持。我没有对它进行大量测试,但我确实测试了它 ManyToOne 和 OneToMany 嵌套 OneToOne 它运作得很好。 - Marcin Necsord Szulc
Wowser!感谢那。我很期待这样做,我会回答你的情况。 - Rob Forrest
几件事,在结束时 hydrateEntity() 你打电话 $entity->setFromArray($data); 你的意思是 $entity->populate($data); ?。我也不得不补充一下 use Doctrine\ORM\Mapping\ClassMetadataInfo; 在最顶端。 - Rob Forrest
除了那些东西,它表现得非常好。我将在未来几个月内愤怒地使用它,并将报告它在现实环境中如何运作。非常感谢。 - Rob Forrest


您可能正在寻找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 对象。

你可以找到它 在这里的文档


3
2018-01-22 15:51



谢谢Jasper,我不认为DTO在这里是非常正确的答案,我热衷于重用已经存在的实体类,而不是创建一个新的类来使用。 - Rob Forrest
别担心!我会把它留在这里作为提醒有类似问题的人,对他们来说这可能是一个有效的选择:) - Jasper N. Brouwer