Bezbolestná integrace Doctrine 2 ORM do Nette – Kdyby/Doctrine

Upozornění: Tohle vlákno je hodně staré a informace nemusí být platné pro současné Nette.
Filip Procházka
Moderator | 4668
+
0
-

Zdravím,
Kdyby/Doctrine je doplněk pro Nette, který do něj integruje Doctrine 2 ORM.

Samozřejmostí je podpora stable i dev Nette a také nejnovější verze Doctrine (tedy 2.4@stable).

Rozšíření je klasicky na Githubu k vašim službám včetně dokumentace, nejlépe ho instalujte pomocí composeru. Chyby nebo nápady na vylepšení hlaste prosím na githubu

Instalace

Pokud chcete používat vývojové Nette, tak do composer.json dáme následující

"require": {
	"nette/nette": "@dev",
	"kdyby/doctrine": "@dev",
	"kdyby/annotations": "@dev",
	"kdyby/doctrine-cache": "@dev",
	"kdyby/events": "@dev",
	"kdyby/console": "@dev",
}

Pro stable Nette je to výrazně jednodušší

"require": {
	"nette/nette": "~2.0",
	"kdyby/doctrine": "~0.10",
}

(Pozn.: řada 0.* je pro ~2.0@stable Nette, řada 1.* je pro ~2.1@dev Nette)

Otázky směřujte sem

Další otázky prosím zakládejte jako samostatná témata na novém fóru help.kdyby.org. Díky!

Na co se těšit

akadlec
Člen | 1326
+
0
-

Měl bych dotaz. Když se vytvoří služba takto:

services:
    articles: App\Articles(@doctrine.dao(App\Article))

Tak se ty nativní DQL skládají přímo v presenteru/komponentě kde se ta služba používá? V současné chvíli totiž používám jinou extension pro doctrine a mám to rozdělené na Entitu – Fasádu – Repozitář a jako službu si předám fasádu která podědí základní metody repozitáře a DQL které potřebuju navíc si pak poskládám v repozitáři.

Jak to tedy funguje s tvým rozšířením?

EDIT:
Takže jsem to pochopil trošku špatně a našel tento článek: http://www.frosty22.cz/…ccess-object

Pokud to tedy již chápu dobře, to co já mám teď jako fasádu je u kdyby model a repozitář zůstává prakticky bezezměn, je tak?

Editoval akadlec (18. 9. 2013 9:17)

Filip Procházka
Moderator | 4668
+
+1
-

Když použijeme termíny na které jsi zvyklý, tak moje služby App\Articles jsou už v podstatě tvoje Facade. DQL můžeš psát přímo do nich, nebo do samostatných tříd.

Hlavní myšlenkou tohoto návrhu je nedědit už dále DAO, protože tím porušuješ SRP

akadlec
Člen | 1326
+
0
-

Ok chápu. A to co je uvedeno v tom článku je korektní příklad?

Filip Procházka
Moderator | 4668
+
0
-

Nelíbí se mi dědit dao jen kvůli autowiringu. A ty operace v konstruktoru jsou sice jen ilustrační, ale určitě matoucí.

akadlec
Člen | 1326
+
0
-

Jo ten konstruktor se mě tam taky nelibí a jako ilustrace to možná mate více než je zdrávo. Ad dao, je tedy lepší si nedělat ty vlastní classy dao pro jednotlivé modely a využívat tak jen dao z kdyby?

Filip Procházka
Moderator | 4668
+
0
-

Přesně tak :) Zase tolik psaní to není, je to pár znaků a navíc nemusíš předávat všechny, protože si můžeš další DAO vytáhnout pomocí related

Jiří Nápravník
Člen | 710
+
0
-

Pěkné rozšíření, ale dneska by dalo zabrat, nevím, jeslti je to moje neznalost, či kde je problém. Zkrátka mám entitu Game, ta má v sobě kolekci $pictures (OneToMany). V kódu jsem potom k této kolekci přistoupil, upravil mj. jeden atribut v ní a uložil. Pseudokód

$game = $this->gameDao->find(1);
$game->getPictures()->get([0])->setPath($newPath);
$this->gameDao->save($game);

no a po uložení se v databázi ona změna neprovedla. Ŕíkal jsem si ok, tak nebero to prvky v kolekci, zavolám $this->gameDao->flush(); nicméně ani to změnu neprovedlo. Musím si exciplitně vytáhnout entitymanager a přes něj zavolat flush() a pak změna proběhne. Chápu to tedy tak, že pokud volám přes dao objekt metodu save/flush, tak se ty změny provádí jenom v rámci entity, ke které je DAO vázáno. Chápu to dobře? Je to opravdu žádoucí, nebylo by lepší pokud by alespoň flush u dao flushnul všechny změny a nemusel se tahat explicitně EntityManager?

Filip Procházka
Moderator | 4668
+
0
-

Ano, je to tak schválně. Jen jsem se ještě za toho 3/4 roku co to používám ještě nerozhodl, jestli je to žádoucí :)

Na druhou stranu, jsi si jistý že ten kód co jsi popsal je nejlepší možný způsob jak s tím pracovat?

$firstPicture = $this->picturesDao->findBy(array('game' => 1), array('id' => 'ASC'), +);
$firstPicture->setPath($newPath);
$this->picturesDao->save($firstPicture);

Na takovéty běžné operace je DAO super, ale jakmile zpracováváš něco většího, tak není od věci si předat i ten EntityManager.

Jiří Nápravník
Člen | 710
+
0
-

Díky, jak jsem tak nad tím přemýšlel, tak mi došlo, že to přeci jen dává asi spíše smysl, jak ot je teď implementováné, že by se zkrázka dao mělo starat opravdu jen o ty entity, které jsou jeho, jen to občas může holt zmást:-)

Ten kód byl spíše pseudokód + hodně zkrácený, abych naznačil, co myslím.

EntityManager si tady předám, ať mi to proběhne hezky v jedné transakci. Což je právě taky podle mě trochu problém toho řešení současného, že to neflushuje všechny změny, zkrátka když pracuji s více DAO a entitami v jedné mětodě, tak většinou chci, aby to proběhlo v jedné transakci, a tak nemohu využít save(), ale musím nakoenc sáhnout pro entitymanager. Ale sám nevím, co je lepší:)

zimmi
Člen | 94
+
0
-

Po instalaci přes composer dostávám

Nette\InvalidStateException

Ambiguous class Doctrine\Tests\DoctrineTestCase resolution; defined in /home/michal/www/doctrine/libs/doctrine/collections/tests/Doctrine/Tests/DoctrineTestCase.php and in /home/michal/www/doctrine/libs/doctrine/cache/tests/Doctrine/Tests/DoctrineTestCase.php.

Jak z toho ven?

jiri.pudil
Nette Blogger | 1029
+
0
-

Nenačítat vendor složku RobotLoaderem. Použij autoloader generovaný Composerem (vendor/autoload.php).

zimmi
Člen | 94
+
0
-

@jiri.pudil Díky!

zimmi
Člen | 94
+
0
-

Zdravím!
Rád bych použil kdyby/doctrine v kombinaci s o5/Grido, který jako zdroj přejímá Doctrine/Querybuilder.
Mám tedy entitu User, která dědí od IdentifiedEntity, s vlastnostmi username, email, password atd.

S kódem dole dostávám RuntimeException Not all identifier properties can be found in the ResultSetMapping: id. Id je přitom děděné z IdentifiedEntity a v selectu je taky uvedeno.

$users = $this->em->getDao(User::getClassName());
$u = $users->createQueryBuilder()
            ->select('u.id')
            ->from('AdminModule\User', 'u');

Když na tom kódu spustím getQuery() a teprve ten potom execute()-uju, vrací správný výsledek. Pokud byste někdo měl nějaký nápad, sem s ním.

Přikládám ještě část IdentifiedEntity, kde je vlastně jen změněno namapování vlastnosti id na sloupec guid v db.

abstract class IdentifiedEntity extends BaseEntity
{

	/**
	 * @var string
	 * @ORM\Id
	 * @ORM\Column(type="string", name="guid")
	 * @ORM\GeneratedValue
	 */
	protected $id;
	...
Filip Procházka
Moderator | 4668
+
0
-

Problém je v tomto řádku

->select('u.id')

Používáš hydrataci, která vytahuje entity, ale dovoluješ mu do SQLka dát pouze nějaký sloupec. To není možné takto kombinovat.

Buďto použij hydrataci na pole, nebo vytahuj celou entitu

->select('u')

Teoreticky můžeš ještě vytáhnout entitu jenom s idčkem

->select('partial u.{id}')

ale to se z různých důvodů nedoporučuje, protože je to pokročilá optimalizační technika a mohl by sis nadělat v aplikaci dost nepříjemný bugy (kterých si ani nemusíš všimnout!).

zimmi
Člen | 94
+
0
-

Díky, pomohlo.

Stic
Člen | 28
+
0
-

Chcel by som sa spytat, ci je normalne ze po instalacii Kdyby/Doctrine mi stupol execution time radovo 3–6x? To som zatial vytvoril len jednu entitu ktora je vyuzita na uplne banalny vypis, aby som vyskusal funkcionalitu…

Execution time predtym na localhoste bol radovo 50–100ms po zacacheovani (200ms bez cache), po integracii Kdyby/Doctrine sa k tejto hodnote ani len nepriblizim a dostavam sa radovo na 300–600ms (bez cache aj okolo 1s).

Chapem ze doctrine nie je dokonaly, ale to naozaj zavadza az taky velky overhead? Uvazoval som totiz nad tym, ci nebude jednoduchsie vyuzit doctrine, pretoze kody vyzerali prehladnejsie, ale s takym navysenim execuion time mi to pride uplne useless, kedze 1s oneskorenie uz len na strane serveru mi pride ako riadny overkill…

Filip Procházka
Moderator | 4668
+
0
-
  1. Zapni nějaké cachování (apc/redis/memcache), bez memory cache se doctrine naprosto reálně používat nedá (ani čistá).
  2. Kdyby žádný overhead nepřidává, pouze v development módu hodně kontroluje, jestli jsi něco nezměnil, aby se ti aplikace chovala dle očekávání. Zapni aplikaci v produkčním módu a pak to změř.

Moje aplikace se načítá 2–8 vteřin v development módu a 200–600ms v produkčním módu na serveru.

Stic
Člen | 28
+
0
-

Ak to spravne chapem, tak Kdyby/Doctrine uz samozrejme DQL prelozene do SQL ma cacheovane pomocou nette nie? Teda tato konfiguracia uz je zbytocna, ci? A ako sa da pripadne v config.neon nastavit Doctrine na memcache?

Filip Procházka
Moderator | 4668
+
0
-

@Stic: Tohle nastavení za tebe řeší Kdyby. Tyhle čtyři nastavení si můžeš změnit na jednu z podporovaných hodnot

Trošku jsem vylepšil práci s memcachí, protože jsem si všiml, že ten výchozí driver má setter a blbě by se konfiguroval, teď (po aktualizaci Kdyby/Doctrine) by to mělo jít nějak takto

doctrine:
	metadataCache: memcache(@memcache.client)
	queryCache: memcache(@memcache.client)
	resultCache: memcache(@memcache.client)
	hydrationCache: memcache(@memcache.client)

services:
	memcache.client:
		class: Memcache()
		setup:
			- addserver(localhost, 11211)

A nebo můžeš nastavit pro celou aplikaci datovou cache na memcache a protože Kdyby/Doctrine by default používá systémovou datovou cache, bude se používat ta.

services:
	cacheStorage:
		class: Nette\Caching\Storages\MemcachedStorage()
enumag
Člen | 2118
+
0
-

@Filip Procházka: Pokud chci použít kdyby/redis, je také nutné nějaké podobné nastavení?

Filip Procházka
Moderator | 4668
+
0
-

@enumag: ne, stačí jenom zapnout storage, který nahradí ten systémový a tedy ho bude automaticky používat i Kdyby/Doctrine.

enumag
Člen | 2118
+
0
-

@Filip Procházka: A jak to funguje? RedisExtension v podstatě jen zaregistruje služby. Očekával bych tedy, že u memcache bude také stačit vytvořit službu memcache.client a předefinovat službu cacheStorage. Rád bych pochopil kde je rozdíl že to u memcache nestačí a u redisu ano. ;-)

Filip Procházka
Moderator | 4668
+
0
-

Přečti si ten můj příspěvek o memcachi znovu :)

Rozdíl je v tom, že když tu memcache narveš rovnou do doctriny a nebudeš tam nechávat mezikrok v podobě Nette\Caching\Cache bude to rychlejší (ale přijdeš o zamykání).

enumag
Člen | 2118
+
0
-

@Filip Procházka: Aha, srry. :-D Už mi to došlo. A kdybych chtěl použít Redis pouze pro doctrine tak by to mělo jít takhle:

kdyby.doctrine:
    metadataCache: redis(@kdyby.redis.driver)
    ...

Editoval enumag (28. 10. 2013 14:45)

zimmi
Člen | 94
+
0
-

Zdravím,
potřeboval bych mít možnost sestavit EntityManager na základě uživatelské identity. Pokud bude uživatel nepřihlášený, použijí se pro nastavení Doctrine údaje uvedené v config.neon, pokud se ale přihlásí, chtěl bych vytáhnout Nette\Security\IIdentity a pro přihlášení do DB použít jméno a heslo právě přihlášeného uživatele.

Lze teda nějak v configu přepsat defaultní EntityManager? Pokud si teď podědím Kdyby\Doctrine\EntityManager a zaregistruju ho do services, dostávám Service 'doctrine.dao': Multiple services of type Kdyby\Doctrine\EntityManager found: doctrine.default.entityManager, manager.

frosty22
Člen | 373
+
0
-

Vzhledem k tomuhle řádku https://github.com/…xtension.php#L242

bych řekl, že by to snad mohlo jít v configu přepsat jako:

doctrine: # za předpokladu, že máš tu extension pojmenovano tak
	orm:
		defaultEntityManager:
			class: Kdyby\Doctrine\EntityManager
			create: Muj\EntityManagerFactory

Editoval frosty22 (17. 11. 2013 14:41)

zimmi
Člen | 94
+
0
-

@frosty22: Zkoušel jsem něco podobného, teď jsem tedy zkusil i tebou navrhovanou variantu, smazal cache, ale stejně tam zůstává defaultní EM. Čekal bych, že to na mě zařve, že neexistuje definovaná třída Muj\EntityManagerFactory, ale to se bohužel taky neděje.

frosty22
Člen | 373
+
0
-

Ona je možná ta cesta špatně, nejsem si jistý odkud to tenhle řádek bere:

isset($builder->parameters[$this->prefix(‚orm.defaultEntityManager‘)])

ale myslím, že není problém, když si tam hodíš dump($builder->parameters) a zjistíš, co to je za parametry .. možná to je:

doctrine: # za předpokladu, že máš tu extension pojmenovano tak
    orm.defaultEntityManager:
         class: Kdyby\Doctrine\EntityManager
         create: Muj\EntityManagerFactory
enumag
Člen | 2118
+
0
-

@frosty22: Já ti nevím ale nemá být tohle spíš v sekci services?

frosty22
Člen | 373
+
0
-

@enumag no já myslím, když ty služby jsou tam přidané přes extension a v té je tedy i použito $this->prefix(…), což ti zaregistruje ty služby pod vlastní namespace, který je podle toho názvu, jak máš přidanou extension – v tomhle případě doctrine.

Nebo jestli myslíš, že to registruje pod:

services:
	doctrine:
		orm:
			defaultEntityManager: ....

je to možné, ale já si myslím že ne kvůli případné kolizi názvů .. ale není nic jednoduššího než to zkusit .. :)

enumag
Člen | 2118
+
0
-

@frosty: No služby se běžně předefinovávají takhle (ale možná má Kdyby/Doctrine nějaký jiný mechanismus):

services:
	doctrine.orm.defaultEntityManager: ...
zimmi
Člen | 94
+
0
-

@frosty22 @enumag vyzkouším hned, jak se k tomu dostanu.

Jiří Nápravník
Člen | 710
+
0
-

Můžu se zeptat, jak to vypadá s integrací Nette formulářů a validátory? Koukal jsem, že na GitHubu je DoctrineForms, je to už použitelné nebo zatím spíše náčrt? Případně nějaký sample?

Filip Procházka
Moderator | 4668
+
0
-

DoctrineForms ještě nejsou ready. Je tam jen jednoduché mapování a validátor je spíš jenom náčrtek :) Až to bude použitelné tak dám určitě vědět.

songoo
Člen | 13
+
0
-

Ako by som vedel zmazat a znovu vytvorit databazu priamo v prezenteri ? (bez rucneho spustania „orm:schema-tool:drop –force“ a „orm:schema-tool:drop“ , pre selenium testy )

Editoval songoo (24. 11. 2013 21:12)

Filip Procházka
Moderator | 4668
+
0
-
$schemaTool = new Doctrine\ORM\Tools\SchemaTool($em);
$schemaTool->dropSchema($em->getMetadataFactory()->getAllMetadata());
$schemaTool->createSchema($em->getMetadataFactory()->getAllMetadata());

Volá se to nad Connection toho EntityManageru, tak bacha ať si nedropneš produkční databázi ;)

songoo
Člen | 13
+
0
-

Diky moc ide to :)

duskohu
Člen | 778
+
0
-

Caute snazil som sa rozchodit Kdyby\Doctrine, ale hned na zaciatku som narazil na problem, v app mam nette.ajax.js, teraz nie je dolezite ze preco tam mam, ale robi mi to jeden problem ked zapnem Kdyby\Doctrine\DI\OrmExtension, tak mi ladenka vypise: smeruje to sem

Class Nette\Config\CompilerExtension has been renamed to Nette\DI\CompilerExtension.

lenze ja tuto extension nemam zapnute, neviem ako to suvisi, ale zacne to robit len ked zapnem Kdyby\Doctrine\DI\OrmExtension

Nasiel som aj dalsie problem, podobne ako nette.ajax.js mam v app aj kcfinder a tam je nieco taketo:

class browser extends uploader {...

a ladenka na to:

Class 'uploader' not found

preco Kdyby\Doctrine riesi tieto veci?

Editoval duskohu (28. 11. 2013 13:16)

Filip Procházka
Moderator | 4668
+
0
-

Kdyby\Doctrine rozhodně nekontroluje žádné tvoje třídy, nijak to spolu absolutně nesouvisí, tohle kontroluje sám DI Container v Nette.

Ohledně té výjimky s přejenováním – vsadil bych se, že máš špatnou verzi kdyby/doctrine nainstalovanou, zkus si pořádně přečíst jak se to instaluje a udělat po správně.

enumag
Člen | 2118
+
0
-

@Filip Procházka: Viděl jsem jeho config a laděnku. Má správnou verzi Kdyby/Doctrine (všechny kdyby věci má @dev) a dělá to nějaká doctriní cache nebo co. Nevim co s tim.

duskohu
Člen | 778
+
0
-

@Filip Procházka instaloval som to takto, a ladenka hadze toto

"kdyby/doctrine": "@dev",
"kdyby/annotations": "@dev",
"kdyby/doctrine-cache": "@dev",
"kdyby/events": "@dev",
"kdyby/console": "@dev"
extensions:
	console: Kdyby\Console\DI\ConsoleExtension
	events: Kdyby\Events\DI\EventsExtension
	annotations: Kdyby\Annotations\DI\AnnotationsExtension
	doctrine: Kdyby\Doctrine\DI\OrmExtension
Jiří Nápravník
Člen | 710
+
0
-

No pokud to píše: Class Nette\Config\CompilerExtension has been renamed to Nette\DI\CompilerExtension. Tak prostě jednoduše v tom nette.ajax.js to přejmenuj na Nette\DI\CompilerExtension ne? Podle mě by tohle správně mělo házet chybu i bez kdyby/doctrine. Protože v 2.1 je to přejmenované.

duskohu
Člen | 778
+
0
-

@Jiří Nápravník tu extension nemam ani v extension zapnutu, a ked vymazem ten subor, tak ako som hore spomenul, naslo si dalsi subor ktory robil problem a dalej som uz neskusal ani mazat.

Quinix
Člen | 108
+
0
-

Bych tipnul že máš moc obecně nastavenou entity dir (možná na celé app?) a AnnotationReader se snaží oskenovat všechny soubory…

duskohu
Člen | 778
+
0
-

@Quinix ano mam to tak, takze to mam specifikovat na konkretne adresare?

Filip Procházka
Moderator | 4668
+
0
-

@duskohu, @enumag Vojtovo rozšíření jednoduše nepočítá s tím přejmenováním, kdybys nahradil

use Nette\Config\CompilerExtension;

za

use Nette\DI\CompilerExtension;

v souboru nette.ajax.js/history/Extension.php tak to pojede.

Nebo taky jednoduše zaregistruješ Vojtovo extension až po Kdyby rozšířeních a mělo by se to též rozjet, protože v Kdyby se vytváří aliasy na tu třídu, tedy jako vedlejší efekt opraví kompatibilitu v nette.ajax.js (teoreticky).


@Quinix ano mam to tak, takze to mam specifikovat na konkretne adresare?

To je velmi dobrý nápad ;)


Ještě je tu varianta, nekopírovat tyhle věci do app/, ale udělat si zvláštní složku libs/ do které tyhle knihovny, které nejsou přes composer, budeš kopírovat. Samozřejmě je třeba počítat s tím, že pro composer se používá vendor/ složka.

Vojtěch Dobeš
Gold Partner | 1316
+
0
-

@duskohu, @enumag, @Filip Procházka: napraveno

akadlec
Člen | 1326
+
0
-

Je to normální chování že když provedu změnu entity v entitě které jsou ve vazbě one-to-one dojde k persistování pouze parent entity? U té kolekce jak je zmíně výše bych to chápal, ale u jednoduché vazby se mě to zdá nežádoucí. Vytáhnu si entitu které měním parametry a také měním parametry druhé entitě ve vazbě tak by se mě líbilo aby došlo flushnutí změn v celém stromu.

enumag
Člen | 2118
+
0
-

Je to normální, ale dá se to změnit pomocí cascade persist.