Stromová struktůra v Doctrine s více rodiči

Upozornění: Tohle vlákno je hodně staré a informace nemusí být platné pro současné Nette.
jasin755
Člen | 116
+
0
-

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
+
0
-

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
+
0
-

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
+
0
-
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
+
0
-

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
+
0
-
$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
+
0
-

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)

jasin755
Člen | 116
+
0
-

Rekurzivně je to 1180 dotazů, které trvají 2,5sec a to tam není ještě všechno, opravdu to není špastná volba… To co jsem napsal nahoře položí celkem pouze 2 dotazy za 35ms a funguje to což je trošku rozdíl, akorát jsem to chtěl namapovat do entit

Tomáš Jablonický
Člen | 115
+
0
-

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.

jasin755
Člen | 116
+
0
-

Tak cachování je už jiná kapitola, ale obecne plati, ze web musi byt pouzitelny i bez cache a v tomto pripade to pak moc pouzitelny neni…

leninzprahy
Člen | 150
+
+1
-

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
+
0
-

To je snad jediné co jsem ještě nezkusil. Dám vědět jak to dopadlo. Zatim díky :)

jasin755
Člen | 116
+
0
-

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
+
0
-

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)