Доктрина 2: Кэш в ассоциациях "один ко многим"

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

Моя конфигурация:

'configuration' => array(
    'orm_default' => array(
        'metadata_cache'    => 'filesystem',
        'query_cache'       => 'filesystem',
        'result_cache'      => 'filesystem',
        'hydration_cache'   => 'filesystem',
    )
),

Моя сущность

class Category
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     *
     * @var string
     */
    protected $id;

    /**
     * @var string
     *
     * @ORM\Column(name="name", type="string", length=100, nullable=false)
     */
    protected  $name;

    /**
     * @var integer
     *
     * @ORM\ManyToOne(targetEntity="Category", inversedBy="childrenId", fetch="EAGER")
     * @ORM\JoinColumn(name="parent_id", referencedColumnName="id")
     */
    protected $parentId;


    /**
     * @ORM\OneToMany(targetEntity="Category", mappedBy="parentId", fetch="EAGER")
     */
    protected $childrenId;
}

My DQL

$result = $this->em->createQueryBuilder()->select('c')
    ->from('App\Entity\Category', 'c')
    ->where('c.parentId IS NULL')
    ->orderBy('c.priority', 'ASC')
    ->getQuery()
    ->setFetchMode("App\Entity\Category", "parentId", \Doctrine\ORM\Mapping\ClassMetadata::FETCH_EAGER);
    ->useResultCache(true, 900, 'categories')
    ->getResult();

У меня 28 категорий, 15 у них есть parentId.
Над запросом выполняется 29 SQL-запросов, но хранилище Doctrine только в кеше 1. Поэтому, когда я снова запускаю этот запрос, он выполняет 28 запросов.

Любая идея, что я делаю неправильно? отсутствует некоторая конфигурация кеша? Отсутствуют некоторые методы в DQL? Я хотел бы кэшировать все запросы не только с одним основным запросом.

Edit

Я хотел бы использовать результат запроса в цикле, например:

foreach($result as $row)
{
    $categories[]['attr'] = $row->getAttribute()->getName();
    $categories[]['value'] = $row->getAttribute()->getValue();
}

но в этом случае кеш не будет работать, поэтому в настоящее время я использую:

foreach($result as $row)
{
        $attributes = $this->em->createQueryBuilder()->select('c, a.name, a.value')
            ->from('App\Entity\Category', 'c')
            ->innerJoin('App\Entity\Attribute', 'a', 'WITH', 'a.id = c.attribute')
            ->where('c.id = :catId')
            ->setParameter('catId', $row['id'])
            ->getQuery()
            ->useResultCache(true, 900, $categoryName.'-attributes')
            ->getArrayResult();
}

Но я предпочел бы работать с объектами, а затем на массивах, но я не могу исключить, использую ли я объект и имеет ассоциацию, тогда эта ассоциация не будет кэшироваться. Поэтому идеально было бы каким-то образом кэшировать объект + ВСЕ его ассоциации.

Ответы

Ответ 1

Ассоциации fetch-modes

Представленный вами запрос извлекает только "родительские" Category сущности, которые увлажняются с неинициализированной коллекцией для детей. При доступе к этой коллекции (например, путем повторения этих детей) Doctrine будет загружать коллекцию, таким образом, выполнить другой запрос. Он будет делать это для всех родительских категорий, увлажненных первым запросом.

Настройка режима fetch для EAGER только изменяет момент выполнения этих запросов. Doctrine будет делать их сразу после увлажнения родительских категорий, это не будет ждать, пока вы не получите доступ к коллекции (например, в режиме Letch). Но он все равно будет выполнять эти запросы.

Запрос Fetch-join

Самый простой способ сказать Doctrine для запроса и гидратации категорий с их дочерними элементами - это выполнить запрос "получить соединение":

$queryBuilder = $this->em->createQueryBuilder();
$queryBuilder
    ->select('p', 'c') // [p]arent, [c]hild
    ->from('App\Entity\Category', 'p')
    ->leftJoin('p.children', 'c')
    ->where('p.parent IS NULL')
    ->orderBy('p.priority', 'ASC');

$query = $queryBuilder->getQuery();
$query
    ->useResultCache(true, 900, 'categories')

$result = $query->getResult();

Обратите внимание на вызовы select() и leftJoin().

Вам также не нужно изменять режим выборки ассоциации (вызывая setFetchMode()), потому что сам запрос подскажет Doctrine, что вы хотите.

Результатом этого является то, что Doctrine выполнит 1 запрос, если он еще не кэширован (или кеш устарел), или 0 запросов, если он кэширован (и все еще свежий).

Предположения

Свойство $parentIdCategory) переименовано в $parent. Это свойство будет содержать родительский объект Category или null, но никогда не будет id.

Свойство $childrenId переименовано в $children. Это свойство будет содержать коллекцию объектов Category (которые могут быть пустыми), но никогда не является коллекцией (или массивом) идентификаторов и, конечно же, никогда не будет содержать одного идентификатора.

В запросе, который я предлагаю выше, учитываются эти имена.

Я полностью игнорирую тот факт, что сразу после вашего "редактирования" появился новый объект Attribute. Это не относится к вашему вопросу или этому ответу IMHO.

Дополнительные уровни

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

При переходе на 3 или более уровней вы можете посмотреть модель вложенного набора. Это тяжелее для записей, но очень оптимизировано для чтения.

Библиотека DoctrineExtensions поддерживает эту функцию, а также Symfony Bundle.

Ответ 2

Согласно документам: http://doctrine-orm.readthedocs.io/en/latest/reference/dql-doctrine-query-language.html#temporarily-change-fetch-mode-in-dql Я думаю, что setFetchMode - EAGER не будет работать для вас, так как это вызовет дополнительные запросы, которые не будут быть кешированным.

Почему вы не загружаете свои категории с атрибутами?

$result = $this->em->createQueryBuilder()
    ->select('c, a.name, a.value')
    ->from('App\Entity\Category', 'c')
    ->innerJoin('App\Entity\Attribute', 'a', 'WITH', 'a.id = c.attribute')
    ->where('c.parentId IS NULL')
    ->orderBy('c.priority', 'ASC')
    ->getQuery()
    ->useResultCache(true, 900, 'categories')
    ->getResult();

И он должен работать так, как ожидалось.