Kdyby/Doctrine obsah z více entit a množství dotazů

d@rkWolf
Člen | 163
+
0
-

Zdravím, bojuju tu s Kdyby/Doctrine, snaží se přijít na to, jak vyřešit problém několika set dotazů do DB pro vykreslení seznamu.

V systému se mi vykresluje tabulka(je to přehled produktů, v administraci, ale částečně je i otázka front-endu), která je složena z dat z několika entit, kolega, co to původně dělal, tam fouknul „findAll()“ a byl happy. Bohužel, i pouze v testovacím systému, kde mám chabých 140 test položek to znamenalo původně skoro 600 dotazů do DB(odstranil jsem nesmyslný samostatný dotaz pro stav ON/OFF každé položky čímž jsem to o 140 dotazů zredukoval, ale furt 450 zbejvá…), aby se to vykreslilo. Děsný čas na zobrazení, přidávají se přes to nové položky, vyrostl by z toho i trpaslík…

Když si troch pohraju s MySQL, takto dostanu vše co potřebuju jediným dotazem, není úplně triviální, ale jinak to redukovat už nejde, to ještě vynechávám jednu tabulku, kde jsou názvy kategorií(zde v category_list pouze seznam ID):

<?php
SELECT *,
GROUP_CONCAT(DISTINCT food_categories.category SEPARATOR ', ') AS category_list,
GROUP_CONCAT(DISTINCT food_resources.resource SEPARATOR ', ') AS resource_list,
GROUP_CONCAT(DISTINCT food_files.file SEPARATOR ', ') AS image_list
FROM `food`
LEFT JOIN food_categories
ON food_categories.food = food.id
LEFT JOIN food_resources
ON food_resources.food = food.id
LEFT JOIN food_files
ON food_files.food = food.id
GROUP BY food.id
?>

Je možné něco takového provést přes DQL v Doctrine(nedaří se mi)? Případně jak takové věci řešíte, pokud je nutné k sestavení dat spojení několika tabulek? Protože 600 dotazů do DB není adekvátní řešení. Zvláště, když je potenciálně možné, že bude nutné k tomu v dohledné době přidávat ještě 1–2 další tabulky, tzn. další znásobení počtu dotazů. Bohužel, kdybych si to navrhoval, zbavil bych se tabulky s obrázky a úvodní si šoupnul přímo k produktu apod. jenže aktuálně by něco takovýho rozsypalo několik systémů a to prostě nemůžu udělat.

Je lepší se prostě na Doctrine vykašlat, hodit si tam dotaz ručně a zpracovat si normálně výstup v poli? Nebo nad tím uvažuju úplně špatně? Nejsem moc fanda Doctrine, ale tady mi nic jiného nezbývá, už v tom bylo. Nebo vybrat z každé tabulky vše bez joinů a pak to pospojovat při výpisu v PHP?

Editoval d@rkWolf (18. 8. 2017 20:06)

kluzi
Člen | 2
+
+5
-

Zdravím,

tohle se dá v Doctrine řešit několika způsoby.

1. Native query
Můžeme použít přímo SQL, které jsi postnul a použít native query. V podstatě tím obcházíme celou Doctrine, takže přícházíme o veškeré výhody, které poskytuje práce s entitami.
Použítí by mohlo vypadat takhle:

$rsm = new ResultSetMapping();
$rsm->addScalarResult('category_list', 'category_list');
$rsm->addScalarResult('resource_list', 'resource_list');
$rsm->addScalarResult('image_list', 'image_list');
$rsm->addScalarResult('name', 'name');

$foods = $this->em->createNativeQuery('SELECT *, GROUP_CONCAT(DISTINCT food_categories.category SEPARATOR ', ') AS category_list, ...', $rsm)
	->execute();

Doctrine v tomto případě ukládá výsledek do obyčejného PHP pole. V šabloně tedy můžeš používat

{foreach $foods as $food}
	{$food['name']}
	{$food['category_list']}
{/foreach}

2. DQL s joiny
V DQL lze využívat joiny podobně jak v čistém SQL.

$foods = $this->em->getRepository(Food::class)->createQueryBuilder()
	->select('f')->from(Food::class, 'f')
	->leftJoin('f.categories', 'c')->addSelect('c')
	->leftJoin('f.resources', 'r')->addSelect('r')
	->leftJoin('f.files', 'fi')->addSelect('fi')
	->getQuery()
	->getResult();

K datům se dostaneme například

{foreach $foods as $food}
	{$food->name}
	{foreach $food->categories as $category}
		{$category->category}{sep}, {/sep}
	{/foreach}
{/foreach}

Tenhle přístup má ale několik nevýhod. Je to typická ukázka, jak náročná může být hydratace objektů (https://ocramius.github.io/…n-hydration/). Dále nelze například řešit stránkování pouhým zavoláním setMaxResults, protože jsou v dotazu využity právě JOINy.

3. DQL s multi-step hydration
Problém s efektivitou hydratace při více joinech lze vyřešit více krokovou hydratací. V praxi to znamená, že z původního dotazu odstraníš všechny JOINy a necháš si pouze dotaz, který načítá samotné entity Food.

$foods = $this->em->getRepository(Food::class)->createQueryBuilder()
	->select('f')
	->from(Food::class, 'f')
	->getQuery()
	->execute();

Nyní si zjistíme, jaká ID mají entity, které jsme načetli.

$foodsIds = array_map(function (Food $food) {
	return $food->id;
}, $foods);

A načteme další objekty (Tohle budeme muset udělat pro všechny související kolekce – categories, resources a files). Celkem tedy získáme všechna data použítím 4 dotazů

$this->em->getRepository(Food::class)->createQueryBuilder()
	->select('PARTIAL f.{id}, c')
	->from(Food::class, 'f')
	->leftJoin('f.categories', 'c')
	->where('f.id IN (:ids)')
	->setParameter('ids', $foodsIds)
	->getQuery()
	->execute();

Jak je vidět, tak výsledek tohoto dotazu nikam neukládáme. Doctrine v tuto chvíli totiž provádí rehydrataci objektů, takže je budeme mít již přístupné přes $food->categories.
Tento způsob vyžaduje trochu více psaní, ale získáš tím plnohodnotné entity se všemi daty.

d@rkWolf
Člen | 163
+
0
-

Po mnoha pokusech se mi podařilo v sobotu vytvořit DQL s joiny, komplet vypadá takto:

<?php
$qb = $this->foodPizza->createQueryBuilder('f');
        $result = $qb->select('f')
                ->distinct()
                ->addSelect("GROUP_CONCAT(DISTINCT fpc.name SEPARATOR ', ') AS category_list")
                ->addSelect("GROUP_CONCAT(DISTINCT rp.name SEPARATOR ', ') AS resource_list")
                ->addSelect("GROUP_CONCAT(DISTINCT ffp.file SEPARATOR ',') AS image_list")
                ->leftJoin('f.categories', 'fpc')
                ->leftJoin(FileFood::class, 'ffp', 'WITH', 'ffp.food = f.id')
                ->leftJoin('f.resources', 'rp');
        if ($category) {
            $result = $qb->where(':category member of f.categories ')
                    ->setParameter('category', $category);
        }
        if ($onlyVisible === true) {
            $result = $qb->andWhere('f.visible = :visible')
                    ->setParameter('visible', 1);
        }
        $result = $qb->groupBy('f.id')
                ->getQuery()
                ->getResult();

        return $result;
?>

a výsledná interpretace dotazu(můj původní pokus přímo v sql připojoval id kategorií, ale ta tabulka je jen JoinTable bez vlastní entity takže v dql musím připojit structure, kde jsou názvy těch kategorií):

<?php
SELECT DISTINCT p0_.id AS id_0, p0_.code AS code_1, p0_.name AS name_2, p0_.description AS
description_3, p0_.price AS price_4, p0_.visible AS visible_5, p0_.type AS type_6,
GROUP_CONCAT(DISTINCT s1_.name SEPARATOR ', ') AS sclr_7, GROUP_CONCAT(DISTINCT p2_.name SEPARATOR
', ') AS sclr_8, GROUP_CONCAT(DISTINCT p3_.file SEPARATOR ',') AS sclr_9
FROM food p0_
LEFT JOIN food_categories p4_ ON p0_.id = p4_.food
LEFT JOIN structure s1_ ON s1_.id = p4_.category
LEFT JOIN food_files p3_ ON (p3_.food = p0_.id)
LEFT JOIN food_resources p5_ ON p0_.id = p5_.food
LEFT JOIN resources p2_ ON p2_.id = p5_.resource
GROUP BY p0_.id
?>

Nicméně, i přes to, jak se mi ten dotaz vůbec nelíbí, stránka se generuje rychleji, než za předchozího stavu se stovkama dotazů.

Nejradši bych se zbavil komplet propojení na tabulku files a úvodní obrázek si dal přímo k produktu, ale na to budu muset zjistit, kde všude máme takto obrázky, páč bych musel vymyslet způsob, jak ty záznamy převést.

Editoval d@rkWolf (21. 8. 2017 10:20)