Nextras\Orm – dalsi orm, fork YetOrmu
- hrach
- Člen | 1844
Toto je jednodenni pokus, pracuje na novem orm, ale zatim neni hotove. Kdy bude nevim. Toto opravdu nepouzivejte.
Ma cenu vubec obhajovat proc dalsi ORM? YetORM se mi moc libi, ale v nekterych aspektech se mi vubec nesedlo. Proto Nextras\Orm.
https://github.com/nextras/orm
Nejake hlavni myslenky a vlastnosti:
- kompatibilní s Nette 2.1-dev
- Entity mohou byt vytvoreny bez db
- Potomek Entity nema pristup k ActiveRow
- Entita se persistuje, zadne update ci create
- mame dao a facade
- getery a setery jsou protected/private
- vsechny property jsou cachovane (funguje spravne i cache property zavisle na jine property!)
Nejlepe napovi asi testy v nette testeru. Na toto chovani jsem pysny :)
EDIT:
- zacal jsem dneska odpo, takze …
- netestovane praxi …
- WIP …
- zadne skarede komentare! :)
- jo, ndab konci
Editoval hrach (1. 2. 2014 18:18)
- castamir
- Člen | 629
@hrach asi budu zlý (sry :D), ale musím se zeptat na mou již tradiční otázku: jak bys realizoval speciální selecty (auto join do jiné tabulky) nebo další automatické inserty do jiných tabulek při uložení nové entity (např. při closure table)?
Jinak to vypadá samozřejmě pěkně. Je to zas velký progress YetORM, takže gj ;)
Editoval castamir (27. 4. 2013 22:18)
- castamir
- Člen | 629
@hrach http://blog.profitak.cz/?p=5
přidání podobných joinů do selectu
SELECT c.*, cc2.ancestor, cc.descendant, cc.depth
FROM category c
JOIN category_closure cc ON (c.category_id = cc.descendant)
LEFT JOIN category_closure cc2 ON (cc2.descendant = cc.descendant AND cc2.depth = 1)
WHERE cc.ancestor = 8 AND cc.depth > 0
aneb něco, co pokud si dobře pamatuju tak NDB ještě neumí (aliasovat). Lze to obejít přes query, ale tím nedostaneš ActiveRow…
Editoval castamir (27. 4. 2013 22:27)
- Šaman
- Člen | 2668
Pěkné, určitě vyzkouším. Ale zůstává jeden problém, který mi taky
trochu vadil už u YetORMu a asi je spjatý s NDb. Mohu vytvořit kolekci
jinak, než jako Ndb Selection?
Konkrétně v tomto případě bych vůbec nechtěl vytvářet entitu Tag a
pracovat s tagy jako s objekty. Tag je vlastnost entity Book a rád bych
napsal getTags() tak, aby mi vracela pole stringů. (Což myslím nejde napsat
ze směru od $book->cosi, ale pomocí query). Ale s výsledkem bych rád
pracoval stejně jako s klasickou kolekcí (typicky na ni budu aplikovat order
by). Dá se tohle nějak pořešit?
//Edit: Koukám, že mezitím popsal podobný problém @castamir.
Prostě tím, že to funguje nad Ndb, tak musím entity a kolekce tvořit
pomocí ActiveRow a Selection. Asi jsem si jen zatím nezvykl na tuto filosofii
a trochu se bojím toho, že co nebudu umět zapsat, tam mi nepomůže ani
čistá SQL query, kde by to napsat šlo..
Výhoda je naprostá jednoduchost a transparentnost těchto ORMů, protože
ActiveRow za nás vyřeší většinu práce s generováním dotazů.
Editoval Šaman (27. 4. 2013 22:36)
- enumag
- Člen | 2118
vsechny property jsou cachovane (funguje spravne i cache property zavysle na jine property!)
Promiň, ale tahle hrubka fakt bije do ksichtu. ;-)
Jinak moc hezké, přesně řešíš ty problémy, kvůli kterým jsem s YetORM váhal (hlavně persist) a chtěl jsem ho forknout.
@castamir tu už psal, že ORM tu poslední dobou rostou jako houby po dešti. Ještěže jsem doteď neměl čas napsat si vlastní! Teď mám mnoho možností z čeho vybírat. :-D
- Šaman
- Člen | 2668
Jestli se ti to podaří, tak snad konečně začnu s klidným svědomím
používat ORM framework :)
V Doctrině2 jsem válčil s jejich DQL, v Petrovo ORMu s neexistující
dokumentací, Fabikovo DAO bylo docela fajn, ale narazil jsem na pár
problémů, které jsem vyřešil migrací k YetORM. S tím jsem zatím
nejvíc spokojený, takže jestli odstraníš i jeho slabiny, tak bude důvod
k oslavě.
- Tharos
- Člen | 1030
@hrach: Mám první dotaz na tohle „naprosto brilantní ORMko“ :). Jak bys v tomhle návrhu řešil například přidání/odebrání tagu knihy? Myslím včetně persistence.
Neber to prosím jako nějakou provokaci, ale jsem upřímně zvědav, jak na tenhle návrh budeš nahlížet poté, co s ním strávíš více, než jedno odpoledne. :–P Já jsem pracoval s původním YetORMem ve dvou reálných projektech a narazil jsem hned na několik problémů, které mi přišly tak nějak principiální… No, jsem upřímně zvědav, jak dopadne tenhle pokus. :)
Editoval Tharos (28. 4. 2013 0:19)
- hrach
- Člen | 1844
@tharos a i ostatni: zde je nejaky road plan:
- validace pred persistenci
- zbaveni se zavislosti na nette\database\table, pridani vrstvy pro mapovani, proste nejakeho mapperu, ale bude to velice easy :)
- parser pro use statement (idealne nekde vygrabovat)
- pridani podpory pro napovidani entity pro IDE
/**
* @property Book[]|EntityCollection $books
*/
class Author extends Entity {}
- automaticka sprava vazev, 1:N, M:N. pribudou dalsi tridy *collection, ktere budou automaticky implementovat spravu techto vazeb. nebude tak treba tento boilerplate. Api by mohlo vypadat treba takto
/**
* @property HasOneCollection $author (author_id, book)
*/
class Book extends Entity {}
/**
* @property HasManyCollection $books (book, author_id)
* @property HasManyCollection $translatedBooks (book, translator_id)
*/
class Author extends Entity {}
Kolekce samozrejme budou mit patricne api a metody pro jejich persistovani. Ano, je mi jasne, ze narazim na plno problemu, ale tak nejak si to vysnivam :)
Editoval hrach (28. 4. 2013 0:33)
- castamir
- Člen | 629
používám pár další rozšíření properties, takže pro inspiraci
/**
* @property enum $pole {allowed x,z,y}
* @property set $mnozina {allowed x,z,y}
* @property string|NULL $prazdny
* @property string $defaultni {default ahoj}
* @property string $dlouhyNazev {column dlouhy_nazev}
*/
Datový typ budu mít i konkrétní instance Entit nebo obecně datové typy PHP nebo databáze (poradím si s oběma)
/**
* @property string $title
* @property Author $author
*/
class Book {}
defaultní vazba je samozřejmě 1:1, ale 1:M a N:M chci také doplnit, ale sám ještě nemám vymyšlené jak ;)
Editoval castamir (28. 4. 2013 0:43)
- Tharos
- Člen | 1030
hrach napsal(a):
- parser pro use statement (idealne nekde vygrabovat)
Kdyby Tě zajímal ten z „mého ORMka“, tak tady ho máš:
<?php
namespace Tharos\Reflection;
/**
* @author Vojtěch Kohout
*/
class Aliases
{
/** @var array */
private $aliases = array();
/** @var string */
private $current = '';
/** @var string */
private $lastPart = '';
public function resetCurrent()
{
$this->current = $this->lastPart = '';
}
/**
* @param string $name
*/
public function appendToCurrent($name)
{
if ($this->current !== '') {
$this->current .= '\\';
}
$this->current .= $this->lastPart = $name;
}
/**
* @param string $name
*/
public function setLast($name)
{
$this->lastPart = $name;
}
public function finishCurrent()
{
$this->aliases[$this->lastPart] = $this->current;
$this->resetCurrent();
}
/**
* @return array
*/
public function getAll()
{
return $this->aliases;
}
}
<?php
namespace Tharos\Reflection;
use Tharos\Exception\UtilityClassException;
/**
* @author Vojtěch Kohout
*/
class AliasesParser
{
const STATE_WAITING_FOR_USE = 1;
const STATE_GATHERING = 2;
const STATE_IN_AS_PART = 3;
const STATE_JUST_FINISHED = 4;
/**
* @throws UtilityClassException
*/
public function __construct()
{
throw new UtilityClassException('Cannot instantiate utility class ' . get_called_class() . '.');
}
/**
* @param string $source
* @return array
*/
public static function parseSource($source)
{
$aliases = new Aliases;
$states = array(
self::STATE_WAITING_FOR_USE => function ($token) use ($aliases) {
if (is_array($token) and $token[0] === T_USE) {
$aliases->resetCurrent();
return AliasesParser::STATE_GATHERING;
}
return AliasesParser::STATE_WAITING_FOR_USE;
},
self::STATE_GATHERING => function ($token) use ($aliases) {
if (is_array($token)) {
if ($token[0] === T_STRING) {
$aliases->appendToCurrent($token[1]);
} elseif ($token[0] === T_AS) {
return AliasesParser::STATE_IN_AS_PART;
}
} else {
if ($token === ';') {
$aliases->finishCurrent();
return AliasesParser::STATE_WAITING_FOR_USE;
} elseif ($token === ',') {
$aliases->finishCurrent();
}
}
return AliasesParser::STATE_GATHERING;
},
self::STATE_IN_AS_PART => function ($token) use ($aliases) {
if (is_array($token)) {
if ($token[0] === T_STRING) {
$aliases->setLast($token[1]);
$aliases->finishCurrent();
return AliasesParser::STATE_JUST_FINISHED;
}
}
return AliasesParser::STATE_IN_AS_PART;
},
self::STATE_JUST_FINISHED => function ($token) use ($aliases) {
if ($token === ';') {
return AliasesParser::STATE_WAITING_FOR_USE;
}
return AliasesParser::STATE_GATHERING;
}
);
$state = $states[self::STATE_WAITING_FOR_USE];
foreach (token_get_all($source) as $token) {
$state = $states[$state($token)];
if (is_array($token)) {
$token[3] = token_name($token[0]);
}
}
return $aliases->getAll();
}
}
Je to takovej callback-based stavovej automat. :) Měl by pokrývat vše
myslitelné (včetně as
, aliasů, více hodnot oddělených
čárkou)…
- automaticka sprava vazev, 1:N, M:N
Já teď u sebe zapisuji vazby v kostce takhle:
/**
* @property Tag[] $tags m:hasMany
* @property Author|null $author m:hasOne
* @property Author|null $maintainer m:hasOne(maintainer_id)
* @property Something[] $somethings m:belongsToMany(book_id:something)
*/
Naimplementoval jsem si i trochu neobvyklé vazby belongsToMany
a belongsToOne
, přičemž v závorce se u všech vazeb dají
vždy ještě upřesnit vazební sloupce a tabulky. No, zatím to obsáhlo vše,
co jsem potřeboval. Super je, že u většiny entit se pak dá vše zapsat jen
pomocí anotací.
K tomuhle už bohužel kód takhle snadno nepošlu, protože logika celého toho mapování je spletitější…
Určitě parsuj (jakýmkoliv způsobem) use statement, protože pak se ty anotace úžasně pročistí.
No, přeji příjemnou zábavu. :)
Editoval Tharos (28. 4. 2013 10:08)
- Tharos
- Člen | 1030
To by určitě bylo fajn, samotného by mě zajímal výsledek. :) Přiznám se, že jsem moc velkou rešerši po okolí nedělal.
Pull tedy ještě připravovat nebudu. Ono by byl stejně jednoduchý –
přidat tyhle dvě třídy je to nejmenší, ale jejich použití už by stejně
bylo na Tobě. Ten můj parser prostě vrátí pole ve tvaru například
['Tag' => 'Model\Entity\Tag', 'Http' => 'Nette\Http']
atp.
Pak je stejně ještě zapotřebí u každé anotace reálně rozhodnout,
jakého je typu (zda typ začíná na \, zda je dostupný relevantní alias,
jaký je namespace celého souboru…). No a to už si Ty umístíš určitě
nejlíp.
Ad motivace) Přispěl bych rád, ale problémem je, že jsem před pár dny právě dopsal první verzi podobného ORM nad Dibi a zatím je to přesně to, co jsem si vždycky přál (troufám si tvrdit, že aktuálně je i vyspělejší – už umí třeba i to mapování vazeb skrze anotace)… Plus si trochu nejsem jist ve věci zde nastíněné persistence – to jsem právě zvědav, jak to dopadne ;). Viz třeba právě správa a persistence M:N vazeb.
V každém případě držím palce, protože i když tohle ORM asi přímo nevyužiji, jeho sledování bude pro mě minimálně inspirativní.
Editoval Tharos (21. 6. 2013 14:17)
- Filip Procházka
- Moderator | 4668
@Tharos ten pull prosím udělej :) Už dlouho si na to brousím zuby (klasicky není čas …) a když to máš už hotové, tak jedině dobře!
- Jan Tvrdík
- Nette guru | 2595
hrach wrote:
- automaticka sprava vazev, 1:N, M:N. pribudou dalsi tridy *collection, ktere budou automaticky implementovat spravu techto vazeb. nebude tak treba tento boilerplate. Api by mohlo vypadat treba takto
/** * @property HasOneCollection $author (author_id, book) */ class Book extends Entity {} /** * @property HasManyCollection $books (book, author_id) * @property HasManyCollection $translatedBooks (book, translator_id) */ class Author extends Entity {}
Víc se mi líbí zápis, který používá Petrovo ORM, tj.
use Nextras\Orm;
/**
* @property Author $author {m:1 author_id, book}
*/
class Book extends Orm\Entity {}
/**
* @property Orm\OneToMany $books {1:m book, author_id}
* @property Orm\OneToMany $translatedBooks {1:m book, translator_id}
*/
class Author extends Orm\Entity {}
Tím, že meta informace jsou v vždycky v {}
tak je snadno
odlišíš od normálního komentáře. Navíc můžeš mít i víc typů
meta dat.
/**
* @property Author $author {m:1 author_id, book} autor knihy
* @property string $status {enum self::STATUS_PUBLISHED, self::STATUS_PLANNED} {default self::STATUS_PLANNED}
*/
class Book extends Orm\Entity
{
const STATUS_PUBLISHED = 'published';
const STATUS_PLANNED = 'planned';
}
- thunderbuff
- Člen | 164
Jakým způsobem lze ORM „nejlépe“ injektovat?
services:
selectionFactory: Nextras\Orm\SelectionFactory
bookDao: BookDao
…ale kam s tímhle? Nějaké
<?php
$selectionFactory->addMap('Author', 'author');
$selectionFactory->addMap('Book', 'book');
$selectionFactory->addMap('Tag', 'tag');
?>
Editoval thunderbuff (29. 4. 2013 0:25)
- thunderbuff
- Člen | 164
Ha! proto je lepší nepouštět se do ničeho po půlnoci!
@enumag: Díky za radu, mělo mě to napadnout :-)
- thunderbuff
- Člen | 164
Jen tak to zkoušim ;-) BTW: DAO je ošklivý výraz, repository (i když možná méně vystihuje podstatu věci) zní lépe :-P
Editoval thunderbuff (29. 4. 2013 0:54)
- thunderbuff
- Člen | 164
Pokud chci pouze spočítat prvky v kolekci a nepotřebuji s nimi pracovat, vytáhnou se i tak data z celé tabulky a spočítá se pole. Možná by stálo za implementaci počítání přes count query.
- hrach
- Člen | 1844
Prosim prosim, ber to jako jenom ukazku. Cele to chci naprosto kompletne
predelat.
O tomto vim, schvalne jsem to odstranil (YetORM) to ma, protoze si myslim, ze
by na takovouto akci mela byt metoda zvlast. Bohuzel je to hlavne kvuli hloupemu
php chovani, kdy countable vyzaduje metodu count.
Ten nazev metody je spatny, protoze je pak $collection->count() nutne count
nad vyfetchovanymi daty.
- Jack06
- Člen | 168
Btw, nepřemýšlel jsi, nebo nechtěl bys popřemýšlet, že bys to
anotacemi spojil s doctrine.
Viděl bych v tom ne jednu výhodu.
- generování databáze z entit – podle mě super věc, nemusíš se zaobírat tím samým na úrovni entit a ještě na úrovni sql
- kompatibilita a porozumění u lidí, kteří používali a používají doctrine
- snadnější přechod z doctrine na NDB či naopak
- jansfabik
- Člen | 193
Napadlo mě, že by každý objekt mohl mít i vlastní třídu pro kolekci. Potom by se dalo místo
return new EntityCollection($this->related('book_tag'), 'App\Model\Tag', 'tag');
psát jenom
return new TagCollection($this->related('book_tag'));
přičemž tabulka i datový typ entity by se deklaroval ve třídě
TagCollection
. Zároveň by se tím otevřela možnost rozšiřovat
kolekce o další metody specifické pro daný objekt. Kolekce by také mohly
pomocí anotace říct IDE, jakého typu jsou entity, takže by pak i krásně
fungovalo našeptávání :)
Edit: Na druhou stranu, tohle by už znamenalo, že pro každý objekt jsou třeba tři třídy (Entity, Collection, Dao). Hodilo by se na to vytvořit i nějaký generátor. :)
Editoval jansfabik (3. 5. 2013 17:16)
- jansfabik
- Člen | 193
Tak třeba jak máš v BookDao
metodu findByTags()
, tak ta by se dala přesunout do BookCollection
pod názvem whereTags($name)
:
public function whereTags($name)
{
$this->selection->where('book_tag:tag.name', (array) $name);
return $this;
}
Kdybych měl těch filtrovacích metod v Dao víc
(findByTags()
, findByWhatever()
), tak bych je nemohl
řetězit. U kolekcí mohu
findAll()->whereTags(...)->whereWhatever(...)
.
Edit: A dalším velmi užitečným přínosem je našeptávání v IDE.
Edit 2: A taky mnohem kratší kód oproti
return new EntityCollection($this->related('book_tag'), 'App\Model\Tag', 'tag');
Editoval jansfabik (3. 5. 2013 17:34)
- jansfabik
- Člen | 193
A pak mě ještě napadlo, že by entita mohla mapper mohl
automaticky převádět camelCase na underscore_case (dalo by se to samozřejmě
i vypnout pro zpětnou kompatibilitu). Je to divné mít někde $person->fullName
a jinde zase $book->author_id,
bylo by lepší to sjednotit.
Editoval jansfabik (3. 5. 2013 23:13)
- nanuqcz
- Člen | 822
Je to schválně, že nemůžu „nextras/orm“ najít na Packagistovi?
http://easycaptures.com/…61512140.jpg
Díky
- elektricman
- Člen | 29
Ahoj,
zkouším tvoje Nextras/ORM (btw parádní práce, konečně něco co má
„interfejs“ jak si ho u orm představuju :))
a narazil sem na problém :(..
jak vytáhnout data z kolekce podle podmínky např. (pole ve findBy sem si právě vycucal z prstu, protože práve nevim jak to udělat):
$this->orm->peoples->findBy(['TIMESTAMPDIFF(YEAR, birthdate, NOW) > ?' => 18]); // věk větší než 18
nebo
$this->orm->peoples->findBy(['this->group->name LIKE %?% OR this->team->name LIKE %?%'=>['whatever','whatever'])); // skupina nebo pozice člověka obsahuje text whatever
Prostě nějaký filtry. Podle zdrojáku to vypadá že to z podmínek prostě umí parsovat jen to „this->“, ale ne normální SQL syntaxi.
Chápu že tohle by se asi nemělo řešit na urovni presentru, ale ani v repository nemám přístup přímo k SQL query, a do mapperu se to samozžejmě nehodí .. .
Editoval elektricman (5. 7. 2014 9:24)
- hrach
- Člen | 1844
vytahnout data z kolekce
to neni uplne tak dofiltrovani kolekce, jako spis proste najit v db nejaky entity, pokud mas takovyto slozitejsi dotaz, nejlepsim resenim je to presunout na vrstvu mapperu – je to v poradku. tj.
PeopleRepository
/**
* @method Nextras\Orm\Entity\ICollection|Person[] findByAge(int $age)
*/
class PeopleRepository extends Nextras\Orm\Repository\Repository
{}
PoepleMapper
class PeopleMapper exntends Nextras\Orm\Mapper\Mapper
{
public function findByAge($age)
{
return $this->databaseContext->query('SELECT * FROM persons WHERE ...');
}
}
je to na Mapper vrstve resene proto, ze pouzivas pro filtrovani vyrazy, ktere samozrejme maji neco spolecneho s sql, tj. repository vrstva o nich nema ani paru.
btw, shoudl work inflection Person → People, https://github.com/…/Inflect.php#L79
- hrach
- Člen | 1844
Toto vlakno bylo vytvoreni pro jednoduseni pokus Nextras\Orm, po roce jsem ale vydal uplne jine orm se stejnym nazvem, jeho diskuzni vlakno je zde – https://forum.nette.org/…nextras-dbal diky :)