Oznámení
před 5 lety
- Tharos
- Člen | 1042
@plzurq: Noo, takhle by to právě teď myslím mělo fungovat. :) Zkoušel jsi to?
Edit: Ještě jsem se více zaměřil na Tvůj příklad a už asi rozumím, co potřebuješ. Zkus to takhle:
$tags=[2,3,4];
$article = //vytahnu z DB article
$article->replaceAllTags($tags);
$this->repo->persist($article);
Editoval Tharos (7. 4. 2014 22:48)
před 5 lety
- Tharos
- Člen | 1042
Jestli mi dnes v Lean Mapperu osobně něco opravdu chybí, tak je to možnost namapovat výsledek nějakého komplexnějšího SQL dotazu na inicializovaný strom entit. Něco, co například v Doctrine krásně řeší DQL (položení dotazu a následná hydratace entit).
Ukažme si na triviálním zadání, o co vlastně jde: vypište autory knih, kteří napsali knihy s průměrným hodnocením lepším než tři hvězdičky, a pro každého z těch autorů vypište všechny takové knihy.
Prvním možným řešením je vybrat autory, inner-joinout
si jejich knihy, vzniklou relaci omezit podle požadovaného hodnocení knih a
pro optimalizaci přenášených dat lze autory sloučit pomocí
GROUP BY
. Během vypisování autorů je pak zapotřebí při
traverzování na knihy opět vyfiltrovat ty s požadovaným
hodnocením.
Druhým možným řešením je vybrat autory a relevantní knihy najednou v rámci jednoho dotazu. Opět bude obsahovat inner join a omezení podle požadovaného hodnocení knih.
Každé z výše uvedených řešní má své výhody a nevýhody. Slabinou prvního řešení je to, že se musí dvakrát filtrovat hodnocení knih. Na druhou stranu se nepřenáší redundandní data. A přesně opačně je pak na tom druhé řešení. Knihy se filtrují pouze jednou, ale ve výsledku se zase přenášejí redundandní data. Klasika. :)
První přístup je sice často výhodnější, ale zejména při joinu většího množství tabulek s košatými restrikcemi je trochu neohrabaný a také ztrácí na efektivitě.
Lean Mapper, inspirovaný NotORMem, doteď podporovat pouze první zmíněný přístup, ale to změnil tento nenápadný commit. Od tohoto commitu lze výsledek jakéhokoliv SQL dotazu „hydratovat“ na inicializovaný strom entit, takže už nic nestojí v cestě k používání druhého přístupu.
Již teď je k vidění v testech ukázka takové „dřevařské hydratace“. Jak vidno, zatím je to záležitost dost krkolomná, ale to se velmi brzy změní. Nic nebrání implementaci nadstavby, která všechnu tu dřevařskou práci udělá za nás. My, koncoví programátoři, to jen budeme dirigovat nějakým LQL:
$query = $queryHelper->createDomainQuery();
$query->select('a, b')
->from(Author::class, 'a')
->join('a.books', 'b')
->where('b.rating >= ?', 3);
$authors = $query->getResult();
foreach ($authors as $author) {
echo "$author->name\n;"
foreach ($author->books as $book) {
echo "- $book->name\n";
}
}
Databázi se položí jeden jediný dotaz.
Celé takové LQL bych rozvíjel v separátním repositáři jako nadstavbu nad Lean Mapperem, kterou lze volitelně použít. Chtěl bych, aby tak vzniklo nějaké výsledné best practice pro modely využívajících Lean Mapper. Má představa je, že takové řešení bude mít tři vrstvy:
1) Sadu nástrojů pro vyloženě ruční hydratování
výsledku nějakého SQL a také pro sestavení takového dotazu. Půjde
o ekvivalent mapování výsledku nativního SQL na entity v Doctrine
2) LQL jako nadstavbu nad tím, která bude sice o něco
omezenější, ale zase bude extrémně pohodlná na používání. Půjde
o ekvivalen DQL, HQL, prostě o další OQL
3) Základ pro Query objekty, které by se překládaly právě
do LQL. Půjde víceméně o ekvivalent Query objektů
z Kdyby\Doctrine
Myslím, že to celé bude pro Lean Mapper zase obrovský krok dopředu.
Maximálně vítám vaše názory, nápady, rady… Můžeme to řešit buďto zde, anebo v této issue na GitHubu. Díky!
Editoval Tharos (28. 5. 2014 16:19)
před 5 lety
- Tharos
- Člen | 1042
Tak výše popsaná novinka už nabývá kontur a dokonce se už i začíná rýsovat. Stay tunned :).
před 5 lety
- Mesiah
- Člen | 242
Ahoj,
prosím Vás, zajímalo by mě, jak můžu pomocí LM elegantně udělat tento
use-case:
Mám entitu Question a kažná má kolekci Vote – hlasů, kde hlas má Value,
což je hodnota +1/-1.
Jak elegantně spočítat skóre?
Pokoušel jsem o něco, ale nikam to nevedlo :/
/**
* Description of Question
*
* @property string $title
* @property string $text
* @property User $author m:hasOne
* @property Answer[] $answers m:belongsToMany
* @property Tag[] $tags m:hasMany
* @property Vote[] $votes m:belongsToMany
* @property-read int $score
*/
class Question extends BusinessEntity
{
public function getScore()
{
return $this->getValueByPropertyWithRelationship('votes', new \LeanMapper\Filtering(function (\LeanMapper\Fluent $statement) {
$statement->select('sum(value)');
}));
}
}
před 5 lety
- Tharos
- Člen | 1042
@Mesiah: Ahoj,
jdeš na to v podstatě dobře, ale fungující (přímočaré) řešení je následující:
/**
* @property int $id
* @property string $name
* @property-read int|null $score m:useMethods
*/
class Question extends Entity
{
/**
* @return int|null
*/
public function getScore()
{
$rows = $this->row->referencing('vote', 'question_id', new Filtering(function (Fluent $statement) {
$statement->removeClause('select')->select('SUM([value]) [score], [question_id]')->groupBy('question_id');
}), Result::STRATEGY_UNION);
return empty($rows) ? null : reset($rows)->score;
}
}
Mimochodem je to krásný příklad na tu UNION strategii. :)
Důležité je, že ten výsledek SQL s hlasováním vůbec nepotřebuješ
mapovat na entity, a proto použití metody
getValueByPropertyWithRelationship
nevede ke kýženému
výsledku.
Podmínkou k tomu, aby to fungovalo, je použití aktuální
release větve (verze 2.2.0 je těsně před vydáním). Jde o to, že
v dibi chybí jedna drobnost pro sestavení potřebného dotazu a supluje to
aktuální LeanMapper\Fluent
.
Pro zajímavost uvedu, že pro takovouto databázi se při traverzování přes všechny otázky a vypisování jejich skóre položí následující optimalizované dotazy:
SELECT `question`.* FROM `question`;
(SELECT SUM(`value`) `score`, `question_id` FROM `vote` WHERE `vote`.`question_id` = 1 GROUP BY `question_id`) UNION (SELECT SUM(`value`) `score`, `question_id` FROM `vote` WHERE `vote`.`question_id` = 2 GROUP BY `question_id`) UNION (SELECT SUM(`value`) `score`, `question_id` FROM `vote` WHERE `vote`.`question_id` = 3 GROUP BY `question_id`);
Editoval Tharos (17. 4. 2014 12:05)
před 5 lety
- Tharos
- Člen | 1042
Ahoj přátelé,
tak jsem dnes vydal verzi 2.2.0. Zde jsou nějaké užitečné odkazy:
Z novinek (kterých je relativně málo) bych rád vyzdvihl následující:
- Zachovávání kolekce ID ve Fluent
- Nová metoda
Connection::hasFilter
- Nově se lze odkazovat na aliasy v SQL (viz praktická ukázka)
- „Preloading“, který umožňuje vznik nadstavby zvané LQL
Podrobný přehled změn viz diff na GitHubu.
Zajímavé je, že změny neobsahují asi žádný BC break, takže aktualizace knihovny je vřele doporučovaná.
Dobrou zprávou budiž, že se trochu hýbe dokumentace. :) Zrovna dnes přibyly další části stránky věnované entitám.
Nadále platí, že dokud nebude dokumentace dokončená, snažím se
aktivně zodpovídat všechny dotazy v issues na GitHubu a také
jsem k dispozici na Skype jako v.kohout
. Nebojte se mě prudit :).
Tím, že Lean Mapper používáte, mu děláte velkou službu.
Na závěr napíšu jedno takové malé zamyšlení… Zhruba od ledna tohoto roku jsem díky projektu, na kterém jsem se začal podílet, de facto denně ve styku nejen s Lean Mapperem, ale i s Doctrine 2. Umožnilo mi to poznat Doctrine mnohem hlouběji, než jsem ji znal, a vidět tak, jak si obě knihovny vedou vedle sebe. Tato zkušenost mě znovu utvrdila v tom, že Lean Mapper má smysl, že je k Doctrine „důstojnou příbuznou“ a výbornou alternativou v mnoha případech. Za úplně nejlepší považuji umět pracovat s oběma těmito ORM a podle konkrétní aplikace se rozhodovat, kterou použít. V podstatě tedy vylučuji, že by se z mé strany z Lean Mapperu stala „dále neudržovaná knihovna“ bez podpory a budoucnosti.
Nyní se chci zaměřit na další rozvoj dokumentace (vím, že je to evergreen) a také na nadstavbu Lean Query, která, až bude hotová, přinese z mého pohledu obrovský posun v tom, jak elegantní budou vaše modely nad Lean Mapperem moci být.
Díky za přízeň!
Editoval Tharos (28. 4. 2014 1:50)
před 5 lety
- Mesiah
- Člen | 242
Ahoj,
narazil jsem na zvláštní chybku, ono to možná bude souviset s nějakou
direktivou PHP, ale nejsem si jistý.
Jde o tohle, mám deklarované property třídy v phpdocu a dostávám
chybku:
LeanMapper\Exception\MemberAccessException Cannot access undefined property 'create' in entity Model\Entity\User.
.
Třída na které se to děje
namespace Model\Entity;
/**
* Description of BusinessEntity
*
* @author Sebastian
* @property int $id
* @property \DateTime|NULL $create
* @property \DateTime|NULL $storno
*/
class BusinessEntity extends \LeanMapper\Entity
{
/**
* Create new entity
*/
public function __construct($args = NULL) {
parent::__construct($args);
if (!isset($this->create)) {
$this->create = new \DateTime('now');
}
}
/**
* Storno entity
* @return \Model\Entity\BusinessEntity
*/
public function storno() {
$this->storno = new \DateTime('now');
return $this;
}
}
namespace Model\Entity;
/**
* Description of User
*
* @author Sebastian
* @property string $name
* @property string $email
* @property string $password
* @property-read int $score
* @property Question[] $questions m:belongsToMany
* @property Answer[] $answers m:belongsToMany
*/
class User extends BusinessEntity
{
}
Narazili jste na tohle? Jen doplním, že na vyvojovém serveru mi to jede, ale na testovacím (subdoména na wedos) bohužel ne.
před 5 lety
- Tharos
- Člen | 1042
@Mesiah: To zní zajímavě. :)
Vyzkoušel bys prosím, co na tom testovacím serveru vypíše následující skript?
$reflection = new ReflectionClass('Model\Entity\User');
var_dump($reflection->getDocComment());
před 5 lety
- Mesiah
- Člen | 242
Tharos napsal(a):
@Mesiah: To zní zajímavě. :)
Vyzkoušel bys prosím, co na tom testovacím serveru vypíše následující skript?
$reflection = new ReflectionClass('Model\Entity\User'); var_dump($reflection->getDocComment());
Hmm, je to zvláštní, ale podle všeho je to v pořádku, více divné je že po zavolání toho snippetu, co jsi mi poslal ta chyba prostě přestane vyskakovat… ale jakmile jej opět zakomentuju, tak je tak zpět… :/
string(274) "/** * Description of User * * @author Sebastian * @property string $name * @property string $email * @property string $password * @property-read int $score * @property Question[] $questions m:belongsToMany * @property Answer[] $answers m:belongsToMany */"
Edit:
Nazdárek,
zkusil jsem ty entity přepsat tak, aby nepoužívaly definici slotů
v phpdocu, ale pomocí metod a to funguje.
Trochu jsem to procházel s původním řešením a dostal jsem se
k tomuhle:
V metodě
public static function getReflection(IMapper $mapper = null)
na
třídě Entity se přistupuje do pole
static::$reflections
a to když dumpnu obsahuje:
array (1)
"Model\Entity\User" => array (1)
"LeanMapper\DefaultMapper" => LeanMapper\Reflection\EntityReflection #0c24
mapper private => LeanMapper\DefaultMapper #e991 { ... }
properties private => array ()
getters private => array ()
setters private => array ()
aliases private => NULL
docComment private => FALSE
internalGetters private => array (7) [ ... ]
name => "Model\Entity\User" (17)
prostě jako by to odstřihlo php doc – neexistuje na to nějaká direktiva v php?
Editoval Mesiah (30. 4. 2014 23:36)
před 5 lety
- mirdič
- Člen | 41
Ahoj,
nejprve bych chtěl poděkovat za povedenou knihovnu. LeanMapper se nyní pokouším implementovat do jednoho projektu. Při použití filtrů jsem narazil na dle mého vcelku nestandardní chování a nevím, zda je to chyba na mé straně či na straně LM. Pokusím se problém popsat.
Moje aplikace
Mám entitu Event a v ní property EventTerm[] m:belongToMany(). Potřeboval
bych EventTerm[] seřadit a limitovat.
Entita
/**
* @property int $id
* ...
* @property EventTerm[] $term m:belongsToMany(#union) m:filter(order,limit)
* ...
*/
class Eventextends Entity
{
}
Filtr
class DefaultFilter
{
public static function limit(\LeanMapper\Fluent $statement, $args = null)
{
if(!empty($args['limit'])){
$statement->limit($args['limit']);
}
}
public static function order(\LeanMapper\Fluent $statement, $args = null)
{
if(is_array($args) and !empty($args['order'])){
$statement->orderBy($args['order']);
}
}
}
Presenter
foreach ($eventRepository->findAll() as $event) {
$filter = array('limit' => 5, 'order' => 'date_from ASC');
foreach ($excursion->getTerm($filter) as $term) {
var_dump($term->date_from);
}
}
V tomto okamžiku se vše chová dle předpokladů Termíny jsou limitovány u každého eventu na maximálně 5 záznamů.
SQL dotaz vypadá takto:
(
SELECT `event_term`.*
FROM `event_term`
WHERE `event_term`.`event_id` = 8
ORDER BY date_from ASC
LIMIT 5)
UNION (
SELECT `event_term`.*
FROM `event_term`
WHERE `event_term`.`event_id` = 9
ORDER BY date_from ASC
LIMIT 5)
UNION (
SELECT `event_term`.*
FROM `event_term`
WHERE `event_term`.`event_id` = 13
ORDER BY date_from ASC
LIMIT 5)
Problém ale nastane v případě, že přidám jiný filtr, nebo pouze změním pořadí filtrů:
* @property EventTerm[] $term m:belongsToMany(#union) m:filter(limit,order)
V tomto případě je SQL dotaz následující a počet Termínu je limitovaný celkem na 5 záznamů, ale ne u každého záznamu, ale celkem.
(
SELECT `event_term`.*
FROM `event_term`
WHERE `event_term`.`event_id` = 8
ORDER BY date_from ASC)
UNION (
SELECT `event_term`.*
FROM `event_term`
WHERE `event_term`.`event_id` = 9
ORDER BY date_from ASC
LIMIT 5)
UNION (
SELECT `event_term`.*
FROM `event_term`
WHERE `event_term`.`event_id` = 13
ORDER BY date_from ASC
LIMIT 5)
LIMIT 5
Všimnul jsem si, že u prvního subselectu chybí limit, který je pak až úplně na konci.
Dále bych se chtěl zeptat, jak nejlépe vyřešit situaci, kdy bych chtěl v anotaci definovat defaultní řazení například:
m:filter(order#date_from ASC)
a následně mít možnost při výpisu, řazení změnit na jiné
$event->getTerm(array("order" => 'date_from DESC'))
ale takto se mi předá do filtru pouze argument uvedený v anotaci.
verze LeanMapperu 2.2.0
Editoval mirdič (17. 5. 2014 22:09)
před 5 lety
- Tharos
- Člen | 1042
@mirdič: Díky za skvěle popsané ukázky.
Ad 1) Zjistil jsem, že jde bohužel o bug v dibi. Zde je dobře patrný:
$connection->select('*')
->from('author')
->where('id = 1')
->orderBy('born')
->limit(1) // limit behind orderBy
->union(
$connection->select('*')
->from('author')
->where('id = 2')
->orderBy('born')
->limit(1) // limit behind orderBy
)
->test();
$connection->select('*')
->from('author')
->where('id = 1')
->limit(1) // limit in front of orderBy
->orderBy('born')
->union(
$connection->select('*')
->from('author')
->where('id = 2')
->limit(1) // limit in front of orderBy
->orderBy('born')
)
->test();
První fluent vygeneruje následující správný dotaz:
SELECT *
FROM [author]
WHERE id = 1
ORDER BY [born]
LIMIT 1
UNION (
SELECT *
FROM [author]
WHERE id = 2
ORDER BY [born]
LIMIT 1
)
Zatímco druhý fluent vygeneruje následující chybný dotaz:
SELECT *
FROM [author]
WHERE id = 1
ORDER BY [born]
UNION (
SELECT *
FROM [author]
WHERE id = 2
ORDER BY [born]
LIMIT 1
)
LIMIT 1
Jak je vidět, dibi funguje dobře pouze tehdy, pokud je limit
voláno až za orderBy
. Dokud to nebude opravené (buďto přímo
v dibi, anebo to nějak fixnu v Lean Mapperu), řešením je hlídat pořadí
filtrů tak, aby by limit volán jako poslední…
Ad 2) On se ten argument předá, ale jako další v pořadí. :) Schválně si zkus do toho filtru přidat další (volitelný) parametr a do něj se předá ta dynamicky daná hodnota.
Což samozřejmě není nic moc a asi by dávalo lepší smysl „fixní argumenty“ nahrazovat „dynamickými“ a ne dělat merge jako nyní. Jako hotfix můžeš použít to, že si do toho filtru přidáš další (volitelný) argument, jehož hodnotu pak upřednostníš, bude-li daná. No a já v nějaké další verzi to přetěžování doladím.
Editoval Tharos (18. 5. 2014 16:37)
před 5 lety
- Tharos
- Člen | 1042
Ahoj,
nadstavba Lean Query (také popsaná zde) se v posledních dnech výrazně posunula a již si s ní lze hrát.
Co aktuálně umí nejlépe ukazují testy, hlavně pak tento.
Za základní ukázku použití můžeme považovat například tohle:
$domainQuery->select('b')
->from(Book::class, 'b')
->join('b.author', 'a')->select('a')
->leftJoin('b.tags', 't')->select('t');
$books = $domainQuery->getEntities();
foreach ($books as $book) {
echo "$book->name\r\n";
echo "\tAuthor: {$book->author->name}\r\n";
echo "\tTags:\r\n";
foreach ($book->tags as $tag) {
echo "\t\t$tag->name\r\n";
}
}
Když tento kód spustíme nad tou SQLite databází, která se nachází v testech, vypíše se následující:
The Pragmatic Programmer
Author: Andrew Hunt
Tags:
popular
ebook
The Art of Computer Programming
Author: Donald Knuth
Tags:
Refactoring: Improving the Design of Existing Code
Author: Martin Fowler
Tags:
ebook
Introduction to Algorithms
Author: Thomas H. Cormen
Tags:
popular
UML Distilled
Author: Martin Fowler
Tags:
Pointou je, že se vykoná je jeden jediný dotaz (vypadá takto).
Asi jste si všimli, že takto se nám vybraly i ty knihy, které nejsou
označeny žádnými tagy. Není problém změnit leftJoin
na
join
:
$domainQuery = $domainQueryFactory->createQuery();
$domainQuery->select('b, a')
->from(Book::class, 'b')
->join('b.author', 'a')
->join('b.tags', 't');
$books = $domainQuery->getEntities();
foreach ($books as $book) {
echo "$book->name\r\n";
echo "\tAuthor: {$book->author->name}\r\n";
}
A rázem z toho máme výpis pouze těch knih, které mají nějaké tagy:
The Pragmatic Programmer
Author: Andrew Hunt
Refactoring: Improving the Design of Existing Code
Author: Martin Fowler
Introduction to Algorithms
Author: Thomas H. Cormen
V této durhé ukázce jsme si také ukázali, že aliasy v
SELECT
části lze zapisovat hromadně a také, že lze v joinech
pracovat s daty, které pak ale nejsou součástí SELECT
,
protože je pak třeba nemusíme vůbec chtít vypisovat. Položený SQL dotaz
vypadá
takto.
Dále bych vám chtěl ukázat tuto situaci:
$domainQuery = $domainQueryFactory->createQuery();
$domainQuery->from(Book::class, 'b')->select('b')
->join('b.author', 'a')
->join('b.tags', 't');
$books = $domainQuery->getEntities();
foreach ($books as $book) {
echo "$book->name\r\n";
echo "\tAuthor: {$book->author->name}\r\n";
echo "\tTags:\r\n";
foreach ($book->tags as $tag) {
echo "\t\t$tag->name\r\n";
}
}
Všimněte si, že pracujeme s daty z celkem čtyř tabulek,
SELECT
se odehrává jenom do jedné tabulky, ale při výpise pak
ta ne-vyselektovaná data chceme vypsat. Lean Mapper (s Lean Query) si s tím
poradí a potřebná data si efektivně donačte lazy způsobem tak,
jak byste očekávali. Výstup bude opět následující:
The Pragmatic Programmer
Author: Andrew Hunt
Tags:
popular
ebook
Refactoring: Improving the Design of Existing Code
Author: Martin Fowler
Tags:
ebook
Introduction to Algorithms
Author: Thomas H. Cormen
Tags:
popular
S tím, že se položí tyto čtyři dotazy.
Zde si neodpustím drobné srovnání s Doktrínou, která by v takovémto případě už spadla do N+1 problému. Příjemné na Lean Mapperu je, že namísto do takového cyklického stylu kladení dotazů spadne do „NotORM stylu“.
Asi jste si všimli, že syntaxe je velmi podobná DQL (respektive Query Builderu v Doctrine). To je umělecký záměr, protože API Doctrine mi přijde zdařilé, intuitivní. Díky tomu vy, kteří máte zkušenosti s Doktrínou, asi budete Lean Query ihned chápat a umět používat.
Nadstavba je zatím trochu v plenkách, ale jelikož ji teď sám chci použít v jednom svém projektu, bude se velmi rychle vyvíjet. Roadmap na následující dny je cca následující:
- Doplnit
WHERE
klauzuli, se kterou to teprve bude plnit ten hlavní účel - Zrefaktorovat vnitřní implementaci. Nyní je řada věcí na můj vkus až příliš array based, v plánu je dekompozice do menších objektů s jasným API
Mám z toho, jak Lean Query krystalizuje, osobně velkou radost. Ukazuje se, jak je Lean Mapper na takovouto nadstavbu perfekně připravený. Všimněte si třeba toho, že celý Lean Query je nyní „záležitost na pár řádků“.
Také se mi líbí obrovský potenciál a možnosti, jaké nabízí. Plánuji například povolit volitelně „obousměrný preloading“, kdy se vám zároveň zinicializuje vlastnící i inverzí strana relace (používám termíny z Doctrine, protože o lepších nevím). Takže budete moci například traverzovat z autorů zpátky na knihy, aniž by se položily nové dotazy databázi (pokud vám samozřejmě budou stačit data, která už jsou z databáze načtená). Tohle celé používat v modelech v kombinaci s nějakými hezkými Query Objekty bude radost!
Odhaduji, že v horizontu několika dní bude k dispozici nějaká RC verze s veškerou základní funkcionalitou. Další funkconalitu pak budu rád doplňovat podle potřeby – mé i dalších uživatelů. Ono vymyslet by se toho dala spousta a praxe nejlépe ukáže, co je jak důležité.
Enjoy!
Editoval Tharos (28. 5. 2014 15:12)
před 5 lety
- Jan Suchánek
- Backer | 403
@Tharos: Moc pěkné!
Editoval jenicek (28. 5. 2014 15:30)
před 5 lety
- Tharos
- Člen | 1042
Inspirovaného knihovnou LeanMapperQuery mě napadla jedna taková neskutečně šikovná věc. Nebo alespoň doufám, že šikovná :). Představte si, že by šlo něco takového:
$authors = $authorRepository->findAll();
foreach ($authors as $author) {
// ... print something from Author
foreach ($author->findBooks(function (DomainQuery $query) {
$query->where('this.name != %s', 'Lean Mapper Guide')
->leftJoin('this.tags', 't')
->where('t.name IN %in', ['ebook', 'printed']);
}) as $book) {
// ... print something from Book
foreach ($book->tags as $tag) {
// ... print something from Tag
}
}
}
Vykonají se celkem dva dotazy: první získá všechny autory, druhý
získá všechny jejich knihy bez knihy Lean Mapper Guide
a
zároveň také všechny tagy těchto knih, které mají název buďto
ebook
nebo printed
.
Při iteraci nad takovými tagy se už žádný další dotaz nepoloží.
V tomhle já osobně vidím neskutečně silnou zbraň. Umožňuje to začít mezi entitami traverzovat klasickým způsobem a až někde „v hloubi“, až se zjistí, že by se něco hodilo pre-loadnout, tak tak učinit.
Svým způsobem je to varianta na criteria matching v Doctrine, ale s tou obrovskou výhodou, že tady by šel udělat i JOIN dalších dat, která by se tím tak předpřipravila.
Editoval Tharos (2. 6. 2014 1:37)
před 5 lety
- Šaman
- Člen | 2275
Tak tohle už je mokrej sen všech programátorů – položit složitý sql
dotaz a orm mi vrátí objektové entity, aniž by to znamenalo používat
brutální moloch, nebo spousty pomocných dotazů. :)
Škoda jen, že od verze 1.3 mi další novinky tak trochu unikají, protože
nestačím sledovat vývoj a dokumentace je achillova pata většiny projektů,
které znám. U LM by ale byla fakt škoda, kdyby se kvůli tomu
nepoužíval.
před 5 lety
- JuniorJR
- Člen | 181
Zdravim, zajimalo by me, jakym zpusobem prakticky resite nebo by jste resili mockovani entit? Prijde mi to pomerne problematicke, tim, ze jsou jejich vlastnosti definovany zpravidla pres anotace. Zatim me jako jedine „rozumne“ reseni napada jit cestou dedeni definovanych entit a prepsani veskerych properties na gettery/settery.
V soucasnosti mi tomuto brani pouze to, ze nepouzivam zadnou entity factory (pouzivam starou verzi LM).
Jde mi hlavne o to, ze bych se z duvodu komplexiti rad vyhnul nutnosti testovani na „zive“ DB.
Diky moc za nazory.
JR
před 5 lety
- Tharos
- Člen | 1042
Ahoj,
nad aktuální develop
verzí lze entity testovat v podstatě
libovolně i včetně traverzování a bez připojení k databázi. Uvedu
ukázku:
class Mapper extends DefaultMapper
{
protected $defaultEntityNamespace = null;
}
/**
* @property int $id
* @property string $name
* @property Book[] $books m:belongsToMany
*/
class Author extends Entity
{
}
/**
* @property int $id
* @property string $name m:passThru(emphase)
*/
class Book extends Entity
{
protected function emphase($value)
{
return strtoupper($value);
}
}
////////////////////
////////////////////
$connection = new Connection([
'lazy' => true,
]);
// mock data
$authorsResult = Result::createInstance([
['id' => 1, 'name' => 'John Doe'],
['id' => 2, 'name' => 'Jane Roe'],
['id' => 3, 'name' => 'Somebody'],
], 'author', $connection, $mapper);
$booksResult = Result::createInstance([
['id' => 1, 'name' => 'First book', 'author_id' => 1],
['id' => 2, 'name' => 'Second book', 'author_id' => 1],
], 'author', $connection, $mapper);
$authorsResult->setReferencingResult($booksResult, 'book', 'author_id');
// use it in entities
$author = new Author($authorsResult->getRow(1));
$author->makeAlive($entityFactory, $connection, $mapper);
echo $author->name, "\n";
$author->name = 'New name';
echo $author->name, "\n";
foreach ($author->books as $book) {
echo "- $book->name\n";
}
Fakt stačí jenom z nějakých dat vytvořit resulty a podle potřeby je
provázat (pokud se má testovat i traverzování). Dá se tak testovat
vysokoúrovňové API, hodnoty v Row
… Abych pravdu řekl,
v souvislosti s entitami aktuálně nevím o záležitosti, která by nebyla
snadno testovatelná a to i bez databáze.
Akorát verze 1.3 na tom bude výrazně hůře, minimálně nebude umět to ruční provazování resultů… :/ Ale většina věcí myslím už v ní šla řešit.
Co přesně bys potřeboval otestovat? Rád Tě ještě lépe navedu.
před 5 lety
- JuniorJR
- Člen | 181
@Tharos: Moc diky za nazornou ukazku, presne tohle jsem potreboval! Je to fakt parada, nevim, co presne za verzi dosud pouzivam, tusim nekdy z konce minuleho leta, nektere metody byly asi prejmenovany nebo pridany, kazdopadne to uz je muj problem. Delsi dobu uvazuji nad updatem knihovny, toto bude asi konecne ten pravy impulz :) Jeste jednou diky za perfektni nasmerovani, z nejakeho duvodu me nenapadlo, ze by to mohlo byt ve skutecnosti tak „primocare“.
Edit: Tak po aktualizaci knihovny na aktualni vyvojovou verzi uvedeny postup funguje bezvadne. Dalsim problemem, ktery me ceka, bude mockovani repositaru :)
Editoval JuniorJR (4. 6. 2014 23:28)
před 5 lety
- JuniorJR
- Člen | 181
K memu zdeseni jsem zahy po updatu na aktualni dev verzi narazil na problem,
ktery nevim, jak presne popsat, ale projevuje se tak, ze v pripade vazby
belongsToMany dostavam jine vysledky (jakoby z jineho resultu asi?) a dojde
k vyhozeni vyjimky, ze dany sloupec xxx neni v radku definovan – vyhodi
metoda Result::getDataEntry.
Databazovy dotaz se pritom polozi spravne a i kdyz je jeho vysledkem napr.
0 radku, vnitrne se nejak podstrci existujici result z nektereho
z predchozich dotazu? Tezko se to vysvetluje, ale nasledujici vypis strucne
popisuje chybu.
<?php
// schematicky:
// prvni pouziti vazby - polozi se dotaz do tabulky event_user
echo count($event->eventUsers);
// druhe pouziti vazby - polozi se dotaz do tabulky event_garant
foreach ($event->garants as $eventGarant) {
echo $eventGarant->garant->name; // hodi onu vyjimku, ze sloupec garant_id neni definovan
}
// pri dumpnuti hodnoty v Result::getDataEntry vypise ve skutecnosti pole
// s hodnotami sloupcu z tabulky event_user
public function getDataEntry($id, $key)
{
...
dump($this->data[$id]);
...
}
?>
Napada me, jestli se nemuze jednat o chybu z me strany, resp. mapperu, nebo je tam schovany brouk?
před 5 lety
- Tharos
- Člen | 1042
Včera večer jsem špatně mergnul jeden konflikt a mám dojem, jestli jsi zrovna náhodou netrefil na problém, který tím vznikl :).
Mohl bys, prosím, vyzkoušet aktuální develop
teď
z rána?
před 5 lety
- JuniorJR
- Člen | 181
@Tharos: Posunul jsem se, uz to nehaze predchozi
chybu, ale presto se projevuje, byt na jinem miste.
Soucasna verze asi nebude uplne stabilni? Take mi to na nekterych mistech haze
chybu, ze neni nastavena EntityFactory a nebo v pripade, ze primarni klic je
entita, tak se poklada spatny dotaz do referencni tabulky. Je mozne, ze se neco
v tomto smeru zmenilo a ja budu muset adaptovat kod? Divne je, ze to nefunguje
asi jenom nekde, nedokazu s urcitosti rici, za jakych podminek.
Zkusim pouzit master.
Edit: Po pouziti master chyba byla ostranena, take jsem zjistil, ze chyba s entityfactory byla zpusobena tim, ze jsem mel u entity napsan getter, ktery vytvarel instanci referencni entity rucne, tudiz chyba nelhala :) Akorat ted nemohu vytvaret Result z pole (podporovana jsou pouze DibiRow).
Editoval JuniorJR (5. 6. 2014 12:39)
před 5 lety
- Tharos
- Člen | 1042
Dobře, že se to posunulo.
Stabilní by měla být verze v2.2.0 (je otagovaná). Dokonce koukám, že to testování entit bez databáze i včetně traverzování bude funkční už i v ní.
Pokud narazíš na nějaký bug v ní, otevři mi prosím issue…
Větev develop
je stabilní po naprostou většinu času. Dost
vzácně si tam vyrobím nějaký překlep nebo něco špatně mergnu (jako
třeba včera).
Koncepčně se nic nezměnilo, nějakých hlubokých zásahů bys měl být ušetřen.
Vyzkoušel bys prosím tu verzi v2.2.0 a dal bys mi vědět? Díky!
před 5 lety
- JuniorJR
- Člen | 181
Tharos: Diky za promptni reakci :)
Soude dle kodu a chybove hlasky jsou ale podporovany pro vytvareni resultu pouze
dibirow?
<?php
LeanMapper\Exception\InvalidArgumentException: Invalid type of data given, only DibiRow or array of DibiRow is supported at this moment.
in LeanMapper-master\LeanMapper\Result.php(96)
?>
před 5 lety
- Tharos
- Člen | 1042
JuniorJR napsal(a):
Tharos: Diky za promptni reakci :)
Soude dle kodu a chybove hlasky jsou ale podporovany pro vytvareni resultu pouze dibirow?<?php LeanMapper\Exception\InvalidArgumentException: Invalid type of data given, only DibiRow or array of DibiRow is supported at this moment. in LeanMapper-master\LeanMapper\Result.php(96) ?>
Jo, to jsem zavedl až po commitu, který ale, zdá se, něco trochu rozbil :).
Stačí při tom ručním vytváření resultů nahradit [...]
za new DibiRow([...])
. Co nevidět ale bude fungovat zase
i pouhé pole.
před 5 lety
- JuniorJR
- Člen | 181
Tharos: Jojo, zatim to nebudu hrotit a kdyztak pouziju DibiRow :) hlavni je, ze to funguje
před 5 lety
- Tharos
- Člen | 1042
No jo, fakt jsem měl v develop
větvi botu
až hanba. :)
Už by vše mělo být fixnuté i v develop
větvi. Pokud bys
měl chuť, můžeš to vyzkoušet. Samozřejmě v té větvi už funguje to
vytváření Result
z polí.
Díky moc za pomoc při lazení :).
před 5 lety
- JuniorJR
- Člen | 181
@Tharos: Tak update pomohl, ale stale jeste dostavam chybu pri pokusu persistovat entitu, ktera ma jako primarni klic entitu, hodi se tam spatny sloupec.
před 5 lety
- Tharos
- Člen | 1042
Přiznám se, že mě takový use case ještě asi nikdy nenapadl. :) To
v té stable verzi funguje? Jak to vypadá v mapperu v metodě
getPrimaryKey
?
Ideálně to můžeme vyřešit v issues na GitHubu.
Editoval Tharos (5. 6. 2014 14:10)
před 5 lety
- Jan Suchánek
- Backer | 403
@Tharos: Šlo by prosím přidat do LM i Extension, tak aby se jednodušeji přidával do projektu?
Editoval jenicek (5. 6. 2014 14:10)
před 5 lety
- JuniorJR
- Člen | 181
Pri pokusu pristoupit k hodnote entity se polozi spatny dotaz do referencni tabulky, ve stable verzi mi to fungovalo. Asi to souvisi s tim, ze je to zaroven primarni klic.
před 5 lety
- Tharos
- Člen | 1042
JuniorJR: Docela mě ta myšlenka entity coby primárního klíče zaujala :), a tak jsem upravil Lean Mapper tak, aby si s tím poradil. Bylo to vlastně docela snadné.
Nyní mohou entity vypadat třeba následovně a vše funguje podle očekávání (čtení, traverzování i perzistence):
/**
* @property int $id Used as PK in database
* @property AuthorDetail $authorDetail m:belongsToOne
* @property string $name
*/
class Author extends Entity
{
}
/**
* @property Author $author m:hasOne Used as PK in database
* @property AuthorContract $authorContract m:belongsToOne
* @property string $address
*/
class AuthorDetail extends Entity
{
}
/**
* @property AuthorDetail $authorDetail m:hasOne Used as PK in database
* @property string $number
*/
class AuthorContract extends Entity
{
}
Více podrobností je pak v tomhle testu. Za povšimnutí stojí to, že se tenhle „styl vazby“ může řetězit přes libovolné množství entit.
Je to k testování v develop
větvi, která už by zase měla
být stabilní. :)
Editoval Tharos (6. 6. 2014 1:30)
před 5 lety
- JuniorJR
- Člen | 181
@Tharos: Tak jsem vyzkousel aktualni dev verzi a vsechno, zda se, opet funguje. Neovlivnila tvoje posledni zmena i zpusob mazani entit?
Editoval JuniorJR (6. 6. 2014 22:13)
před 5 lety
- Tharos
- Člen | 1042
@JuniorJR: Ty jo, neovlivnila. :) Narazil jsi na nějakou změnu? S jakou verzí aktuální chování porovnáváš? Díky!
před 5 lety
- JuniorJR
- Člen | 181
@Tharos: Ok, zatim vse funguje a zkousel jsem stable (2.2 tusim) a dev.
před 5 lety
- medhi
- Bronze Partner | 189
Ahoj, co když mám tabulku user
, která zahrnuje různé typy
userů (student, teacher). Udělám tedy abstraktní entitu User
a
od ní další entity Student
a Teacher
. Jak by potom
měly vypadat repozitáře? StudentRepository
ani
TeacherRepository
to být nemůže, protože ty tabulky neexistují
a UserRepository
nedokáže vytvořit entitu,
protože Cannot instantiate abstract class Model\Entity\User.
Díky za pošťouchnutí!
před 5 lety
- castamir
- Člen | 631
@medhi: UserRepository
Persistence by měla fungovat v pohodě, ale vytváření entit popř. kolekcí entit si musíš upravit (musíš explicitně určit, kterou entitu chceš použít viz API createEntity resp. createEntities ).
před 5 lety
- Casper
- Člen | 253
@mehdi: Popisuješ single table inheritance, mrkni na testy. V mapperu však bývá kromě metody getEntityClass třeba definovat ještě metody getTable a getEntityField. Repozitáře tedy zůstanou standardní, o vše se ti postará mapper.
// table 'user' contains entities Company and Person
public function getTable($entityClass) {
$shortClassName = $this->trimNamespace($entityClass);
if ($shortClassName === 'Company' || $shortClassName === 'Person') {
return 'user';
}
return parent::getTable($entityClass);
}
public function getEntityField($table, $column) {
if($table === "company" || $table === "person") {
$table = "user";
$column = str_replace(array("company", "person"), "user", $column);
}
return parent::getEntityField($table, $column);
}
před 5 lety
- medhi
- Bronze Partner | 189
@Casper: Díky za vysvětlení, už mám svůj mapper. Měl bych ještě doplňují dotaz k propojovacím tabulkám, které s tím souvisí.
Mám používat jednu propojovací tabulku user__lesson nebo pro každtý typ usera extra tabulku
student__lesson a teacher__lesson?
Něco mi říká, že by to měl být user__lesson, ale co když pro různé typy userů potřebuju v propojovací tabulce různé parametry?
Díky moc
před 5 lety
- medhi
- Bronze Partner | 189
castamir napsal(a):
@medhi: UserRepository
Persistence by měla fungovat v pohodě, ale vytváření entit popř. kolekcí entit si musíš upravit (musíš explicitně určit, kterou entitu chceš použít viz API createEntity resp. createEntities ).
Takže jestli to chápu dobře, musím si vytvořit vlastní createEntity, ale bohužel tápu jak, můžeš mi nahodit prosím nějaký příklad?
Děkuji
před 5 lety
- castamir
- Člen | 631
Ne, jen zavoláš ty metody s jiným parametrem…
// UserRepository
/**
* find students
*/
public function findStudentsBy($conditions) {
$rows = $query->where($conditions)->fetchAll();
return $this->createEntities($rows, 'Student');
}
/**
* find teachers
*/
public function findTeachersBy($conditions) {
$rows = $query->where($conditions)->fetchAll();
return $this->createEntities($rows, 'Teacher');
}
před 5 lety
- medhi
- Bronze Partner | 189
Ale já ty metody zatím vlastně nikde nevolám, stará se o to Lean Mapper, pokud chci třeba vypsat všechny studenty školy. Mám entitu School:
// School Entinty
/**
* @property int $id
* @property string $name
* @property Student[] $students m:belongsToMany
*/
class School extends \LeanMapper\Entity
{
}
A pak vypisuji její studenty v presenteru:
$students = $this->school->students;
V tu chvíli Lean Mapper magicky vyhledá dle cizích klíčů všechny studenty, ti jsou však potomky té abstraktní třídy User, která má role student a teacher. Dostanu tedy hlášku
Cannot get value of property ‚students‘ in entity Model\Entity\School due to low-level failure: Inconsistency found: property ‚students‘ in entity Model\Entity\School is supposed to contain an instance of ‚Model\Entity\Student‘ (due to type hint), but mapper maps it to ‚Model\Entity\Teacher‘. Please fix getEntityClass method in mapper, property annotation or entities inheritance.
Moje getEntityClass
vypadá podle mě správně:
public function getEntityClass($table, Row $row = NULL)
{
if ($table === 'user' and $row !== NULL and $row->role !== NULL) {
if ($row->role == 'student') {
return 'Model\Entity\Student';
}
if ($row->role == 'teacher') {
return 'Model\Entity\Teacher';
}
}
return parent::getEntityClass($table, $row);
}
před 5 lety
- Tharos
- Člen | 1042
@medhi: Ahoj,
jak vypadá ta vazební tabulka? Je tedy nakonec jedna? Tipuji, že ano a
při traverzování na $students
se tedy děje následující:
- Z databáze se přes tu vazební tabulku vytáhnout všechny potřebné
řádky z tabulky
user
. - Pro každý řádek se pomocí mapperu určí, jestli je to
Student
neboTeacher
. - Vytvoří se z toho kolekce, která jde ven z té property.
No a tímto způsobem načteš mix studentů i učitelů, a proto ta
„kontrola úplně nahoře“ vyhodí výjimku. Takhle bys totiž musel mít
v té anotaci User[] $users
. Pokud bys chtěl jenom studenty, je
zapotřebí to traverzování přehnat přes filtr.
Potvrď mi, že je to tak (že máš jednu vazební tabulku s nějakým extra sloupcem), a já Ti klidně načrtnu řešení.
před 5 lety
- medhi
- Bronze Partner | 189
@Tharos: Ahoj,
Žádná vazební tabulka tam není, je to pouze 1:N (school:users), ale jinak to asi probíhá přesně tak jak píšeš. Výsledkem je mix. A protože hned první řádek není student, ale teacher – a já potřebuji studenty – hodí to výjimku.
Budu moc rád za náčrt řešení, jak to filtrovat. Ale nejdříve dotaz: Neměl by to automaticky filtrovat už Lean Mapper? Vždyť on ví, že chci pouze studenty (řekl jsem mu to v anotaci entity School), tak proč mi to nevyfiltruje sám a nevrátí pouze entitu, kterou chci?
Díky moc za skvělý produkt a přístup.
před 5 lety
- Tharos
- Člen | 1042
@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
nebo
Teacher
, 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 i
StudentRepository
a TeacherRepository
, ve kteréch se
při zavolání protected
metody createFluent
ten
filtr automaticky použije, takže už se o to taky nemusíš více starat.
Persistovat přes ty StudentRepository
a
TeacherRepository
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
sloupce type
(v mém ukázkovém případě). Asi bych si na to
přetížil metodu Repository::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. :)
Editoval Tharos (17. 6. 2014 16:14)
před 5 lety
- medhi
- Bronze Partner | 189
1. dotaz: Lze u LM zapnout profiler pro Tracy?
profiler: TRUE
v configu nic nezapne. Ostatní nastavení by mi
mělo vycházet ze sandboxu. Verze poslední.
2. dotaz: Lze mít vazbu 1:N, ale se spojovací tabulkou? Potřebuji si k té vazbě uložit nějaké informace.
3. dotaz: Jak k nově vytvořené instanci entity přiřadím kolekci jiných entit? Vyhazuje mi to výjimku, ač mám snad vše dobře nastaveno.
Entita Lesson:
/**
* @property Teacher[] $teachers m:hasMany
*/
Presenter:
$teachers = $this->users->findBy([
"id" => 1,
"role" => "teacher",
]);
$lesson->teachers = $teachers;
Vyhazuje: Unexpected value type given in property ‚teachers‘ in entity Model\Entity\Lesson, Model\Entity\Teacher expected, array given.
Díky
Editoval medhi (19. 6. 2014 18:32)
před 5 lety
- joe
- Člen | 250
Ahoj, chválím Lean Mapper, líbí se mi myšlenka i jeho zatím „snadné“ používání, teprve ho začínám začleňovat do projektu. Chtěl bych se ale zeptat, jakým způsobem řešíte napovídání v IDE, například ve spojení s Nette mám v presenteru:
$user = $this->userRepository->find(2);
$user-> ...
IDE neví, že v proměnné $user
je instance
App\Model\Entity\User
. Dá se tam nějak dostat? Případně i do
cyklu při použití findAll()
a kdyby to šlo i do Latte šablon,
tak to by bylo super :) – používám NetBeans.
před 5 lety
- Tharos
- Člen | 1042
@joe: Ahoj, v PhpStormu funguje následující:
/**
* @method User find($id)
* @method User[] findAll
*/
class UserRepository extends Repository
{
}
Bohuže nevím, jak jsou na tomhle v téhle věci NetBeans. :(
Editoval Tharos (22. 6. 2014 22:22)
před 5 lety
- joe
- Člen | 250
@Tharos:
Tak to je super :) hlásím podporu i v NetBeans, minimálně v aktuální vývojové verzi. Teď už mi nebrání nic v používání.
před 5 lety
- mirdič
- Člen | 41
Ahoj, rád bych se zeptal, jaký je best practice v následujícím případu:
Mám např. kategorie v eshopu a každá kategorie má několik produktů (1:N).
Nyní bych chtěl vypsat pouze ty kategorie a pouze ty produkty, kde je vyšší cena než 100,– Kč. Tzn. že se mi kategorie, kde budou všechny produkty s nižší cenou než 100,– nevypíšou a zároveň, u kategorie která má alespoň jeden produkt s cenou více jak 100,– se vypíšou pouze produkty, které tuto podmínku splňují.
Pokud si v repozitáři „Kategorie“ vytvořím metodu findHigherPrice($price)
public function findHigherPrice($price)
{
$query = $this->connection->select('category.*')
->from('category')
->join('product')
->on('product.category_id = category.id')
->where('product.price > %i', $price);
return $this->createEntities($query->fetchAll());
}
tak se mi vypíší pouze kategorie s alespoň jedním produktem se správnou cenou, ale zároveň se mi vypíší i produkty u dané kategorie, které podmínku nesplňují.
Jediné řešení, které mne napadá, je použití filtru u produktu při výpisu.
foreach($categoryRepository->findHigherPrice(100) as $category){
$category->getProducts(100);
}
Otázka tedy zní: Existuje jiný způsob, abych nemusel filtrovat na dvou místech?