Stromová struktůra v Doctrine s více rodiči
- jasin755
- Člen | 116
Zdravím,
měl bych vcelku obecný dotaz. A to jak nejlépe řešit stromové struktury
s Doctrinou v případě, že potomek má více rodičů.
Zkoušel jsem něco sám a narazil jsem problém donutit Doctrine vložit něco do ArrayCollection, tak aby si to pamatovala.
Konkrétně jsem si rekurzivním SQL dotazem (něco jako:)
WITH RECURSIVE category_par (id_cat) AS (
SELECT
C .id_category,
cp.parent_id,
C . NAME,
0 AS depth,
cast('' AS varchar) AS parent_ids,
vytáhnul vcelku dobrý výsledek kde mám id_category a k tomu parent_ids ve stringu oděleny čárkami. Něco jako (,3,4,5,6). Dotaz je velmi rychlý.
Dokážu z toho udělat bez problému pole se stromovou strukturou, kterou
potřebuji, ale z jistého důvodu potřebuji aby to vše byli entity.
Zkoušel jsem tedy dalším dotazem vytáhnout všechny kategorie
něco jako:
$categories = $this->entity_manager->getRepository('Entity')->findAssoc(array('deleted' => 0),'id_category');
Mám rekurzivní funkci (která asi není uplně košér, ale na testování dostačuje zatím):
private function addValueToArray(&$category_entites,&$array = NULL,&$indexes,&$value,$i = 0){
if(count($indexes) === (int)$i){
$array->setChildren($value);
}else{
if(!is_object($array) && !empty($array[$indexes[$i]])){
$this->addValueToArray($category_entites,$array[$indexes[$i]],$indexes, $value,$i+1);
}
}
}
kde volám právě:
$array->setChildren($value);
Když si pak nad entitami iteruji, a volám $category->getChildren(), tak vznikne lazy loading a začne mi to pokládat dotazy na children na databázi.
Lze nějak donutit, aby si Doctrina brala věci co ji tam nasetuji, aniž bych musel něco flushovat?
Díky moc za odpovědi.
- Tomáš Jablonický
- Člen | 115
Nestačilo by si vytáhnou entitu úrovně, kterou potřebuješ? Pak by nemělo být probléme iterovat nahoru přes ->getParent() nebo dolu k potomkům přes ->getChildren().
http://doctrine-orm.readthedocs.org/…mapping.html#…
Edit: getChildren ti sice vrátí potomky ale inicializace se provede až když je to potřeba. Jinak myslím, že KDYBY vrací přímo array s klíči aniž by provedlo inicializaci entit.
Přes DQL by to šlo, ale musel by jsi se rekurzivně dotazovat na potomky dané větve a pro úsporu času si vracet array namísto Entity object.
Lze nějak donutit, aby si Doctrina brala věci co ji tam nasetuji, aniž bych musel něco flushovat?
Jak je toto myšleno? „Flusuješ“ jen tehdy, když jsi provedl nějakou změnu entity a chceš aby se to projevilo v DB. Když něco taháš z DB, tak přeci flush vůbec nepoužiješ.
Editoval jablon (20. 1. 2015 14:00)
- jasin755
- Člen | 116
Pole vypadá ve výsledku nějak tahle když ho nechám vytvořit:
[1] => array(
'id' => 1,
'name' => 'Kategorie'
'child' => array(
[2] => array(
'id' => 2,
'name' => 'Potomek',
'child' => array(
[3] => array(
'id' => 3,
'name' => 'Potomek potomka',
'child' => NULL
)
)
)
)
)
atd… prostě stromová struktura v poli, ale to nepotřebuji přesně. Jinak jak to to mysliš entitu úrovně kterou potřebuji? Tím findAssoc() se vytáhnou uplně všechny entity a neřeší se jestli jsou něčí rodiče nebo ne a následně se to snažím pomocí setteru to přidat do kolekce children. Jinak potřebuji všechny úrovně. Metoda vypisuje celý strom kategorií a nechci pokádat rekurzivní dotazy nad DB.
- jasin755
- Člen | 116
Lze nějak donutit, aby si Doctrina brala věci co ji tam nasetuji, aniž bych musel něco flushovat?
Myšleno tak, že když mám např. 2 kategorie a ta 2. s ID 2 je zanořená v té 1. s id 1
Kategorie (1)
|__Podkategorie (2)
a udělám něco jako:
$kategorie = $this->entity_manager->find('Category',1);
$podkategorie = $this->entity_manager->find('Category',2);
//Schválně napsané blbě přes getter a pak add aby bylo vidět, že se to snažím přidat do kolekce
$kategorie->getChildren()->add($podkategorie);
//Tady nechci aby se pokládali dotazy do DB, ale vzalo to, co jsem nasetoval o par řádku výše
foreach($kategorie->getChildren() AS $children){
echo $children->getId();
}
- Tomáš Jablonický
- Člen | 115
No já tady prostě nevidím důvod proč si nevzít Entity(Kategorie id = 1) a pak z ní přes getter nevzít její potomky (v tomto případě Podkategorie id = 2).
<?php
$kategorie = $this->entity_manager->find('Category',1);
$podkategorie = $this->entity_manager->find('Category',2);
//Schválně napsané blbě přes getter a pak add aby bylo vidět, že se to snažím přidat do kolekce
$kategorie->getChildren()->add($podkategorie);
//Tady nechci aby se pokládali dotazy do DB, ale vzalo to, co jsem nasetoval o par řádku výše
foreach($kategorie->getChildren() AS $children){
echo $children->getId();
}
?>
To co navrhuješ je stejné – prostě znovu inicializuješ Entitu a nastavuješ jí zbytečně children.
<?php
$kategorie->getChildren()->add($podkategorie); //tady si dokonce přidáš znovu podkategorii, takže dojde k duplicitě ve stromu
?>
Metoda vypisuje celý strom kategorií a nechci pokádat rekurzivní dotazy nad DB.
Pokud to budeš chtít dělat přes DQL, tak se rekurzi nevyhneš. Tam ale můžeš hodně ušetřit čas a vracet si namísto pole entit jen pole s daty.
<?php
$em->createQueryBulder()
->select('c')
->from('Category', 'c')
->where('c.parent IS NULL')
->getQuery()
->getScalarResult();
//dotaz vrací les - vsechny koreny kategorii jen jako pole, nikoliv jako entity.
?>
Edit: Pokud ti jde o rychlé vypsání kategorií na Frontendu, stačí si to vypsat jednou a výsledek „zacachevat“ třeba na půl hodiny nebo dokud nedojde ke změně ve struktuře stromu.
Editoval jablon (20. 1. 2015 14:24)
- jasin755
- Člen | 116
$qb->createQueryBulder()
->select('c')
->from('Category', 'c')
->where('c.parent IS NULL')
->getQuery()
->getScalarResult();
toto DQL ti vytáhne jenom nejnadřazenější kategorie, jakmile se zeptáš na childrenm tak začneš spamovat databází a hlavně nechci ScalarResult, ale nomrmální Result() do pole si to umím nastrkat jedním nativním SQL dotazem, který jsem psal výše. Výše uvedene příklady jsou samozřejmě strašná kravina, ale nevím jak lépe vysvětlit to čeho chci dosáhnout.
- Tomáš Jablonický
- Člen | 115
jasin755 napsal(a):
$qb->createQueryBulder() ->select('c') ->from('Category', 'c') ->where('c.parent IS NULL') ->getQuery() ->getScalarResult();
toto DQL ti vytáhne jenom nejnadřazenější kategorie, jakmile se zeptáš na childrenm tak začneš spamovat databází a hlavně nechci ScalarResult, ale nomrmální Result() do pole si to umím nastrkat jedním nativním SQL dotazem, který jsem psal výše. Výše uvedene příklady jsou samozřejmě strašná kravina, ale nevím jak lépe vysvětlit to čeho chci dosáhnout.
No samozřejmě, že to vytáhne jen kategorie první úrovně – měl to být jen příklad na to jak to udělat rychleji než přes getResult(). V rekurzi pak jen nahradíš ->where(c.parent IS NULL') za ->where(‚c.parent=?1‘) kde parametr je ID rodiče …
Samozřejmě si můžeš vytáhnout v dotazu všechny kategorie a pak to splácat do stromu – ale to bude docela opruz, a nevím jestli je lepší vyměnit udržovatelnost kódu za rychlost výpisu.
Já osobně bych to neřešil. Vypsal bych si to rekurzivně, tak nejrychleji a čistě jak to jen jde, a použil cache (kolikrát za den změníš strukturu kategorie?).
A co myslíš, že ti vrátí ->getResult()? Zase inicializované Entity …
Editoval jablon (20. 1. 2015 14:38)
- Tomáš Jablonický
- Člen | 115
jasin755 napsal(a):
Rekurzivně je to 1180 dotazů, které trvají 2,5sec
Udělej tuhle rekurzi a pak si výstup dej do \Nette\Caching\Cache pod Tagem. Další zavolání již nebude mít žádné dotazy na DB (vracíš obsah Cache) a budeš možná na menším čase než jsi na tom tvém algoritmu. V rekurzi nepoužívej ->getResult ale getScalarResult aby jsi ušetřil i čas na inicializací Entit.
Když provedeš editaci kategorie, tak si pak smažeš Tag Cache a celý proces se provede znovu.
- leninzprahy
- Člen | 150
Doctrine nativní dotazy podporuje, jen si musíš napsat vlastní mapování na entity. Z příkladů je použití celkem zřejmé :)
- jasin755
- Člen | 116
Tak bohužel tudy cesta asi taky nepovede. Doctrina si sama nepřiřadi asociace. Takže pokud napíšu např.:
$rsm = (new ResultSetMapping())
->addEntityResult('Category','c')
->addFieldResult('c', 'id_category', 'id_category')
->addFieldResult('c', 'name', 'name')
->addJoinedEntityResult('Category', 'cp', 'c', 'parents')
->addFieldResult('cp', 'parent_id', 'id_category');
a zavolám ArrayResult() abych se kouknul na strukturu tak mi vznikne např.:
array(
array(
0 => array(
id_category => 10,
name => Child2Kategorie
parents => array(
0 => array(
id_category => 9,
name => Child1Kategorie
)
),
1 => array(
id_category => 9,
name => Child1Kategorie,
parents => array(
0 => array(
id_category => 8,
name => TopKategorie
)
)
)
coz je blbě. Výsledek by měl být takový:
array(
array(
0 => array(
id_category => 10,
name => Child2Kategorie
parents => array(
0 => array(
id_category => 9,
name => Child1Kategorie,
parents => array(
0 => array(
id_category => 8,
name => TopKategorie,
parents => array()
)
)
)
)
a takovou věc bohužel asi namapovat ani nepůjde. Googlil jsem zda jsou vytvářet dynamické aliasy v PostgreSQL, ale bohužel.
Přidávám ještě nativní SQL které volám:
WITH RECURSIVE category_par (id_category) AS (
SELECT
c.id_category,
cp.parent_id,
c.name
FROM category AS c
LEFT JOIN category_parent AS cp ON c.id_category = cp.id_category
WHERE
cp.parent_id IS NULL
UNION ALL
SELECT
c.id_category,
c.parent_id,
cc.name
FROM
category_parent C
NATURAL JOIN category cc
INNER JOIN category_par cp ON c.parent_id = cp.id_category
)
SELECT * FROM category_par
- jasin755
- Člen | 116
Tak jsem nakonec nějaké řešení našel. Podotýkám, že je použitelné pro ty co potřebuji pracovat nutně s entitami (kompletními nebo nekompletními). Pro ty, kterým stačí výsledek ve scaláru stačí použit výše uvedeny rekurzivní SQL dotaz (funguje na PosgreSQL, na MySQL nikoli).
Výsledná metoda pro výpis kategorií vypada následovně:
public function getCategoryList(){
$sql = "WITH RECURSIVE category_par (id_cat) AS (
SELECT
c.id_category,
1 AS depth
FROM
category AS c
LEFT JOIN category_parent AS cp ON c.id_category = cp.id_category
WHERE
cp.parent_id IS NULL
UNION ALL
SELECT
c.id_category,
cp.depth + 1
FROM
category_parent c
NATURAL JOIN category cc
INNER JOIN category_par cp ON c.parent_id = cp.id_cat
) SELECT MAX(depth) AS depth FROM category_par";
$depth = $this->getEntityManager()->getConnection()->executeQuery($sql)->fetchColumn();
$query = $this->getEntityManager()->createQueryBuilder()
->from('Category','c1');
$i=1;
$select = array();
while($i <= $depth){
$select[] = 'partial c'.$i.'.{id_category,name}';
$query->leftJoin('c'.$i.'.children','c'.(++$i));
}
return $query->select($select)
->where('c1.defaultcategory IS NULL')
->getQuery()
->getResult();
}
Výsledkem jsou 2 dotazy. Jeden trvající 8ms a druhý 15ms s celkým časem běhu scriptu 683ms (v develop módu) Určitě by to šlo optimalizovat hlavně co se týče získávání maximální hloubky zanoření.
Pokud by někoho napadl lepší způsob určitě ho uvítám :)
EDIT:
Podmínkou je, že entita Category musí mít asociasi defaultcategory, která
určuje jaká je výchozí a nejnadřazenější kategorie musí mit tuto
asociaci NULL
Editoval jasin755 (20. 1. 2015 20:01)