Oznámení
před 5 lety
- Tharos
- Člen | 1042
@mirdič Ahoj, přesně tohle řeší (mimo jiné) nadstavba Lean Query. V ní by řešení vypadalo následovně:
$categories = $domainQueryFactory->createDomainQuery()
->select('c')
->from(Category::class, 'c')
->join('c.products', 'p')->select('p')
->where('p.price > %i', $price)
->getEntities();
Velmi důležitá je tam ta část ->select('p')
, díky
které se v rámci toho jednoho dotazu získají i produkty (které jsou
dražší než daná cena). Získaná data se nahydratují tak, že při
traverzování se už žádné dotazy znovu nepokládají.
A díky inner joinu jsou i kategorie vyfiltrované požadovaným způsobem.
Best practice je tedy použít Lean Query, byť by to mělo být na tuhle jedinou věc v celé aplikaci. V jiném případě musíš udělat to, co píšeš: provádět restrikci dvakrát.
Editoval Tharos (27. 6. 2014 0:57)
před 5 lety
- Tharos
- Člen | 1042
@mirdič Mimochodem tenhle use-case mi byl vlastně největší motivací k tomu něco jako Lean Query psát. Viz tenhle můj popis úplně ekvivalentního problému.
před 5 lety
- medhi
- Bronze Partner | 189
Tharos napsal(a):
@medhi: Super, díky za přiblížení.
Automatické filtrování je strašně pohodlné. Nemusíš u všech vazeb, které vedou na konkrétní
Student
neboTeacher
, myslet na to, že je třeba do toho traverzování nějak zasáhnout. Proto Ti chci právě takové automatické řešení nastínit :). Dají se k tomu využít tzv. implicitní filtry v mapperu:public function getImplicitFilters($entityClass, Caller $caller = null) { if ($entityClass === Student::class) { return new ImplicitFilters(function (Fluent $statement) { $statement->where('[user.type] = %s', 'student'); }); } if ($entityClass === Teacher::class) { return new ImplicitFilters(function (Fluent $statement) { $statement->where('[user.type] = %s', 'teacher'); }); } return parent::getImplicitFilters($entityClass, $caller); }
Pak se při každém traverzování na studenty nebo učitele z libovolné entity bude hlídat, aby ve výsledku byly správné entity.
Mimochodem, s těmi implicitními filtry pracují i repositáře. S výše uvedeným filtrem můžeš mít nejen
UserRepository
, kde si vytažení správných dat musíš hlídat sám, ale například iStudentRepository
aTeacherRepository
, ve kteréch se při zavoláníprotected
metodycreateFluent
ten filtr automaticky použije, takže už se o to taky nemusíš více starat.Persistovat přes ty
StudentRepository
aTeacherRepository
je také možné, stačí tam jen při vkládání do databáze vložit do low-level dat požadovanou hodnotu do sloupcetype
(v mém ukázkovém případě). Asi bych si na to přetížil metoduRepository::insertIntoDatabase
… U persistence hodně záleží na tom, jak to celé spravuješ, jaký máš backend, jaké jsou případy užití atp. Je více možností, jak takové entity persistovat.Kdyby Ti bylo cokoliv dalšího nejasného, klidně se ptej. :)
Ještě bych se rád vrátil k tomuto řešení. Funguje v klasickém případě, ale tyto implicitní filtry se asi nespouští v tomto případě:
$students = $this->getValueByPropertyWithRelationship('students');
Co s tím? Díky
EDIT: Už jsem na to asi přišel, takto:
$students = $this->getValueByPropertyWithRelationship('students', new Filtering(function (Fluent $statement) {
$statement->where("role = 'student'");
}));
Editoval medhi (27. 6. 2014 10:45)
před 5 lety
- mirdič
- Člen | 41
@Tharos děkuji za odpověď a názornou ukázku.
Pokouším se o implementaci, ale narazil jsem na problém:
Pokud mám v entitě toto
public function getDate_from()
{
$date = new PlaceInTime($this->row->date_from);
return $date->format("d.m.Y");
}
Skončí s chybou
LeanMapper\Exception\InvalidArgumentException #1 Missing 'date_from' column in row with id 2.
EDIT: tak je to pouze, když není entita definována v anotaci, pokud je i v anotaci, tak to funguje.
Editoval mirdič (27. 6. 2014 14:51)
před 5 lety
- mirdič
- Člen | 41
Tak jsem narazil na problém, který nevím jak řešit a musím se poradit :)
Mám entitu exkurze, každá exkurze má několik termínů (term) a odjezdových míst (departure).
/**
* @property int $id
* @property string $title
* @property Excursion_departure[] $departure m:belongsToMany() m:filter(order)
* @property Excursion_term[] $term m:belongsToMany()
*/
class Excursion extends Entity
{
}
Termíny
/**
* @property int $id
* @property Excursion $excursion m:hasOne()
* @property bool $open
* @property string $date_from
* @property string $date_to
*/
class Excursion_term extends Entity
{
}
Místo odjezdu
/**
* @property int $id
* @property Excursion $excursion m:hasOne
*/
class Excursion_departure extends Entity
{
}
Chtěl bych vypsat exkurze, které mají alespoň jeden nadcházející termín a alespoň jedno místo odjezdu.
Bez LeanQuery mi funguje následující řešení:
$query = $this->connection->select('excursion.*')
->from('excursion')
->join('excursion_term')
->on('excursion_term.excursion_id = excursion.id')
->join('excursion_departure')
->on('excursion_departure.excursion_id = excursion.id')
->where("excursion_term.date_from >= NOW()")
->groupBy("excursion_term.id");
return $this->createEntities($query->fetchAll());
Ale pokud chci to samé s LeanQuery
$query = $domainQuery->select('e')
->from('\Model\Entity\Excursion', 'e')
->join('e.term', 't')->select('t')
->join('e.departure', 'd')
->where("t.date_from >= NOW()");
return $query->getEntities();
Tak skončím s LeanMapper\Exception\InvalidArgumentException
/**
* @param string $alias
* @param Relationship|string $relationship
* @throws InvalidArgumentException
*/
public function addRelationship($alias, $relationship)
{
if (array_key_exists($alias, $this->relationships)) {
throw new InvalidArgumentException;
}
$this->relationships[$alias] = $relationship instanceof Relationship ? $relationship : Relationship::createFromString($relationship);
}
Co dělám špatně? Btw pokud zakomentuji throw new InvalidArgumentException; tak to funguje.
Editoval mirdič (28. 6. 2014 18:14)
před 5 lety
- Šaman
- Člen | 2275
Ahoj, po pročtení většiny tohohle vlákna jsem nenašel jednoduchý
způsob, jak vyřešit tohle:
Potřebuji vrátit počet navázaných property a nechci sčítat počet
záznamů na hotové kolekci (není to efentivní). Vytvořil jsem si
proto filtr:
<?php
public function count(Fluent $statement)
{
$statement->removeClause('select')->select('count(*)');
}
?>
Ale jak ho teď mám použít? Ideálně v anotaci, ale už jsem pochopil, že to asi nepůjde:
<?php
/**
* @property-read Task[] $tasksCount m:belongsToMany m:filter(count) <- odešle správný SQL dotaz, ale výsledek se snaží napasovat do kolekce
* @property-read int $tasksCount m:belongsToMany m:filter(count) <- belongsToMany nemůže vracet int
?>
Ok, druhá možnost je napsat si vlastní getTasksCount()
, ale
tady začalo peklo. Nízkoúrovňový přístup není zdokumentovaný ani
v těch střípkách v tomto vláknu. Filtr mám připravený a
zaregistrovaný, proč mám tedy znovu vytvářet instanci Filtering? A jak jí
předat parametr? Callback nefunguje, název filtru nefunguje, trvá to na
Filtering objektu.
Za nějakou podporu efektivního countování, ideálně na úrovni anotace bych se velmi přimlouval. Je to myslím docela častý požadavek a pokud to nejde efektivně, většina lidí to bude dělat hrubou silou dokud nezačne mít problémy s rychlostí.
Vím, že tohle by dobře řešila LeanQuery, ale zatím mám postavený projekt na LM a query teď přidávat nechci. Díky.
před 5 lety
- Tharos
- Člen | 1042
@Šaman: Ahoj, je to úplně jednoduché:
/**
* @property int $id
*/
class Book extends Entity
{
}
/**
* @property int $id
* @property-read int $booksCount m:useMethods
*/
class Author extends Entity
{
public function getBooksCount()
{
$rows = $this->row->referencing('book', 'author_id', new Filtering(function (Fluent $statement) {
$statement->removeClause('select')->select('COUNT(*) [count], [author_id]')
->groupBy(['author_id']);
}));
return empty($rows) ? 0 : reset($rows)->count;
}
}
$authorRepository = new AuthorRepository($connection, $mapper, $entityFactory);
foreach ($authorRepository->findAll() as $author) {
echo $author->id, "\n";
echo $author->booksCount, "\n";
}
SELECT [author].* FROM [author]
SELECT COUNT(*) [count], [author_id] FROM [book] WHERE [book].[author_id] IN (1, 2, 3, 4, 5) GROUP BY [author_id]
Pokud bys nechtěl mít názvy sloupců takhle hard-coded, samozřejmě by
sis je mohl zjišťovat z reflexe entity atp. Také by to šlo určitě
zobecnit do nějaké BaseEntity
.
Důležité je nezapomenout na tu GROUP BY
klauzuji, protože
bez ní není možné získat počty pro všechny entity z kolekce, nad kterou
se traverzuje.
Editoval Tharos (15. 8. 2014 9:29)
před 5 lety
- Šaman
- Člen | 2275
Díky, tohle funguje. Ale abych řekl pravdu, doufal jsem v jednodušší řešení, ideálně na úrovni anotace.
Je možné se nějak dostat na Fluent jiné property? Třeba v mém případě ty tasky projdou nejprve filtrováním, potřeboval bych dostat z názvu property její připravený $statement. V něm bych jen vyměnil část select a přidal groupBy a bylo by. Z toho už by se dala udělat nějaká zkratka v abstraktní Entitě.
před 5 lety
- Tharos
- Člen | 1042
Pokud bys to chtěl nějak zobecnit, postupoval bych osobně cca následovně:
- Pomocí
__call
vBaseEntity
bych odchytával metodygetCountOf<Name>
. - Pri vlastním získávání počtu bych použil metodu
getValueByPropertyWithRelationship
s tím, že bych jí předával potřebné filtry. Určitě bych při tom využíval metoducreateImplicitFilters
, aby byly do hry zapojeny i implicitní filtry. - Metoda
getValueByPropertyWithRelationship
musí navrátit entitu, a proto bych provedl takový drobný fígl. Pomocí filtru bych vykouzlil například takovýto SELECT:SELECT COUNT(*) `
count`, 1 `
isCountQuery`
s tím, že bych si pro takový výsledek vytvořil vlastní entitu a v mapperu bych ji v metoděgetEntityClass
vrace pro takovýRow
, které obsahuje ten sloupecisCountQuery
. :)
To by mělo být slušně univerzální řešení. Stačí Ti to takhle popsat, anebo mám napsat ten kód? :)
Editoval Tharos (15. 8. 2014 23:05)
před 5 lety
- Šaman
- Člen | 2275
Ahoj, narovinu – nejsem z toho moc moudrý.
Tohle nespěchá, zatím používam předchozí řešení, ale až budeš někdy
něco podobného řešit, dej sem prosím ten kód. :)
před 5 lety
- Matey
- Člen | 137
Zdravím,
čítal som https://forum.dibiphp.com/…orm-nad-dibi?p=16 a zapáčilo sa mi také riešenie ktoré @Tharos ukázal, mať celú entitu pokope a preloženú do potrebného jazyka a dokonca aj fuknčné persistovanie, ale nerozchodil som to pre najnovší LM 2.2.0
Problém bol v prepisovaní metod ktoré boli v tých časoch iné ako sú v LM 2.2
šlo by prosím túto ukážku aktualizovať? bol by som veľmi vďačný
před 5 lety
- Šaman
- Člen | 2275
Jestli chceš už připravený aktuální LM balíček, tak si natáhni přes
composer saman/leanmodel.
Ukázka natavení extension je tady,
plus samozřejmě informace o databázi.
Pokud budeš dědit od mých entit a repository, máš k dispozici nějaké
fičury navíc, ale všechno by mělo být velmi kompatibilní
s původním LM.
Pokud bys chtěl čistý LeanMapper, tak si do konfigurace doplň původní LeanMapper EntityFactory a jako base třídy používej zase ty původní (LeanMapper si composer stáhne taky, stejné tak Dibi).
Editoval Šaman (18. 8. 2014 12:24)
před 5 lety
- Matey
- Člen | 137
@Šaman áno ja už saman/leanmodel používam, už sme sa o tom bavili na nette fore, len mám problém napasovať do toho tie preklady, či už použijem mapper, entity, repository z leanmodelu alebo leanmapperu vždy narazím na to že potrebujem prepísať niečo čo je v leanmapperi ako protected, takže predpokladám že sa od tohoto času https://forum.dibiphp.com/…orm-nad-dibi?p=16 zmenilo niečo v LM
edit: translate entity som už rozbehol
Editoval Matey (4. 9. 2014 1:59)
před 5 lety
- Šaman
- Člen | 2275
Ajo, sorry, nějak mi nedošlo, komu odpovídám. S překladama nevím, ty nepoužívám, takže asi budeš muset počkat na Tharose.
před 5 lety
- sitnarf
- Člen | 27
Chtěl bych se zeptat, mam v databazi sloupec roles, ktery obsahuje string roli oddelenych carkou ve tvaru „admin,salesman“, ja to ale v entite chci mit jako pole, kde mam volat explode? Děkuji.
před 5 lety
- Etch
- Člen | 404
Jednoduše v getteru
public function getRoles(){
return explode(',', $this->row->roles);
}
před 5 lety
- sitnarf
- Člen | 27
Díky, a když to budu chtít hodit zpátky do db z array na CSV, tak přes
filter?
Jen menší rýpalství: Je to úplně správné z hlediska návrhu? Pochopil
jsem, že Entity má být nezávislá na persistentní vrstvě, tzn. že kdybych
entity exportoval pomocí jiného repository např. do JSONu a tam už to bude
čistě v array, rozsype se mi to, kvůli explode.
Etch napsal(a):
Jednoduše v getteru
public function getRoles(){ return explode(',', $this->row->roles); }
Editoval sitnarf (27. 11. 2014 10:18)
před 5 lety
- Etch
- Člen | 404
@sitnarf
Přes setter, protože filter nelze pustit na basic typ.
K tomu rýpalství…
Je z hlediska návrhu databáze správné mít v „databazi sloupec roles, ktery obsahuje string roli oddelenych carkou ve tvaru “admin,salesman”“?? :)
před 5 lety
- Etch
- Člen | 404
@sitnarf
A ještě co se toho getteru týče, tak nikdo neříká, že musí být naimplementován takto jednoduše. Já ho napsal takto triviálně jen na základě „požadavku“. Stejně tak to může vypadat třeba takto:
public function getRoles($asArray = false){
if($asArray === true){
return explode(',', $this->row->roles);
}
return (string)$this->row->roles;
}
public function setRoles($roles){
if(is_array($roles)){
$roles = implode(',', $roles);
}
$this->row->roles = (string)$roles;
}
následně pak getter funguje takto:
dump($user->roles);
/* vrací: */
"admin,salesman" (14)
dump($user->getRoles());
/* vrací: */
"admin,salesman" (14)
dump($user->getRoles(true));
/* vrací: */
array (2)
0 => "admin" (5)
1 => "salesman" (8)
a setter dokáže přijnout jak pole, tak string
$user->roles = 'admin,salesman';
$user->setRoles('admin,salesman');
$user->roles = array('admin','salesman');
$user->setRoles(array('admin','salesman'));
Editoval Etch (27. 11. 2014 14:02)
před 5 lety
- Tharos
- Člen | 1042
Také lze použít příznak m:passThru
, vypadalo by to
například takhle:
m:passThru(decodeFromJson|encodeToJson)
Alternativou je samozřejmě vlastní getter a setter. Pokud bys nechtěl
přímo pracovat s Row
, můžeš jej napsat takhle:
public function getRoles()
{
return Json::decode($this->get('roles'));
}
public function setRoles(array $roles)
{
$this->set('roles', Json::encode($roles));
}
Tenhle zápis jen vyžaduje mít i anotaci
@property array $roles
, na kterou se ty interní metody
Entity::get
a Entity::set
odvolávají.
Editoval Tharos (1. 12. 2014 12:29)
před 5 lety
- Joe Kolář
- Člen | 13
Zdravím,
v projektu mám entitu zápasu, která dvě vazby hasOne na entity družstva,
pro domácí a hostující tým. U každého družstva bych chtěl přistupovat
k jeho zápasům, jak na to?
<?php
/**
* @property int $id
* @property Team $homeTeam m:hasOne(home_team_id:team)
* @property Team $awayTeam m:hasOne(away_team_id:team)
*/
class Match extends Entity
{
}
/**
* @property int $id
* @property-read Match[] $matches m:useMethods
*/
class Team extends Entity
{
public function getMatches()
{
// jak zde implementovat pristup k zapasum pres home_team_id i away_team_id?
return [];
}
}
?>
Díky za vaše případné rady.
Editoval Joe Kolář (26. 12. 2014 10:25)
před 5 lety
- Etch
- Člen | 404
@JoeKolář:
Takhle z patra mě napadají dvě možnosti.
- Můžeš property zápasů prohnat filtrem a do statementu si přidat podmínku pro druhý sloupec.
- Můžeš si na to udělat view.
Oboje má své výhody i nevýhody.
před 5 lety
- Joe Kolář
- Člen | 13
@Etch:
Pokusil jsem se to vyřešit přes filtr:
<?php
public function allMatches(Fluent $statement, Team $team, Property $p)
{
$statement->removeClause('where')->where('[home_team_id] = ', $team->id, 'OR [away_team_id] =', $team->id);
}
?>
Do databáze jde upravený dotaz s podmínkou pro oba sloupce, který vrátí správný počet řádků. Ale průchodem přes BelongsToMany vazbu mi to nazpět vrátí jen entity (řádky), u kterých sedí id s hodnotou ve sloupci uloženým v BelongsToMany::columnReferencingSourceTable. Dočasně jsem to vyřešil separátní metodou v MatchRepository, ale moc se mi to řešení nezdá. Jak obejít to omezení v BelongsToMany?
před 5 lety
- JuniorJR
- Člen | 181
@JoeKolář Ahoj, zkusil bych neco ve stylu:
<?php
class Team extends Entity
{
public function getAllMatches()
{
$matchTableName = $this->mapper->getTable('Match');
// @see https://github.com/Tharos/LeanMapper/blob/develop/LeanMapper/Row.php#L233
$allMatches = array();
foreach ($this->row->referencing($matchTableName, 'home_team_id') as $homeMatch) {
$allMatches[$homeMatch->id] = $homeMatch;
}
foreach ($this->row->referencing($matchTableName, 'away_team_id') as $awayMatch) {
$allMatches[$awayMatch->id] = $awayMatch;
}
return $allMatches;
}
}
?>
Editoval JuniorJR (30. 12. 2014 9:57)
před 5 lety
- Joe Kolář
- Člen | 13
@JuniorJR: Chtěl jsem to vyřešit na jeden dotaz, takhle je to na dva, ale běží to. Musí se teda ještě prohrat přes Entity Factory (referencing vrací jen Row) a oživit, ale ok. Díky moc.
Editoval Joe Kolář (30. 12. 2014 15:43)
před 4 lety
- Felix
- Nette Core | 898
Udelal jsem fork Davidova db-benchmark repa a udelal par vylepseni.
Muj fork:
https://github.com/…db-benchmark
Knihovny:
- NDB 2.0
- NDB 2.1
- NDB 2.2
- NDB 2.3
- NDB 2.4-dev
- NotORM
- LeanMapper
- Doctrine2
Mam i aktualni benchmarky:
https://github.com/…nchmark-logs
před 4 lety
- martin.knor
- Člen | 23
Ahoj, resili jste nekdo u translate repository metodu findBy? Jde o to ze pokud potrebuju filtrovat pomoci sloupce ktery je v translate tabulce, tak mi to samozrejme hleda podle sloupce v puvodni – prekladane tabulce.
Mam to vyresene takto, ale nevim jestli je spravne vytvaret prazdnou entitu kvuli tomu abych zjistil ktere pole jsou translated.
public function findBy(array $options, $fetch = \Collection::ANY, $lang = null)
{
$table = $this->getTable();
$query = $this->createFluent($lang);
$entityClass = $this->mapper->getEntityClass($table);
$langCol = array();
if (is_subclass_of($entityClass, 'Entity\TranslatableEntity')) {
$langCol = $this->getTranslatableColumns($this->createEmptyEntity($entityClass));
}
foreach ($options as $col => $val) {
$query->where('%n.%n = %s', in_array($col, $langCol) ? $this->mapper->getTranslationTable($table) : $table, $col, $val);
}
$collection = $this->createEntities($query->fetchAll());
return $fetch === \Collection::ONE
? $collection->first()
: $collection;
}
A dalsi vec, pokud bych chtel prochazet vsechny preklady pro jeden radek (napriklad pro edit form), jak na to? Mam vytvorit entitu pro preklady a pripojit ji k prekladane entite pomoci belongsToMany? Ikdyz to mi prijde celkem pracne pro kazdou prekladanou entitu tohle delat.
Editoval martin.knor (22. 3. 2015 16:25)
před 4 lety
- Pavel Janda
- Člen | 801
@Tharos Předpokládám, že neplánuješ upgrade LeanMapperu pro Dibi ~3.0? …
Jsem si jist, že by to mnozí uvítali. :) Nepovažuji LeanMapper, natož Dibi za mrtvý projekt. (https://packagist.org/…mapper/stats)
Editoval Pavel Janda (9. 11. 2015 8:59)
před 4 lety
- castamir
- Člen | 631
@PavelJanda co presne je tam za problem? Ja LM aktivne pouzivam na vice projektech…
před 4 lety
- Pavel Janda
- Člen | 801
castamir napsal(a):
@PavelJanda co presne je tam za problem? Ja LM aktivne pouzivam na vice projektech…
Vždyť jsem to právě (haha, před více jak 2 měsíci) napsal.
před 3 lety
- castamir
- Člen | 631
Vydal jsem verzi 3-RC1, ktera je kompatibilni s dibi v3.x
před 3 lety
- Šaman
- Člen | 2275
Ahoj, řeším práci s vazebními tabulkami. Mám tam jeden sloupec navíc (kromě dvou sloupců se idčkama spojovaných entit). Čtení mám zvládnuté přes filtr, ale zjistil jsem, že to toho sloupce neumím zapisovat. Zkoušel jsem ohnout LM aby mi vzal takovýto zápis:
<?php
// Přidání místnosti osobě, ale s nastavením sloupce 'hidden' na TRUE
$this->person->addToRooms($roomId, ['hidden' => TRUE]);
?>
To se mi ale nepodařilo. Je nějaký jednodušší způsob, než si
vytvářet novou entitu?
Díky.
před 3 lety
- Pavel Janda
- Člen | 801
@castamir Nechceš implementovat něco jako psal @Šaman výše? Určitě by to použil každý druhý člověk, který používá LeanMapper. Já bych to též ocenil.
před 3 lety
- Šaman
- Člen | 2275
Jen resume, jak jsem to řešil a nepřišel na jiny způsob:
Musel jsem to upravit tak, abych vytvořil pravé entity. Problém je za prvé že jim neodpovídá entita v ER diagramu a hlavně musím dodatečně k již existujícím spojovacím tabulkám doplnit sloupec id.
Čtení ze spojovacích tabulek s dodatečným sloupcem je vykoušené přes filtry, takže jde opravdu jen o možnost zápisu. I za cenu toho, že „editace“ by znamenala odstranit starou vazbu a přidat novou, jinak nakonfigurovanou.
před 3 lety
- Pavel Janda
- Člen | 801
Taky to tak občas řeším, ale neuvěřitelně se mi to nelíbí. Vazba, která má vlastnost (sort, locale, whatever), ještě nemusí být nutně další entita. Není to entita, je to vazba.
před 3 lety
- Ravenss
- Člen | 12
Jakým způsobem docílím v entitě aby mohla mít ze stejné tabulky další „nadřazenou“ entitu?
(Potřebuji generovat kategorie a z nich hierarchické menu)
před 3 lety
- janpecha
- Člen | 58
@Ravenss myslíš něco takového?
/**
* @property int $id
* @property string $name
* @property Category|NULL $parent m:hasOne(parent_id)
*/
class Category extends Entity
{
}