Здесь я описываю моменты связанные с третьей версией доктрины.
Доктрина в третье версии убрала хинты HINT_FORCE_PARTIAL_LOAD и HINT_PARTIAL:
## BC BREAK: Partial objects are removed
- The `PARTIAL` keyword in DQL no longer exists.
- `Doctrine\ORM\Query\AST\PartialObjectExpression`is removed.
- `Doctrine\ORM\Query\SqlWalker::HINT_PARTIAL` and
`Doctrine\ORM\Query::HINT_FORCE_PARTIAL_LOAD` are removed.
- `Doctrine\ORM\EntityManager*::getPartialReference()` is removed.
т.е. всякие хинты при запросах и в аннотациях не работают, речь про такие:
$query->setHint(Query::HINT_FORCE_PARTIAL_LOAD, true)
$query->setFetchMode(Bank::class, 'bank', ClassMetadata::FETCH_EXTRA_LAZY)
Даже, если значение второй таблицы может быть NULL:
#[ORM\OneToOne(inversedBy: 'news', cascade: ['persist', 'remove'])]
#[ORM\JoinColumn(nullable: true)]
private ?Document $document = null;
// nullable потому что News создается до Document
#[ORM\OneToOne(mappedBy: 'news', cascade: ['persist', 'remove'])]
private ?News $news = null;В подтверждении в коде можно найти строчку "Inverse side of x-to-one can never be lazy"
А еще, если в таблице 2 нет данных, то строка из таблицы 1 не отобразится, потому что doctrine использует INNER:
SELECT * FROM table_1 INNER JOIN table_2
Поэтому теперь нельзя подтянуть данные из первой таблицы и не подтянуть данные из второй (подтягиваются сразу данные из обеих таблиц). Значит, решить проблему можно только:
Для себя я решил этот вопрос так:
Оказывается, когда мы выполняем запрос который внутри не имеет JOIN-ов (например: findAll), то доктрин применяет:
Query::HYDRATE_SIMPLEOBJECT => new Internal\Hydration\SimpleObjectHydrator($this)и если какой-то объект ссылается на другой объект (в базе есть FK), то мы получаем \Proxies\__CG__\App\Entity\Person
В противном случае (когда join-ы есть), применяется:
Query::HYDRATE_OBJECT => new Internal\Hydration\ObjectHydrator($this)и мы не видим Proxy-объектов.
Да, но у каждого из способов есть свои нюансы, давайте рассмотрим их.
Указываем что именно нужно гидрировать сразу (с помощью хинтов, которые заполняем с помощью setFetchMode):
public function getAll(): array {
$qb = $this->createQueryBuilder('t')->select('t');
$q = $qb->getQuery();
$q->setFetchMode(Card::class, 'parent', ClassMetadata::FETCH_EAGER);
$q->setFetchMode(Card::class, 'kids', ClassMetadata::FETCH_EAGER);
$q->setHint(UnitOfWork::HINT_DEFEREAGERLOAD, false);
return $q->getResult();
}Как видно, мы указали, что поля parent и kids должны подтягиваться сразу, а не создаваться прокси-объектами (по-умолчанию) + отключили запасную стратегию создания прокси объектов.
Без хинтов, объекты на которые ссылаются из полей parent и kids становятся Proxy объектами (такая вот оптимизация от доктрины).
Детали:
Вот и все, однако есть еще одна деталь - хинты не помогают, если объект, который вытаскивается из базы, уже был создан ранее как Proxy-объект, приведу пример:
Вывод: в первом запросе тоже нужно указывать хинты или использовать способ 2 (см. ниже).
Заставим сразу создавать полноценные объекты
#[ORM\ManyToOne(targetEntity: self::class, fetch: 'EAGER', inversedBy: 'kids')]
private ?self $parent = null;если так сделать, то в результате будут полноценные объекты.
Однако, как всегда есть ложка дегтя - теперь доктрина будет делать JOIN таблицы самой на себя, например:
$personRepository->find(57)выполнит SQL запрос:
SELECT t1.*, t2.*
FROM person t1
LEFT JOIN person t2 ON t1.parent_id = t2.id
WHERE t1.id = ? Это неоптимально когда мы просто хотим вытащить все объекты из таблицы + как уже выше было сказано, при join-ах просто срабатывает другая стратегия гидрации (ObjectHydrator).
Кстати, не пользуйтесь константами в аннотациях, т.е. нельзя указывать: fetch: ORM\ClassMetadata::FETCH_EAGER
иначе получите ошибку: Entity 'App\Entity\Person' has a mapping with invalid fetch mode '3'
все дело в том, что Доктрина ожидает, что мы не будем пользоваться константами, а просто напишем fetch: 'EAGER'
В репозитории объекта создадим метод:
public function findAllItems(): array
{
return $this->createQueryBuilder('t')->getQuery()->getResult(AbstractQuery::HYDRATE_SIMPLEOBJECT);
}ОПАСНО: хинт HYDRATE_SIMPLEOBJECT не будет порождать Proxy-объекты, но там где есть указание ID другого объекта - будет null (хотя по факту число). Например у объекта есть FK-поле parent, которое в базе имеет значение 59 (person.parent_id=59), но доктрина выставит значение null ($person->getParent() === null).
Порывшись в исходниках я нашел способ отключать прокси-объекты недокументированным способом (документированным не получается: enable_lazy_ghost_objects: false выдает ошибку). Нужно добавить ядру своего бандла метод CompilerPass-а:
<?php
namespace App;
use Doctrine\ORM\Configuration;
use Doctrine\ORM\UnitOfWork;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
class Kernel extends BaseKernel implements CompilerPassInterface
{
use MicroKernelTrait;
public function process(ContainerBuilder $container): void
{
/** @var Configuration $configuration */
//$configuration = $container->get('doctrine.orm.configuration'); // ->setPublic(true);
//$configuration->setDefaultQueryHints([UnitOfWork::HINT_DEFEREAGERLOAD => false]);
// Выше указанный способ не работает, т.к. судя по всему объект Configuration создается позже, поэтому
// конфигурируем инициализацию объекта Configuration (говорим, что в момент инициализации нужно вызвать метод):
$c = $container->getDefinition('doctrine.orm.default_configuration');
$c->addMethodCall('setDefaultQueryHints', [[UnitOfWork::HINT_DEFEREAGERLOAD => false]]);
}
}Proxies я продолжаю пользоваться существующими doctrine-методами поиска объектов, такими как find, findAll, и т.д.Proxies:parent указываю аннотацию fetch: 'EAGER' (см. вше Вариант 2 - да, будет некоторый оверхед по данным, зато без Proxy)fetch: 'EAGER' и создавая методы в репозиториях указываю setFetchMode (см. выше Вариант 1)Person.parent я продолжаю использовать UnitOfWork::HINT_DEFEREAGERLOAD => false (см. Вариант 4) @ManyToOne, @OneToOne и @OneToMany (следовательно объекты есть, но теперь они не будут связаны в коде), потому что не люблю когда вдруг начинают появляться странные дополнительные SQL-запросы, это позволит:Всем удачки.
Вместо поля с type=array я использую type=text потому, что при использовании type=array:
Комментарии
Комментариев пока нет.