KdybyDoctrine – ako zefektívniť DQL
- Čamo
- Člen | 798
Zdravím,
dnes som robil jeden datagrid napojený na Doktrínu a keď som ho dorobil,
začal som rozmýšľať ako to DQL v datasource zefektívniť. Ide o to, že
tam mám väzbu 1:N medzi článkom a jazkovými mutáciami, takže som použil
join. To samozrejme vo výsledku SQL vygeneruje duplicitné riadky kôli tým
jazykom. A pokiaľ viem to je pri Doktríne problém pri hydratácii. Chcel by
som vedieť ako sa to dá riešiť. Kód pre datagrid vyzerá takto:
$grid = new DataGrid();
$grid->setDataSource( $this->articleRepository->createQueryBuilder()
->select( 'article', 'user', 'status', 'langs' )
->from( 'App\Model\Entity\Article', 'article' )
->innerJoin( 'article.status', 'status' )
->innerJoin( 'article.user', 'user' )
->innerJoin( 'article.langs', 'langs' )
->whereCriteria( ['status.id =' => [Entity\Status::STATUS_DRAFT, Entity\Status::STATUS_PUBLISHED, Entity\Status::STATUS_UNPUBLISHED]] )
);
$grid->addColumnText( 'id', 'ID' );
$grid->addColumnText( 'title', 'Title', 'langs.title' )
->setRenderer( function ( $article ) {
return $article->lang->getTitle(); // Calls $article->getLang( $code = 'sk' )
})
->setSortable()
->setFilterText()
->setSplitWordsSearch( FALSE );
Pri title sa volá metóda $article->getLang(), ktorá vracia jeden riadok z kolekcie $article->langs a je implementovaná cez filter
class Article extends Nette\Object
{
//...
public function getLang( $code = 'sk' )
{
return $this->langs->filter( function ( $item ) use ( $code ) {
return $item->getCode() == $code;
})->first();
}
Skúšal som to vypodmienkovať v DQL, aby mi vracalo pre langs iba jeden riadok, ale doktrína to nejako obyšla…
- Čamo
- Člen | 798
Hmmmm, podarilo sa mi napísať metódu, ktorá urobí multi-step hydration, ale nedá sa to použiť v Datagride, pretože ten pracuje s inštanciou QueryBuildera. Takže Datagrid nezaujíma, že treba vykonať nejaké partial query… Ale v aplikácii pri ručnom renderovaní sa to bude dať dúfam použiť.
public function findBy( $by )
{
$by_langs = [];
$by_others = [];
foreach ( $by as $key => $value )
{
// Because in first QB langs key is undefined and on the contrary in second QB other keys are undefined
// E.g. langs.title = throws exception 'langs' is not defined in first QB.
if ( strpos( $key, 'langs' ) === 0 ) $by_langs[$key] = $value;
elseif ( strpos( $key, 'id' ) === 0 || strpos( $key, 'article.id' ) === 0 ) $by_others[$key] = $by_langs[$key] = $value; // id is in both conditions.
else $by_others[$key] = $value;
}
$articles = $this->articleRepository->createQueryBuilder()
->select( 'article', 'user', 'status' )
->from( 'App\Model\Entity\Article', 'article' )
->innerJoin( 'article.status', 'status' )
->innerJoin( 'article.user', 'user' )
->whereCriteria( $by_others )
->getQuery();
// Next result is discarded. This is just re-hydrating the collections.
$this->articleRepository->createQueryBuilder()
->select( 'partial article.{id}', 'langs' )
->from( 'App\Model\Entity\Article', 'article' )
->innerJoin( 'article.langs', 'langs' )
->whereCriteria( $by_langs )
->getQuery()
->setHint( Query::HINT_FORCE_PARTIAL_LOAD, 1 )
->getResult();
// Returns ResultSet because of paginator.
return new Kdyby\Doctrine\ResultSet( $articles );
}
Edit: https://forum.nette.org/…ektivnit-dql#…
Editoval Čamo (26. 7. 2017 10:10)
- cubic
- Člen | 45
Řešil jsem něco podobného minulý týden. Shop, 60.000 objednávek,
4 joiny, Ublaboo/DataGrid, na každé vykreslení potřebné 2 SQL dotazy po
4 sekundách – byly to subquery, nad kterými se prováděl count() a
stránkování.
Na MariaDB 10.1 to běželo celkem bez problémů (vyřadila z query všechny
zbytečné joiny), přecházel jsem ale na Perconu, která ty dotazy podle
explain zpracovávala úplně jinak (ta tam joiny nechala a ještě vytvořila
2 temp tabulky navíc).
Nakonec se mi osvědčilo vyhodit z QueryBuilderu všechny joiny, přidal jsem
je do setCondition, jen pokud je zadaná hodnota filtru. Pokud není, tak lazy
loading a několik desítek dotazů navíc. I tak je grid vygenerovaný za
zlomek původního času.
Strávil jsem tím několik hodin, úplně elegantní to není, ale funguje.
Pokud tam těch stránek nemáš tisíce, tak Ti to ale asi nepomůže :-)
- cubic
- Člen | 45
@Čamo
Tohle je podobný jednodušší případ – výchozí entita + k tomu
2 joiny. Přes QueryBuilder mám nadefinovaný ten subquery jako data source,
DataGrid z toho pak vygeneruje uvedený SQL dotaz pro stránkování + distinct
pro vytažení ID záznamů.
Jinak MariaDB asi lépe optimalizuje subquery (= pročístí je od
nepotřebných věcí), ale je těžké ty verze porovnat, každý si jede
svoje.
V jiné situaci mi to vyšlo v Perconě na 15 tabulek (vč. 2 temporary
derived, celkem přes 4 sekundy), zatímco úplně stejný dotaz v MariaDB
7 tabulek (1× temporary derived, čas cca 0,5s), koukal jsem na to jak
blázen.
SELECT COUNT(id_68)
FROM (
SELECT o0_.id AS id_68, o0_.created AS created_2, i1_.id AS id_136, i1_.invoice_id AS invoice_id_137,
i1_.symbol AS symbol_138, i1_.date_created AS date_created_139, i1_.name AS name_143,
i1_.surname AS surname_144, i1_.company AS company_145, i1_.street AS street_146, i1_.city AS
city_147
FROM orders o0_
INNER JOIN order_state o2_ ON o0_.order_state_id = o2_.id
LEFT JOIN invoice i1_ ON o0_.id = i1_.order_id
WHERE o0_.visible = 1) dctrn_result
ORDER BY created_2 DESC
LIMIT 10
OFFSET 0
Editoval cubic (24. 7. 2017 22:07)
- newPOPE
- Člen | 648
@Čamo nie som isty co berie ako datasource ten grid ale pokial chces mat vysledky rychlo tak DQL ti v tomto bude povacsine klast klacky pod nohy.
Optimalnejsie v danych situaciach je proste pouzit native query, namapovat to na nejake ViewEntity (ak ich vobec potrebujes) ktore su len na citanie a tie zobrazovat.
Nieco ale myslim ide aj cez operator NEW
v DQL konkretne 4ty example.
- Čamo
- Člen | 798
Díky za nápady. Dnes som ešte zistil, že sa dá do joinu pridať AND cez \Doctrine\ORM\Query\Expr\Join::WITH , takže to nakoniec dokáže odstrániť duplicity. Akurát, že mi pribudol jeden innerjoin navyšše.
public function findByForDatagrid( array $by )
{
return $this->articleRepository->createQueryBuilder()
->select( 'article', 'user', 'status', 'langs' )
->from( 'App\Model\Entity\Article', 'article' )
->innerJoin( 'article.status', 'status' )
->innerJoin( 'article.user', 'user' )
->innerJoin( 'article.langs', 'langs' )
->innerJoin( 'langs.lang', 'lang', \Doctrine\ORM\Query\Expr\Join::WITH, 'lang.id = :lang' )
->whereCriteria( $by )
->setParameter( 'lang', Entity\Lang::SK );
}
Inak tiež som použil Ublaboo datagrid…
Editoval Čamo (25. 7. 2017 13:58)
- Čamo
- Člen | 798
Teraz ma ešte napadlo, že tá moja multi-step hydration metóda nieje dobre napísaná. Ak sa nemýlim, tak všetky tie väzby a podmienky musia byť rovnaké pre obidve query. Rozdiel má byť iba v tom, čo je v metóde select() tj. čo sa bude hydratovať…
A ešte to nezahŕňa problém so stránkovaním. Ak si dobre pamätám, tak F. Procházka to rieši, cez postFetch event v query objektoch. Doktrína resp. QueryBuilder to ale nepodporuje, že ano?
Vie mi to niekto upresniť? Tiež by nebol odveci nejaký komplexný príklad na tú multistep-hydration so stránkovaním.
public function findBy( $by )
{
$articles = $this->articleRepository->createQueryBuilder()
->select( 'article', 'user', 'status' )
->from( 'App\Model\Entity\Article', 'article' )
->innerJoin( 'article.status', 'status' )
->innerJoin( 'article.user', 'user' )
->innerJoin( 'article.langs', 'langs' )
->whereCriteria( $by )
->getQuery();
// Next result is discarded. This is just re-hydrating the collections.
$articles = $this->articleRepository->createQueryBuilder()
->select( 'partial article.{id}', 'langs' )
->from( 'App\Model\Entity\Article', 'article' )
->innerJoin( 'article.status', 'status' )
->innerJoin( 'article.user', 'user' )
->innerJoin( 'article.langs', 'langs' )
->whereCriteria( $by )
->getQuery();
// Returns ResultSet because of paginator.
return new Kdyby\Doctrine\ResultSet( $articles );
}
Editoval Čamo (26. 7. 2017 10:12)
- newPOPE
- Člen | 648
@Čamo :D nie ze by som nechcel aby sa ti to podarilo. Ale ked to
narves a zabalis do sluzby ktora pusti native query s custom mappingom mas
vyhrate (a bude to rychle) za chvilu. Ak mas Ublaboo tak tam jasne pisu, ze
staci implementovat Object that implements IDataSource
a je.
- Čamo
- Člen | 798
newPOPE
Hlavný problém je že som neni taký skiller. Tieto knižnice používam, aby
mi uľahčili a urýchlili písanie kódu. Písať vlastné entity a objekty mi
pripadá ako pravý opak. Ja si viem napísať a „vyhydratovať“ SQL dotaz
aj cez 8 tabuliek, ale nechcem skončiť s modelom, čo má 2000 riadkov.
Preto sa tomu bránim.
Editoval Čamo (26. 7. 2017 9:56)
- Čamo
- Člen | 798
Nešlo by urobť, že by mal konštruktor
new Kdyby\Doctrine\ResultSet( $articles )
ako druhý parameter
callback ktorý by sa spustil na postFetch()… Len neviem ako by sa ten kód
v callbacku nastránkoval…
EDIT: Nastránkoval by sa proste automaticky cez query, ktoré by ten callback vrátil. A musel by to byť array calbackov…
Editoval Čamo (26. 7. 2017 11:07)