YetORM – kvaziORM nad Nette\Database

Pavel Kravčík
Člen | 1195
+
0
-

Možné to není, protože mě nepustí YetORM výjimka. Napadá mě to v téhle situaci třeba.
Ale rozumím tomu principu, asi je to tak čistější do budoucna.

Šlo mi to – dát si do té getMetody json_decode, přišlo mi, že to patří do Entity.

Díky za nullable.

Pavel Kravčík
Člen | 1195
+
0
-

@uestla: Nepřemýšlel si o rozšíření EntityCollection o další možnosti filtrování podle NTDB? Na mysli mám where() podmínky na $selection.

uestla
Backer | 799
+
0
-

Přemýšlel :-)

Pavel Kravčík
Člen | 1195
+
0
-

A jak to dopadlo? Třeba pak, když člověk se chce pokusit napsat DataSource pro Grido, tak musí převést EntityCollection do Array a filtrovat to jako pole. Což myslím není moc hezké.

Jen mi zajímalo, jestli není třeba někde beta – přepisovat třídy ve Vendoru mi rozumné nepřišlo. :))

uestla
Backer | 799
+
0
-

Dopadlo by to pak, že by se logika příslušící do YetORM\Repository přesouvala bůhvíkam a s internostmi Nette\Database by se operovalo na nevhodných místech. Proto jsem to tam nepřidával.

Pavel Kravčík
Člen | 1195
+
0
-

Jasně, díky za info. Tomu rozumím.

Zkusím něco jiného.

ryder
Člen | 17
+
0
-

Zdravím,

s YetORM, resp. s Nette začínám a YetORM mě oslovila jako abstrakce, kterou jsem hledal :)

Řeším problém s transakcemi, které nevím jak správně s YetORM používat:

Pokud chci uložit entitu, která se skládá z dalších entit, tak to potřebuji obalit celé do transakce, abych měl v DB uloženy buď kompletní data nebo nic.

Jak tedy realizovat transakce, pokud ukládám několik různých entit do různých repository?

uestla
Backer | 799
+
0
-

To je dobrý dotaz.

Původně jsem chtěl navrhnout mít service, který by měl instance repozitářů všech svázaných entit. Pak mi došlo, že transakce se na repozitáři zvenčí volat nedají. Nejpřímočařejší (ale nehezké) by bylo mít vazbu realizovanou přímo uvnitř „hlavního“ repozitáře (repozitáře s hlavní entitou), který by měl ostatní repozitáře v sobě.

Čistší by asi bylo mít oddělenou vrstvu pro transakce někde bokem a ne takhle natvrdo svázanou s repozitářem. Zkusím to nějak rozumně napsat a přidat, kdyžtak dám vědět. Díky za námět!

ryder
Člen | 17
+
0
-

Děkuji za reakci :)

Zatím to řeším přidáním metody insert do Repository, která provede insert nad NDB, a transakce si řeším ve vrstvě nad Repository také nad NDB, ale není to čisté řešení a moc se mi nelíbí.

Stav kdy by se řešili transakce bokem, nezávisle na Repository by byl fajn :)

uestla
Backer | 799
+
+3
-

Ahoj,

tak jsem přidal třídu YetORM\Transaction coby vrstvu pro transakce. Každá repository si vytváří vlastní instanci a na ní pak volá svoje „transakční“ metody, takže se navenek nic nemění. Přidává to ale možnost si ve vlastní službě vytvořit vlastní Transaction instanci a v ní pak persistovat více entit z různých repozitářů:

use Nette\Database\Connection;


class MyService
{
	private $books;
	private $authors;
	private $transaction;

	function __construct(Connection $connection, BookRepository $books, AuthorRepository $authors)
	{
		$this->books = $books;
		$this->authors = $authors;
		$this->transaction = new YetORM\Transaction($connection);
	}

	function persistAll($books, $authors)
	{
		$this->transaction->transaction(function () use ($books, $authors) {
			foreach ($books as $book) {
				$this->books->persist($book);
			}

			foreach ($authors as $author) {
				$this->authors->persist($author);
			}
		});
	}
}
mdsystem
Backer | 5
+
0
-

Ahoj,
mám dotaz ohledně vytváření entit z relací.
Mám entitu book a v ní následující metodu

function getAuthor()
	{
		return new Author($this->record->author);
	}

Vadí mi to new. Neměl bych si spíš do té entity předat relaci která vytváří Author entitu a zavolat něco jako

	function getAuthor()
	{
		return $this->authorRelation->createEntity($this->record->author);
	}

Je mi jasné že vazbu na author relation bych musel dát i do book relatin a ta by ji předávala svým entitám, ale přijde mi, že jsou tak dodrženy principy DI.

Nebo mi něco uniklo?

Editoval mdsystem (11. 5. 2015 15:08)

uestla
Backer | 799
+
0
-

Většinou si při složitějších vazbách udržuji službu EntityCreator, která má metody createAuthor($row) apod. Tu si pak konstruktorem předávám do jednotlivých entit, aby tak vytváření instancí probíhalo prostřednictvím téhle služby.

mdsystem
Backer | 5
+
0
-

uestla napsal(a):

Většinou si při složitějších vazbách udržuji službu EntityCreator, která má metody createAuthor($row) apod. Tu si pak konstruktorem předávám do jednotlivých entit, aby tak vytváření instancí probíhalo prostřednictvím téhle služby.

Díky za odpověď.
Samozřejmě jsem to napsal před tím blbě. Tím authorRelation jsem myslel authorRepository. Nicméně jestli jsem tě správně pochopil, není špatně pokud entita zná repository (což je také služba), kterou nechá jinou entitu vytvářet. To jestli to zabalím ještě do nějaké obecné služby (tvá EntityCreator), už podle mě není podstatné. Prostě není problém, aby entita volala createEntity metodu nějaké repository.
Šlo mi o to jestli tím nějak nejdu proti myšlence ORM.

Editoval mdsystem (12. 5. 2015 1:39)

uestla
Backer | 799
+
0
-

Předávat repozitory je neprozíravé v tom, že pokud jsou entity na sobě závislé vzájemně, dostaneš se do cyklické závislosti uvnitř SystemContaineru. Proto si vytvářím nezávislou EntityCreator, kterou si předávám jak do entity, tak do repozitářů. V repozitářích si pak akorát upravím createEntity() tak, aby volala metodu právě oné služby.

mdsystem
Backer | 5
+
0
-

uestla napsal(a):

Předávat repozitory je neprozíravé v tom, že pokud jsou entity na sobě závislé vzájemně, dostaneš se do cyklické závislosti uvnitř SystemContaineru. Proto si vytvářím nezávislou EntityCreator, kterou si předávám jak do entity, tak do repozitářů. V repozitářích si pak akorát upravím createEntity() tak, aby volala metodu právě oné služby.

Jo, tak už jsem na to také přišel. Když se dva repository potřebují v konstruktoru navzájem, není to úplně ono 8o). Jen mi to nejdřív přišlo ne úplně pěkné mít službu, která zná v podstatě všechny repository.

mdsystem
Backer | 5
+
0
-

Ještě jsem přišel na problém s kolekcemi. Pokud přetížím konstruktor entity abych mohl do entity předávat nějaké další parametry (např. $imageDir co máš použit v testu), tak se mi to shoří při vytváření kolekce, na tom, že nezná druhý parametr konstruktoru. Samozřejmě mohu jeho výchozí hodnotu nastavit na NULL, ale to pak v Entitách získaných z kolekce ten $imageDir nebudu mít.

Jediné co mě napadlo, ale nejsem si jistý správností a přijde mi to neohrabané, je při vytvářaní EntityCollection místo názvu třídy entity předat callback na objekt, který mi bude zajišťovat volání createEntity z její repository, což by mohla být zase tebou navrhovaná služba EntityCreator. Pak ale, vytvářím službu, která zná všechny repository a všechny Entity na ní závísí a to mi nepřijde úplně kosher.

uestla
Backer | 799
+
0
-

Služba EntityCreator nezná všechny repozitáře, jen všechny závislosti všech entit. Ale to je jen proto, že jsem si to tak zvolil. Můžou existovat tenčí továrničky pro každou entitu zvlášť.

Ohledně EntityCollection – to, co popisuješ, funguje, tj. místo názvu entitní třídy jde předávat callback, tj. např. Nette\Utils\Callback::closure($entityCreator, 'createAuthor').

mdsystem
Backer | 5
+
0
-

uestla napsal(a):

Služba EntityCreator nezná všechny repozitáře, jen všechny závislosti všech entit. Ale to je jen proto, že jsem si to tak zvolil. Můžou existovat tenčí továrničky pro každou entitu zvlášť.

Ohledně EntityCollection – to, co popisuješ, funguje, tj. místo názvu entitní třídy jde předávat callback, tj. např. Nette\Utils\Callback::closure($entityCreator, 'createAuthor').

Už v tom mám jasno, jen jsem to původně chtěl narvat do repository, tak aby se každý repository staral o své entity a nedošlo mi, že mi mohou vzniknout cyklické závislosti.
Moc dík za reakce, dost mi to zjednodušil život.

Gappa
Nette Blogger | 208
+
0
-

uestla napsal(a):

Většinou si při složitějších vazbách udržuji službu EntityCreator, která má metody createAuthor($row) apod. Tu si pak konstruktorem předávám do jednotlivých entit, aby tak vytváření instancí probíhalo prostřednictvím téhle služby.

Ahoj,

šlo by prosím postnout ukázku takové služby? Potřeboval bych trošku „nakopnout“ :)

Díky

uestla
Backer | 799
+
+1
-

Jasně, např.

class EntityCreator extends Nette\Object
{
	function createCategory($row = NULL)
	{
		return new Category($this->createArticle, $row);
	}

	function createArticle($row = NULL)
	{
		return new Article($this->createCategory, $row);
	}
}

Obě entity totiž na sobě závisí navzájem:

class Category extends YetORM\Entity
{
	private $articleFactory;

	function __construct(\Closure $factory, $row = NULL)
	{
		parent::__construct($row);
		$this->articleFactory = $factory;
	}

	function getPublishedArticles()
	{
		$selection = $this->record->related('article', 'category_id')
				->where('published', TRUE)
				->where('written <= NOW()');

		return new YetORM\EntityCollection($selection, $this->articleFactory);
	}
}

A v jednotlivých repozitářích pak

use Nette\Database\Context as NdbContext;

class CategoryRepository extends YetORM\Repository
{
	private $creator;

	function __construct(EntityCreator $creator, NdbContext $database)
	{
		parent::__construct($database);
		$this->creator = $creator;
	}

	function createEntity($row = NULL)
	{
		return $this->creator->createCategory($row);
	}
}

Obdobně u entity Article a její ArticleRepository

uestla
Backer | 799
+
+3
-

Verze 9.0.0

Před malou chvílí jsem vydal verzi 9. Hlavní změnou je přidání CI a code coverage.

Došlo také k pár změnám a menším BC breakům (proto navýšení major verze).

Velké díky za pomoc patří @Felix ! :-)

Felix
Nette Core | 1199
+
0
-

@uestla Neni zac, rad jsem pomohl. :)

Pavel Kravčík
Člen | 1195
+
0
-

Ještě drobnost k nové verzi. Proč checkEntity zůstala private? Mohla by být protected? :) To je prakticky jediný věc, kvůli které „forkuju“.

Use-case, dědím si Repository do BaseRepository a při každém volání persist() ukládám odlitek změn někam do „historie“.

uestla
Backer | 799
+
+1
-

@PavelKravčík Asi by mohla být final protected, podobně jako getTableName() a getEntityClass()

Pavel Kravčík
Člen | 1195
+
0
-

@uestla: Nějakým zázrakem jsem pochopil, jak udělat PR na Githubu.

uestla
Backer | 799
+
+1
-

@PavelKravčík Mergnuto, díky :-)

CZechBoY
Člen | 3608
+
0
-

Jak vkládat do M:N tabulky?
ZKoušel jsem to klasicky přes vlastní repozitář + entitu, ale hází mi to chybu při volání setRow s integerem.
https://ctrlv.cz/AP97

Ještě přidám jak to volám

$MNEntity = $this->MNRepository->createEntity();
$MNEntity->id1 = $id1;
$MNEntity->id2 = $id2;
$this->MNRepository->persist($MNEntity);

Editoval CZechBoY (8. 4. 2016 20:04)

uestla
Backer | 799
+
+1
-

Myslím, že k tomu dochází, když není definovaný primární klíč na vazební tabulce. Pokud nepomůže jej definovat jako dvojici entita_1_id:entita_2_id, tak by mělo pomoct vytvořit zvláštní primární klíč…

webdata
Člen | 153
+
0
-

Ahoj, bude oficiálně nahrazení nette/object za trail?

uestla
Backer | 799
+
0
-

Ahoj, přemýšlel jsem nad tím. Problém je, že např. v Repository přetěžuji __call() a v závěru se odvolávám na rodiče. U traity by to znamenalo si vytvořit aliasní metodu a tu pak volat namísto parent::. To se mi ale moc nelíbí, tak to zatím nechávám otevřené…

uestla
Backer | 799
+
+1
-

CZechBoY napsal(a):

Jak vkládat do M:N tabulky?
ZKoušel jsem to klasicky přes vlastní repozitář + entitu, ale hází mi to chybu při volání setRow s integerem.

Přidal jsem do Repository::persist() kontrolu a snad i lepší chybovou hlášku :-)

idiox
Člen | 14
+
0
-

Ahoj :)

Jak byste prosím řešili nastavení defaultního jazyka pro repository nebo entitu?

Mám entitu post(id, parent, author) a entitu jazykových mutací post_lang(lang, title, content) a když si načtu

<?php
$posts = $this->postRepo->findParents();
?>

tak pak musím v každým cyklu psát

<?php
$post->getTitle( $this->lang );
// getTitle vrací PostLang( $this->record->related('post_lang', 'post_id')->where(['lang_id.code' => $this->language])->fetch()->title
?>

a rád bych tam měl prostě jen

<?php
$post->getTitle();
?>

Díky za nápady :)

uestla
Backer | 799
+
0
-

Ahoj,

defaultní jazyk bych držel v repozitáři a upravil v něm metodu createEntity(), kde bych do entity vložil defaultní jazyk.

Pak bych přidal metodu Post::getTranslation($lang = NULL), která by vracela spřaženou instanci PostLang podle jazyka – pokud by byl NULL, vrátila by defaultní překlad. Instance bych navíc cachoval, abych ušeřil dotazy.

Všechny následné metody Post::getTitle() apod. by měly volitelný parametr $lang, který by vracel hodnoty z instance překladu.

Tedy vše výše uvedené v kódu (psáno na koleně):

class PostRepository extends YetORM\Repository
{
	private $defaultLang = 'en';

	public function createEntity($row = NULL)
	{
		$class = $this->getEntityClass();
		return new $class($this->defaultLang, $row);
	}
}


class Post extends YetORM\Entity
{
	private $defaultLang;
	private $translations = [];

	public function __construct($defaultLang, $row = NULL)
	{
		$this->defaultLang = $defaultLang;
		$this->record = Record::create($row);
	}

	public function getTranslation($lang)
	{
		if (!isset($this->translations[$lang])) {
			$row = $this->record->related('post_lang', 'post_id')
					->where('lang_id.code', $lang)
					->fetch();

			$this->translations[$lang] = new PostLang($row);
		}

		return $this->translations[$lang];
	}

	public function getTitle($lang = NULL)
	{
		return $this->getTranslation($lang ?: $this->defaultLang)->getTitle();
	}
}

Snad ti to alespoň ukázalo možný postup :-)

Kdybys něčemu nerozuměl, zeptej se.

Editoval uestla (20. 12. 2016 10:26)

idiox
Člen | 14
+
0
-

Ahoj, díky, vypadá to skvěle :) Tohle by mě asi fakt nenapadlo :( :D Vyzkouším asap :)

Pavel Kravčík
Člen | 1195
+
0
-

Ahoj @uestla, chtěl bych přetížit AnnotationProperty – nenapadá Tě hezký způsob jak na to? Nechce se mi jít do forku – spíše kdyby to šlo rozšiřovat pomocí interface. Připravím PR klidně, jen zatím vymýšlím jak na to.

Use case – chceme sami rozhodovat, kdy se má sloupec v ResultSet měnit v DateTime. Nelíbilo se mi, že pro každý sloupec, který má nastaveno date se tvoří v Nette (new DateTime) i když to není potřeba. Napsal jsem tedy driver, který to nedělá. V entitě bych však rád zachoval obojí chování v property nastavit jak string či DateTime a vytvoření new DateTime by byla zodpovědnost entity. Bohužel to neprojde přes tu kontrolu v AnnotationProperty#96. Díky.

uestla
Backer | 799
+
0
-

@PavelKravčík Je ono použití stringu namísto DateTime z důvodu výkonu?

V danou chvíli bych navrhoval spíše změnit use case (tedy trvat vždy pouze na stringu a v případě potřeby jej ručně na DateTime převést- třeba pomocí explicitního getteru na entitě) než ohýbat interní AnnotationProperty (i třeba pomocí rozšíření na interface a vlastní implementace).

Pavel Kravčík
Člen | 1195
+
0
-

@uestla: Ano – výkonnově se nám to moc nelíbilo.

Díky to je také dobrý nápad, chtěli jsme to mít programátorsky přívětivější, ale explicitní funkce bude čistější řešení i pro testování.