Doctrine 3

Здесь я описываю моменты связанные с третьей версией доктрины.

Связи

Доктрина в третье версии убрала хинты 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

Поэтому теперь нельзя подтянуть данные из первой таблицы и не подтянуть данные из второй (подтягиваются сразу данные из обеих таблиц). Значит, решить проблему можно только:

  • получаем данные через DBAL, а уже затем мапим данные на объекты, но это уже не ORM
  • получением id-ки из первой таблицы и затем уже выбираем средствами DQL из второй, но это уже тоже не совсем ORM

Для себя я решил этот вопрос так:

  • не делаю связи в объектах, которые я раньше вязал как OneToOne

Proxy объекты

Оказывается, когда мы выполняем запрос который внутри не имеет 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-объектов.

Можно ли заставить дотрину не создавать Proxy объекты

Да, но у каждого из способов есть свои нюансы, давайте рассмотрим их.

Вариант 1

Указываем что именно нужно гидрировать сразу (с помощью хинтов, которые заполняем с помощью 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 объектами (такая вот оптимизация от доктрины).

Детали:

  • \Doctrine\ORM\UnitOfWork::createEntity - именно тут происходит гидрации и срабатывают ранее выставленные хинты.
  • \Symfony\Component\VarExporter\Internal\LazyObjectState - именно этот объект используется для будущего превращения прокси-объекта в полноценный (можете в конструкторе выставить брейкпоинт и увидеть, откуда растут ноги).

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

  1. Вы запросили объект A, у которого есть поле parent который ссылается на объект B (Доктрин сделает оптимизацию, она из базы вытащит данные для объекта A, но не вытащит данные для объекта B, а для поля parent создаст Proxy-объект)
  2. далее Вы делаете запрос, который должен достать объекты B и C, но Доктрина снова сделает оптимизацию, она из базы вытащит данные для создания объектов B и C, но создаст только объект С (ведь объект B уже был создан в первом шаге как Proxy-объект)

Вывод: в первом запросе тоже нужно указывать хинты или использовать способ 2 (см. ниже).

Вариант 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' 

Вариант 3

В репозитории объекта создадим метод:

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).

Вариант 4

Порывшись в исходниках я нашел способ отключать прокси-объекты недокументированным способом (документированным не получается: 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]]);
    }
}

Большой итог

  1. там где мне приемлемы Proxies я продолжаю пользоваться существующими doctrine-методами поиска объектов, такими как find, findAll, и т.д.
  2. там где неприемлемы Proxies:
    • полю parent указываю аннотацию fetch: 'EAGER' (см. вше Вариант 2 - да, будет некоторый оверхед по данным, зато без Proxy)
    • остальным полям не указываю fetch: 'EAGER' и создавая методы в репозиториях указываю setFetchMode (см. выше Вариант 1)
    • чтобы не создавать Doctrine Proxy объекты для случаев подобно Person.parent я продолжаю использовать UnitOfWork::HINT_DEFEREAGERLOAD => false (см. Вариант 4) 
  3. я более не хочу копаться в недрах Доктрин, поэтому теперь стараюсь избегать аннотаций @ManyToOne, @OneToOne и @OneToMany (следовательно объекты есть, но теперь они не будут связаны в коде), потому что не люблю когда вдруг начинают появляться странные дополнительные SQL-запросы, это позволит:
    1. сэкономит память и трафик между приложением и базой (ведь теперь не будет SELECT table1.*, table2.*, ...)
    2. самостоятельно решать где мне нужны джоины, а где нет (без оглядки на версии Doctrine)
    3. мне не нужно будет копаться в исходниках Доктрины и делать костыли
    4. код будет простым и быстрым
    5. я буду уверен в коде

Всем удачки.

Array field

Вместо поля с type=array я использую type=text потому, что при использовании type=array:

  • данные в базу сохраняются не JSON_UNESCAPED_UNICODE (т.е. посмотреть кирилицу в базе уже не получится)
  • type=array является deprecated и доктрин советует использовать type=json, что в свою очередь является тоже неконтролируемым преобразованием array в json поддерживаемый базой или в строку (doctrine сама решит как преобразовать и опять прощай кирилица)

11.09.2021 07:08

Комментарии

Комментариев пока нет.


Copyrights © 2007 - 2026 YaPro.Ru