tiny ‘n’ smart
database layer

Odkazy: dibi | API reference

Oznámení

Omlouváme se, provoz fóra byl ukončen

Lean Mapper – tenké ORM nad dibi

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í:

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í:

  1. Z databáze se přes tu vazební tabulku vytáhnout všechny potřebné řádky z tabulky user.
  2. Pro každý řádek se pomocí mapperu určí, jestli je to Student nebo Teacher.
  3. 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

Tharos
Člen | 1042

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?

Stránky: Prev 1 … 20 21 22 23 Next