Addon objektový přístup k datům z Nette pro Dibi – nORMa

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

Tak jsem se odhodlal a zkusím přispět taky svojí troškou do mlýna :)
Dovolte mi, abych Vám představil addon nORMa :)
Pro svojí potřebu jsem si udělal takovou jednoduchou ORM nadstavbu nad Dibi a Nette.

Celý system pracuje s Entitama a kolekcemi entit. Definice entit je (skoro) ala Doctrine2:
(Doctrine2 mě inspirovalo, ale je to na mě moc robustní a nepřehledný :) )

<?php
	/** @table orders */
	class Objednavky {
		/**
		 * @type Integer
		 * @identifier		// určuje primary key v tabulce
		 */
		public $id;
		/**
		 * @type String
		 * @column nejaky_popis	// mapovaní názvu property na sloupec v tabulce, pokud se nezadá, mapuje se název proměné
		 * @nullable 		// může být null
		*/
		public $muj_popis;
		/**
		 * @type Collection
		 * @collection Polozky
		 * @rule(table=polozky, key=id, remoteKey=orderID)
		*/
		public $polozky;
		/**
		 * @type String
		 * @leftjoin(table=users, key=user_id, remoteKey=id)
		 * @column user_name
		*/
		public $uzivatel;
		/**
		 * @type Integer
		*/
		public $stav;
	}
	/** table @order_items */
	class Polozky {
		/**
		 * @identifier
		 * @type Integer
		*/
		public $id;
		/**
		 * @type Integer
		*/
		public $order_id;
		/**
		 * @type String
		*/
		public $nazev;
	}
?>

V této třídě se definují jen jednotlivé property, nic jiného se tu nepíše. Systém se sám stará o validaci typů, dle zadané definice a mužete si sami definovat vlastní typ dat:

<?php
	class TypKladnyInt extends Norma\BaseDataType{
		public function validate($value){
			return(is_int($value)&&$value>=0);
		}
	}
?>

A pak už jen zadat do definice entity @type TypKladnyInt.
Vlastní použití je následující:

<?php
	use Norma;
		...
		...
	$em=new EntityManager($configDB);
		/* $configDB je název sekce v .ini s pripojením k db
		   pokud se nezadá, bere se default 'database'
		   takto se může instancovat víc EntityManageru s připojením k různým db
		*/
	$em->useTransaction(true);	// zatím se testuje a není plně funkční
	$em->useCache(true);		// zatím nepoužívat, dělá se na tom :)

	$foo=$em->getOne('objednavky',5);  //vytvoří entitu dle třídy Objednavka s id=5
	$foo['muj_popis']='blabla'; // entita implementuje ArrayAccess takže se dá použít
	$foo->muj_popis='blabla'; // nebo takhle

	$foo= new Entity('objednavky'); // vytvoření nové položky v db
	$foo->persist();

	$foo=$em->getAll('objednavky',array('stav'=>1)); // instancování kolekce

	/* dají se předat i doplňující parametry: */
	       $options = array(
                 'orderBy' => array(
                   'id' => 'asc',
                 ),
                 'limit' => 3,
                 'offset' => 2,
                 'assoc' => 'id',
	       );
	$foo=$em->getAll('objednavky',array('stav'=>1),$options);

		/* vypsaní kolekce */

	foreach($foo as $objednavka){
		echo $objednavka->muj_popis;
	}
	echo $foo[1]->muj_popis;
?>

Čtení a zápis do db se snažím optimalizovat (stalé na tom pracuju), takže třeba:

<?php
	$kolekce=$em->getAll('objednavky',array('stav'=>1)); // předpokladejme, že se vrati objednavky s id (1,2,3,4,5)
	$entita=$em->getOne('objednavky',2);		    // už se znova do db nesahá - id 2 už je načteno

	// to same při zapisu

	$kolekce->muj_popis='blabla';
	$entita->muj_popis='bubu';
	$em->flush();		// provede dva zapisy s (where stav=1) a (where id=2)

	// ale:
	$entita->muj_popis='bubu';
	$kolekce->muj_popis='blabla';
	$em->flush()		// provede jen jeden zápis (where stav=1), protože kolekce přepsala dříve zadanou hodnotu
?>

Kolekce může být i součástí entity, např entita Objednávky obsahuje kolekci položky objednavky (viz definice v úvodu):

<?php
	foreach($entita->polozky as $polozka){
		echo $polozka->nazev;
	}

	// do kolekcí se dá vkládat:
	$entita->polozky->add();	// vloží novou prázdnou
	$foo=new Entity('polozky');
	$foo->nazev='blabla';
	// nebo $foo=$em->getOne('polozky',5); // přesune už existují entity do této kolekce
	$entita->polozky->add($foo);	// vloží už předvyplněnou
		// !!! pokud zvolíte jinou asociaci v poli $options než primary key, musíte zajistit naplnění teto property před předáním entity kolekci

	// nebo mazat
	$entita->polozky->delete(5); 	// smaže položku s id=5
	$entita->delete();		// smaže entitu. Zatím nejsem rozhodnutý, jestli se májí máznout i data subkolekcí, ale asi jo...

	$entita->polozky->clear();	// vyprázdní kolekci - položku budou smazány v db!!!
?>

Všechny UPDATE a DELETE se provádějí až po příkazu $em->flush(). Akorát INSERT se provede okamžitě při persistování, kvůli získání primárního klíče k identifikaci.
System umí vyhazovat vyjímky při různých událostech např: zápis do neexistující property, chyba typu, zápis do readOnly, pokus o přepsání identifikátoru…
Ještě je na tom spousta práce a hlavně optimalizace kodu (a vymazání různého balastu v kodu), ale už mi to běhá v jednou projektu a zatím jsem spokojen :)
Rychlosti jsem neměřil, ale jak říkám – je to před optimalizací.
Momentálně jsou rozpracovaný ještě dva doplňky a to sice:
TableComponent – vytvoří komponentu ze zadané entity(kolekce) a zobrazí tabulku, včetně paginátoru, změny řazení, rozklikávací sub tabulky s vloženýma kolekcema… – vývoj zamrznul, potřebuji dynamické snippety :(
FormComponent – vytvoří komponentu formůláře včetně validace dat a správného zobrazení prvku (vazba article->category se zobrazí jako rozbalovací seznam katergorií atd.)

Určitě jsem ještě zapoměl na něco co to děla a umí, tak jestli by to někoho zajímalo, klidně se ptejte.
Zdroj je k mání na gitu: git://github.com/rampa/nORMa.git (aktuální v branchi daily)
Dokumentace ještě není, ale pracuji na ní. Použil jsem Apigen, tak už jen dodělávám ty otravný annotace :)
Dále je rozpracovaná automatická tvorba entit z SQL dotazu, používání cache a zpožděné čtení dat, ale dělám na tom :)

Editoval Rampa (15. 12. 2010 13:54)

Filip Procházka
Moderator | 4668
+
0
-

Takže už jsi si také napsal vlastní ORM a teď už ti nic nebrání přejít na Doctrine2 :)

Rampa
Člen | 65
+
0
-

HosipLan napsal(a):

Takže už jsi si také napsal vlastní ORM a teď už ti nic nebrání přejít na Doctrine2 :)

No… chvilku jsem se s Doctrinou zabýval, ale je to moc obsáhlý… 333 souborů a třeba to neumí vlastní associace entit (nebo jsem aspoň nepřišel na to jak)
Např jsem potřeboval udělat něco jako dibi…fetchAssoc(‚timestamp‘) :)

Panda
Člen | 569
+
0
-

Sice trochu omezenější, ale funkční je INDEX BY.

arron
Člen | 464
+
0
-

Klobouk dolu, neco takoveho bych asi nikdy nenapsal :-) Nicmene co do vizaze a pouziti je to Doctrine 2 v bledemodrem :-) (kdybych nevedel, ze je to Tvoje ORM, tak podle zapisu bych se ochotne vsadil, ze je to Doctrine 2 ;-))

Pravdicka ale je, ze mi v Doctrine 2 opravdu chybi fetchAssoc…premyslel jsem, zkusit to dopsat sam, ale zatim se mi nepodarilo rozlustit klicove casti doctrine, abych z nich mohl cerpat. Nicmene nakonec jsem dosel k tomu (kdyz jsem premyslel, k cemu vlastne fetchAssoc je), ze to v podstate ani neni potreba, protoze data v podobnem formatu dostavam z Doctrine 2 nativne a na vsechno ostatni je INDEX BY :-)

Filip Procházka
Moderator | 4668
+
0
-

fetchAssoc si můžeš přece napsat jako nějaký array tool helper (nápad) a pak si ho volat na přeskládání v potřebných funkcích v repozitáři entity :)