Kdyby/Doctrine obsah z více entit a množství dotazů
- d@rkWolf
- Člen | 163
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
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
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)