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 6 lety

Šaman
Člen | 2275

O více mapperech jsem už přemýšlel, ale počkám na dokumentaci – možná už je lze použít.
Jde mi o to, aby se výjimky oproti konvenci psaly do samostatné třídy rozšiřující hlavní mapper. V jednom adresáři modelu by pak byla typicky třída entity (např. User), repozitář (UserRepository) a mapper (UserMapper). Repositář i mapper však často bývají prázdné, takže je otázka, zda by jejich existence byla nutná, nebo by se dala nějak na pozadí nahradit defautními třidami App/../Repository a App/../Mapper.

Výhoda by byla v tom, že při přidání nové entity s vlastními mapovacími pravidly by se přidal nový soubor místo zásahu do existujícího (hrozí rozkopání odladěného kódu a to, že jediný mapper přeroste pokud bude pravidel mnoho pro mnoho entit). Taky se pak budou pravidla ke konkrétní entoté lépe hledat, než v ifech velkého Mapperu.

před 6 lety

Tharos
Člen | 1042

@Šaman: To, co popisuješ, bych osobně řešil tak, že bych si vytvořil nějaká MapperExtension rozšíření. Ta bych pak do hlavního Mapperu registroval obdobně, jako se registrují například extenze do Nette konfigurátoru před sestavením DI kontejneru. Určitě by šlo udělat, aby se to odehrálo úplně automaticky: jakmile bys nějakou takovou extenzi zaregistroval jako službu do DI kontejneru, kontejner by ve vhodný okamžik mohl projít všechny MapperExtension, které obsahuje, a zaregistrovat je do hlavního Mapperu.

Jak jsem psal, Mapper se předává při traverzování mezi entitami a byl by neudržitelný hokej, kdyby jich bylo víc (těch Mapperů) a každý by tvrdil třeba něco trochu jiného. Je to jako kdybys pracoval s více DibiConnection zároveň, každé jsi měl připojené do jiné databáze a snažil se z toho celého uvařit nějaký konzistentní výsledek. (Lepší analogie mě nenapadla :–P.)

Editoval Tharos (29. 1. 2014 16:57)

před 6 lety

Filip111
Člen | 244

@Tharos:
Dynamický mapper…že mě to jen nenapadlo :)
Díky.

před 6 lety

peter.z
Člen | 37

Zdravim :)

Uz par dni sa hram s generovanim tabuliek v databaze pomocou entit z LeanMapperu. Objavil som pomerne silnu funkciu Doctrine – schemy (http://docs.doctrine-project.org/…ntation.html). Pomocou tohto je generovanie tabuliek naozaj hrackou :)

Tu je moj nastrel – https://gist.github.com/…mous/8782935

Samozrejma je aj implementacia vazieb, cudzich klucov, vlastne flagy (m:size, m:autoincrement, m:unique, m:format(date|time) pri DateTime).

Daju sa s tym urobit hotove zazraky, mozete si ukladat stare schemy a pri zmene entity vam to moze vygenerovat potrebne SQL na zmenu databazy. Tu je priklad – https://gist.github.com/…mous/8782990 :)

Uvitam akekolvek pripomienky a napady :)

před 6 lety

Tharos
Člen | 1042

Kloubok dolů! :)

Hraji si tady s tím a funguje to jako víno. Díky!

Připomínky

Využít téhle funkcionality z Doktríní DBAL je výborný nápad. Tím je vyřešeno nespočet věcí, které by jinak bylo zapotřebí řešit (znovu-vynalézat). Těší mě, jak ty nástroje souzní.

Já to určitě budu používat a budu Ti poskytovat připomínky a zpětnou vazbu, pokud narazím na nějakou nesrovnalost. Určitě budu preferovat Tvůj nástroj před dopsáním svého generátoru, který jsem někdy na podzim začal psát úplně od nuly a je na něm ještě práce jako na kostele.

Ve svých úvahách jsem přemýšlel, zda metainformace potřebné pouze pro generátor schéma (auto-increment, délky sloupců…) by nebylo vhodné mít mimo anotace v entitách. To proto, aby v entitách zbytečně nevznikalo příliš balastu. Dopředně přemýšlím i nad tím, že já bych ve výsledku chtěl psát jenom entity a generovat nejen schéma databáze, ale i automaticky sestavovat formuláře. No a k formulářům budou zapotřebí další speciální metadata a v entitě by toho ve výsledku mohlo být poměrně hodně. Je to ale věc osobních preferencí.

před 6 lety

peter.z
Člen | 37

Dakujem :)

Tiez som to chcel pisat vsetko rucne a odznova… ale potom som na jednom projekte zacal pouzivat schemy, kde som si vsetko rucne definoval (kazdy stlpec, vazbu) a ked som mal to iste pisat aj do entity, tak som si povedal ze tu musi byt aj ina cesta.

Osobne sa mi paci mat zapisane metadata priamo v entitach, nejako mi to tam proste pasuje. Je sice pravda, ze po case to moze nabobtat, ale to by muselo ist o uber-entity :)

Nad generovanim formularov som premyslal, no narazil som na jeden zasadny problem – ak by som chcel vytvorit HasOne a HasMany vazby, musim mat dostupne repository k danej vazbe. Napr. mam entitu Product a ta ma HasOne na Category. Otazka ale znie, ako sa mozem vo formulari dostat ku CategoryRepository alebo k niecomu podobnemu? Musela by tam byt nejaka RepositoryMap, kde by bola trieda entity a k nej patricke repository, aby som sa mohol k tym entitam vo formulari dostat… Otazkou je, ze ci je to spravny pristup :)

před 6 lety

Nobody.guy
Člen | 22

Koukám, že mě někdo o pár dnů předběhl :-) dobrá práce!

před 6 lety

batko
Člen | 219

Ahoj,

právě jsem začal používat tento výtečný nástroj…Jen mi zaboha nefunguje ukládání datetime.

<?php
$x->inserted = new \DateTime();
?>

persistuji a:

Unexpected value type given in property ‚inserted‘ in entity Model\Entity\Phrase, Model\Entity\DateTime expected, instance of DateTime given.

děkuji za radu

před 6 lety

Šaman
Člen | 2275

Však ti to píše, v čem je problém. Pravděpodobně jsi v definici sloupce v anotaci entity nedal před DateTime lomítko, nebo, lépe, nevyčlenil DateTime do use sekce. Takže to teď očekává instanci Model\Entity\DateTime a odmítá to DateTime.

před 6 lety

danik
Člen | 56

Ahoj vespolek,

tak nějak se rozkoukávám v Lean Mapperu a zatím se mi to moc líbí! Můj zatím jedinej „teologickej“ problém s LM je, že je to striktně databázová vrstva, což je podle mě podmnožina toho, co by měl model v tom nejširším pojetí umět.. nejsem nijak velkej odborník na ostatní ORM ani na návrhové vzory obecně, ale pocitově bych řekl, že má-li model zapouzdřovat obecně všechnu práci související s perzistentními daty a vztahy mezi nimi, patří mezi tyhle data třeba i některé obrázky (dokonce někdy zejména obrázky), nebo obecně vzato věci, které chceme spíš uložit do souboru. Pokud pomineme možnost ukládat obrázky do BLOB sloupců v databázi (uf, úplně mi vstávaj vlasy na zátylku), žádná striktně databázová modelová vrstva nám tuhle možnost nedá.

Takže se chci zeptat, jak tohle řešit konkrétně u Lean Mapperu? Rád bych bral LM jako tu modelovou vrstvu, obalovat ho ještě něčím zvenku mě neláká. Ale třeba konkrétní příklad obrázkové galerie: jednoduchá tabulka pictures (id INT, postedOn DATETIME, description TEXT) a někde odpovídající adresář pictures, ve kterém čekáme JPEGy pojmenované <id>.jpg. Až na ten soubor je to celkem pěkná entita.. ale jakmile začnu přemejšlet o tom, že ta entita vlastně popisuje soubor a nějaká jeho metadata, začnu mít divný otázky a nemít na ně odpověď:

  • Jak taková entita má vzniknout? Řekněme, že uploaduju novej obrázek. Vzniká entita. Moje představa je taková, že bych entitě asi měl předat nějakou cestu (třeba $upload->getTemporaryFile()) a entita (respektive její repository) by si před perzistováním měla uvědomit, že má tenhle soubor odcestovat na patřičné místo a cestu si aktualizovat. Dobře, to by nebyl takovej problém (pomocí eventů atp).
  • Zároveň ale – nejsou binární data obrázku tak trochu „náplní“ (smyslem) téhle entity – a neměly by tudíž bejt skrze ni nějak dostupný? nebo třeba instance Nette\Image? Protože jinak práci s binárními daty vyčleňuji ven z modelu (pro ten bude property filename prostě jen string bez hlubšího významu).. kdo pak obrázky zmenší? ořízne? obrátí vzhůru nohama? atd.

Jak prostě zařídit, aby mi tahle část doménové logiky (ha, termitus technikus!) nezmizela z modelu a nezačala se, nikým nezvaná, objevovat někde, kde ji nechci?

před 6 lety

Etch
Člen | 404

danik napsal(a):

Můj zatím jedinej „teologickej“ problém s LM je, že je to striktně databázová vrstva, což je podle mě podmnožina toho, co by měl model v tom nejširším pojetí umět.

Já teda nevím „školy nemaje“, ale neslouží ORM už z podstaty k mapování dat z RDBMS??

Pokud pomineme možnost ukládat obrázky do BLOB sloupců v databázi (uf, úplně mi vstávaj vlasy na zátylku), žádná striktně databázová modelová vrstva nám tuhle možnost nedá.

Přijde mi „nesprávné“ chápat samotné ORM jako „modelovou vrstvu“.

Takže se chci zeptat, jak tohle řešit konkrétně u Lean Mapperu? Rád bych bral LM jako tu modelovou vrstvu, obalovat ho ještě něčím zvenku mě neláká.

Co znamená „obalovat ho ještě něčím z venku“? Je vytvoření třeba nějaké FACADE pro tebe obalování?

  • Jak taková entita má vzniknout? Řekněme, že uploaduju novej obrázek. Vzniká entita. Moje představa je taková, že bych entitě asi měl předat nějakou cestu (třeba $upload->getTemporaryFile()) a entita (respektive její repository) by si před perzistováním měla uvědomit, že má tenhle soubor odcestovat na patřičné místo a cestu si aktualizovat. Dobře, to by nebyl takovej problém (pomocí eventů atp).
  • Zároveň ale – nejsou binární data obrázku tak trochu „náplní“ (smyslem) téhle entity – a neměly by tudíž bejt skrze ni nějak dostupný? nebo třeba instance Nette\Image? Protože jinak práci s binárními daty vyčleňuji ven z modelu (pro ten bude property filename prostě jen string bez hlubšího významu).. kdo pak obrázky zmenší? ořízne? obrátí vzhůru nohama? atd.

Jak prostě zařídit, aby mi tahle část doménové logiky (ha, termitus technikus!) nezmizela z modelu a nezačala se, nikým nezvaná, objevovat někde, kde ji nechci?

To zní, jako kdyby si naznačoval, že co ti „zmizí“ z ORM, ti zmizí z modelu. Možná že se mýlím, ale tento způsob chápání ORM jako „celé“ modelové vrstvy, mi přijde jako poněkud „divná“ myšlenka.

Editoval Etch (5. 2. 2014 23:27)

před 6 lety

danik
Člen | 56

Etch napsal(a):

Já teda nevím „školy nemaje“, ale neslouží ORM už z podstaty k mapování dat z RDBMS??

Já právě také školy nemaje čtu „Object Relational Mapping“ doslovně, tedy jako „vztahovou mapu objektů“ – v čemž mi „databáze“ nutně nerezonuje. Není snad obrázek ve fotogalerii objektem, který má nějaké vlastnosti a nějaké vazby na okolní svět?

Přijde mi „nesprávné“ chápat samotné ORM jako „modelovou vrstvu“.

K chápání ORM jako ucelené modelové vrstvy mě vede to, že už pracuje s těmi objekty, které mě zajímají – tj s entitami a jejich vztahy, nějak úplně objektově nevidím, kde by tam měla být další vrstva.

Co znamená „obalovat ho ještě něčím z venku“? Je vytvoření třeba nějaké FACADE pro tebe obalování?

Vytvoření fasády bezesporu obalováním je :-) minimálně ve smyslu „zapouzdření“. Může to samozřejmě mít svůj smysl, ale ten podle mě u facade patternu spočívá především v definování API nezávislého na spodní vrstvě fasády, případně ke spojení API a odpovídající funkcionality rozložené do více služeb / nástrojů v jeden, pokud to má smysl. Ale pořád se mi vnitřně ta entita „obrázek“ nechce rozpadnout na „metadata entitu“ a „binární data obrázku“ sjednocené pod jednou fasádou.. představ si stejnou situaci u článku: „článek“ jako takovej je nepochybně ten samotnej text, kterej je v databázi uloženej ve sloupci typu TEXT, všechno okolo (datum publikování, ID autora, …) jsou jen metadata. Měl by „článek“ bejt vlastně jen fasáda nad entitou „metadata článku“ a nějakou jinou položkou „článek“? To by nedávalo po žádný stránce smysl. Tak proč by to u obrázku (nebo obecně objektu, kterej nechceme ukládat do databáze) mělo bejt nutně jinak, když problém je jenom se samotnou „storage“ vrstvou?

To zní, jako kdyby si naznačoval, že co ti „zmizí“ z ORM, ti zmizí z modelu. Možná že se mýlím, ale tento způsob chápání ORM jako „celé“ modelové vrstvy, mi přijde jako poněkud „divná“ myšlenka.

No to právě viz výše. Když už celej ORM pracuje s entitama, což je pro mě osobně tak nějak to, s čím chci pracovat vně modelu, přijde mi nepřehledný a z pohledu objemu kódu / práce zbytečný obalovat každou jednotlivou entitu ještě nějakou fasádou (dělat to jen u jedný by nebylo konzistentní), abych zajistil funkci, která podle mě už tak nějak uvnitř tý stávající struktury má bejt..

Ale ať zas tak moc neteologizujem, nehádám se, že by Lean Mapper měl umět ukládat data kamkoliv a jakkoliv nebo že bych na něm obecně něco měnil, spíš mě jen zajímalo, jak se tohle podle konvencí Lean Mapperu správně řeší – tj. je nějaká cesta, jak Lean Mapper používat přímo jako modelovou vrstvu a pokud ano, kam a jak tedy patří zpracovávání takovéhohle typu dat? Nebo to prostě takhle být nemá a Lean Mapper má sloužit jen jako jedna ze „spodních vrstev“ Modelu s velkým M, kterej to má celý obalit?

A tady je další otázka z trochu jiného soudku: lokalizace. Ne, nekřičte, četl jsem tohle vlákno asi celé a vím, že se to řešilo. Mám na to řešení ale trochu větší nároky. Východisko je stejné: pro lokalizovanou entitu mám dvě tabulky, jednu s nepřekládaným obsahem a jednu navázanou přes cizí klíč s lokalizovanými variantami překládaného obsahu (tj. unique klíč nad entity_id, language_code). Jako vstupní parametr při nasosávání entity ale nemám jeden jazyk, nýbrž pole jazyků seřazených podle preferencí uživatele; výstupem má být entita, která tohle kritérium splňuje nejlíp, jak to jde.

Příklad: uživatel preferuje jazyky en, cs a de v tomhle pořadí. Článek má dostupné překlady pro cs, sk a ru. Vrácená entita tedy má být lokalizována do češtiny. Pokud by ovšem článek žádný z uživatelem preferovaných překladů neměl, měla by se vrátit entita lokalizovaná řekněme do první dostupné jazykové verze (takže třeba sk). Tohle umím jedním dotazem (je to trochu mysql magie, ale to mi pro můj use-case nevadí).

Jak mám ale s entitou pracovat v hypotetickém administračním rozhraní, kde admin v jednom formuláři může vytvářet překlady pro všechny webem podporované jazykové mutace? Formulář má část prvků pro nepřekládané property entity a pak má kontejnery s překladovými variantami. Najednou bych chtěl místo:

<?php
$entity->title = '..';
$entity->content = '..';
?>

použít něco jako:

<?php
foreach ($langs as $lang) {
    $entity->$lang->title = '...';
    $entity->$lang->content = '...';
}
?>

Ono by to teda asi nebylo takhle přímo na entitě, ale spíš v nějakém poli $entity->contents[$lang], ale jde o jinou věc – při čtení entity vůbec nemám žádné pole contents a místo něj mám property title, content …, naopak při zápisu nemám tyhle property a mám pole contents.. jak na to? :-)

Editoval danik (6. 2. 2014 3:14)

před 6 lety

Šaman
Člen | 2275

Jenomže tady dochází k drobnému zmatku v terminologii. To R v ORM neznačí relaci objektů (jako např. ER diagram), ale relační databázi. Jedná se skutečně o mapování objektů do tabulek (relací) a zpátky.
Když se v přednáškách o pětivrstvém modelu řeší mappery pro různá úložiště, tak to vlastně znamená naučit model používat společná rozhraní pro ORM a jiné mappery pro specifická úložiště (memcache, filesystem). Taky už jsem narazil na problémy, kdy mi v modelu existovaly entity které nepocházely z databáze, ale pokud používáš jen ORM (což LM je), tak je logické, že se ti je nepodaří podchytit.
Entita z ER diagramu totiž nutně neznamená databázová tabulka a tedy dědit třídy entit od ORM entit je zjednodušení, které nám sice často stačí, ale může nás i omezovat.

Ty chceš nějaký O-E Mapper, tedy mapování entit na objekty a zpátky. To je ale ještě o jednu abstrakci výše, než ORM. Pokud se ti ale všechny entity vejdou do databáze, tak se celý problém redukuje na ORM.

Editoval Šaman (6. 2. 2014 5:39)

před 6 lety

batko
Člen | 219

Pročítám toto forum a mám následující dotaz.

Jakým způsobem vyřešit následující problem:

Mám knihy a ty mají tagy. A rád bych dostal jen knihy, které tagy nemají.

Řeší se toto již v repository? Nebo to mohu „vyfiltrovat“ až po načtení entit? Čtu zde o filtrech, je to správné řešení?

Děkuji

před 6 lety

duke
Člen | 637

@batko Dle mého názoru by to mělo řešit repository, a sice buď speciální metodou nebo interpretací nějakého query objektu, tak aby se to filtrování řešilo už na úrovni databáze. Z hlediska databázového dotazu na to můžeš jít buď nějak takto:

SELECT * FROM book WHERE id IS NOT IN (SELECT book_id FROM book_tag)

… nebo takto (toto řešení bude nejspíš v MySql o něco rychlejší):

SELECT book.*, MIN(book_tag.tag_id) AS firstTag FROM book LEFT OUTER JOIN book_tag ON book.id=book_tag.book_id GROUP BY book.id HAVING firstTag IS NULL

Ty dotazy jsem netestoval, takže bez záruky…

před 6 lety

Tharos
Člen | 1042

@danik: ORM skutečně znamená objektově relační mapování.

Pokud bys chtěl mít data uložená nějak „hybridně“ (něco v relační databází, něco třeba v nějakém XML na disku, něco v nějakých souborech…), tak vše, co budeš potřebovat vyřešit, už za Tebe bohužel ORM nevyřeší. A nebude toho málo, hlavně bude pracné udržovat konzistenci dat (referenční integritu). Určitě se v takovém případě neobejdeš bez dalších vrstev nad nějakým ORM.

Nejen z výše uvedených důvodů se podobné problémy často řeší právě tak, že třeba i Tebou uvedené obrázky jsou „mapovány“ v relační databázi a na disku (nebo v nějaké CDN) se už jen udržují související binární data. Je to velmi výhodné a vcelku standardní řešení.

V Lean Mapperu není problém mít entitu Image, která bude mít například položku uri. Jelikož entity v Lean Mapperu mohou být „chytré“, není problém si do takové entity injektnout službu, jejíž zdopovědností bude formulování správného uri.

S ukládáním a následným managementem těch dat mimo relační databázi Ti ale Lean Mapper příliš nepomůže, Lean Mapper je prostě ORM. Může jít o velmi pestrou činnost (ukládání někam na disk vs. upload do CDN…) a je právě na programátorovi, aby tohle v rámci modelu elegantně vyřešil. :) Lean Mapper Ti jen vychází vstříc vcelku flexibilními repositáři, do kterých se lze snadno „nahookovoat“, anebo systémem událostí.

před 6 lety

batko
Člen | 219

Začal jsem celkem úspěšně používat LM a jsem moc spokojen.

Nyní řeším následující.

Mám entitu Phrase a do property next a prev bych potřeboval dostat entitu Phrase nacházející se v DB před a za aktuální Phrase. Bohužel se mi do těchto property nedaří entity dostat.

<?php
/**
 * @property int $id
 * @property string $text
 * @property DateTime $inserted
 * @property User $user m:hasOne
 * @property Tag[] $tags m:hasMany(:::tag)
 * @property Phrase $next
 * @property Phrase $prev
 */
?>

před 6 lety

Tharos
Člen | 1042

@batko: Luxusní požadavek, to tu ještě nebylo :).

Nad SQLite databází, která je součástí Lean Mapperových testů, bych to osobně řešil následovně:

/**
 * @property int $id
 * @property string $name
 * @property string $pubdate
 * @property-read Book|null $previous m:hasOne(previous)
 * @property-read Book|null $next m:hasOne(next)
 */
class Book extends \LeanMapper\Entity
{
}

class BookRepository extends \LeanMapper\Repository
{

    /**
     * @return Book[]
     */
    public function findAll()
    {
        return $this->createEntities(
            $this->createFluent()->fetchAll()
        );
    }

}
class Mapper extends DefaultMapper
{

    protected $defaultEntityNamespace = null;

    /** @var Connection */
    private $connection;


    /**
     * @param Connection $connection
     */
    public function __construct(Connection $connection)
    {
        $this->connection = $connection;
    }

    /**
     * @param string $entityClass
     * @param Caller $caller
     * @return mixed
     */
    public function getImplicitFilters($entityClass, Caller $caller = null)
    {
        if ($entityClass === 'Book') {
            return function (Fluent $fluent) {
                $fluent->select(
                    $this->connection->select('prevbook.id')->from('[book] [prevbook]')
                        ->where('[prevbook.id] < [book.id]')
                        ->orderBy('id')->desc()->limit(1)
                )->as('previous')
                ->select(
                    $this->connection->select('nextbook.id')->from('[book] [nextbook]')
                        ->where('[nextbook.id] > [book.id]')
                        ->orderBy('id')->limit(1)
                )->as('next');
            };
        }
        return parent::getImplicitFilters($entityClass, $caller);
    }

}

Připravíme-li si poté následující testovací skript:

$mapper = new Mapper($connection);

$bookRepository = new BookRepository($connection, $mapper, $entityFactory);

foreach ($bookRepository->findAll() as $book) {
    echo "[$book->id] $book->name\n";
    if (isset($book->previous)) {
        echo str_repeat(' ', 5) . "[{$book->previous->id}] {$book->previous->name}\n";
    }
    if (isset($book->next)) {
        echo str_repeat(' ', 5) . "[{$book->next->id}] {$book->next->name}\n";
    }
}

Dostaneme takovýto výstup:

[1] The Pragmatic Programmer
     [2] The Art of Computer Programming
[2] The Art of Computer Programming
     [1] The Pragmatic Programmer
     [3] Refactoring: Improving the Design of Existing Code
[3] Refactoring: Improving the Design of Existing Code
     [2] The Art of Computer Programming
     [4] Introduction to Algorithms
[4] Introduction to Algorithms
     [3] Refactoring: Improving the Design of Existing Code
     [5] UML Distilled
[5] UML Distilled
     [4] Introduction to Algorithms

S tím, že se vykonají následující dotazy:

SELECT [book].* , (SELECT [prevbook].[id] FROM [book] [prevbook] WHERE [prevbook].[id] < [book].[id] ORDER BY [id] DESC LIMIT 1) AS [previous] , (SELECT [nextbook].[id] FROM [book] [nextbook] WHERE [nextbook].[id] > [book].[id] ORDER BY [id] LIMIT 1) AS [next] FROM [book]
SELECT [book].* , (SELECT [prevbook].[id] FROM [book] [prevbook] WHERE [prevbook].[id] < [book].[id] ORDER BY [id] DESC LIMIT 1) AS [previous] , (SELECT [nextbook].[id] FROM [book] [nextbook] WHERE [nextbook].[id] > [book].[id] ORDER BY [id] LIMIT 1) AS [next] FROM [book] WHERE [book].[id] IN (1, 2, 3, 4)
SELECT [book].* , (SELECT [prevbook].[id] FROM [book] [prevbook] WHERE [prevbook].[id] < [book].[id] ORDER BY [id] DESC LIMIT 1) AS [previous] , (SELECT [nextbook].[id] FROM [book] [nextbook] WHERE [nextbook].[id] > [book].[id] ORDER BY [id] LIMIT 1) AS [next] FROM [book] WHERE [book].[id] IN (2, 3, 4, 5)

Věřím, že mé myšlenkové pochody jsou z té ukázky dobře patrné.

A teď ještě takové malé zamyšlení… Ty dotazy nejsou úplný skvost a také je nehezké, že to není lazy. ID předchozí a následující knihy se zjišťuje bez ohledu na to, zda bude zapotřebí. Jenomže udělat to úplně lazy asi není principiálně dost dobře možné… Nabízí se možnost ta ID dopředu nezjišťovat, ale pak by je bylo nutné nějak rekonstruovat při traverzování. To by ale asi nebylo vzhledem k „NotORM“ stylu dotazování možné…

Východiskem je samozřejmě udržování těch ID „předvypočtených“ přímo v databázi. Pokud by se výše uvedené řešení ukázalo jako problematické z výkonnostního hlediska, nebál bych se toho. Integritu lze s použitím Lean Mapperu zaručit (i když ony ty dotazy budou rychlé, ke všemu se použijí indexy).

Edit: Samozřejmě by také šlo mít položky $previous a $next až v nějakém potomkovi Book (třeba TraversableBook?) a v případech, kdy se dopředu ví, že toto traverzování nebude zapotřebí, by se mohlo pracovat s entitami Book. Je to ale už trochu překombinované…

Mimochodem netvrdím, že neexistuje nějaké celkově lepší řešení. :)

Editoval Tharos (7. 2. 2014 23:57)

před 6 lety

Ripper
Člen | 56

Zdravím,

mohl by mi někdo poradit, jak efektivně zaregistrovat filtr? Zkoušel jsem to v Repozitáři (hlavním), ale tam to hlásí chybu, že už je filtr jednou zaregistrovaný (což chápu). Kde ho tedy registrovat?

Díky.

před 6 lety

Tharos
Člen | 1042

@Ripper: Ahoj, já se s tím moc nepářu a filtry registruji v bootstrap.php… Netvrdím ale, že je to ideální.

Lze je registrovat všude možně: v nějaké samostatné, k tomu určené třídě, v nějakém LeanMapperExtension kompileru… poslední zmíněná varianta by se mi líbila asi nejvíce, ale sám takové rozšíření napsané nemám. :–P

před 6 lety

Tharos
Člen | 1042

Tharos napsal(a):

Mimochodem netvrdím, že neexistuje nějaké celkově lepší řešení. :)

Tak nakonec existuje. :) Lze na to jít velmi výhodně „z druhé strany“:

/**
 * @property int $id
 * @property string $name
 * @property string $pubdate
 * @property-read Book|null $previous m:belongsToOne(*next) m:useMethods
 * @property-read Book|null $next m:belongsToOne(*previous) m:useMethods
 */
class Book extends Entity
{

    public function getPrevious()
    {
        return $this->getValueByPropertyWithRelationship('previous', new Filtering(function (Fluent $fluent) {
            $fluent->select(
                    $fluent->createSelect('nextbook.id')->from('[book] [nextbook]')
                        ->where('[nextbook.id] > [book.id]')
                        ->orderBy('id')->limit(1)
                )->as('next');
        }));
    }

    public function getNext()
    {
        return $this->getValueByPropertyWithRelationship('next', new Filtering(function (Fluent $fluent) {
            $fluent->select(
                    $fluent->createSelect('prevbook.id')->from('[book] [prevbook]')
                        ->where('[prevbook.id] < [book.id]')
                        ->orderBy('id')->desc()->limit(1)
                )->as('previous');
        }));
    }

}

class BookRepository extends \LeanMapper\Repository
{

    public function findAll()
    {
        return $this->createEntities(
            $this->createFluent()->fetchAll()
        );
    }

}

No a to je vše. Při následujícím testu:

$bookRepository = new BookRepository($connection, $mapper, $entityFactory);

foreach ($bookRepository->findAll() as $book) {
    echo "[$book->id] $book->name\n";
    if (isset($book->previous)) {
        echo str_repeat(' ', 5) . "[{$book->previous->id}] {$book->previous->name}\n";
    }
    if (isset($book->next)) {
        echo str_repeat(' ', 5) . "[{$book->next->id}] {$book->next->name}\n";
    }
}

Vypíše se:

[1] The Pragmatic Programmer
     [2] The Art of Computer Programming
[2] The Art of Computer Programming
     [1] The Pragmatic Programmer
     [3] Refactoring: Improving the Design of Existing Code
[3] Refactoring: Improving the Design of Existing Code
     [2] The Art of Computer Programming
     [4] Introduction to Algorithms
[4] Introduction to Algorithms
     [3] Refactoring: Improving the Design of Existing Code
     [5] UML Distilled
[5] UML Distilled
     [4] Introduction to Algorithms

A vykonají se následující dotazy:

SELECT [book].* FROM [book]
SELECT [book].* , (SELECT [nextbook].[id] FROM [book] [nextbook] WHERE [nextbook].[id] > [book].[id] ORDER BY [id] LIMIT 1) AS [next] FROM [book] WHERE [next] IN (1, 2, 3, 4, 5)
SELECT [book].* , (SELECT [prevbook].[id] FROM [book] [prevbook] WHERE [prevbook].[id] < [book].[id] ORDER BY [id] DESC LIMIT 1) AS [previous] FROM [book] WHERE [previous] IN (1, 2, 3, 4, 5)

S tím, že ten druhý a třetí je volán lazy až v okamžiku, kdy se k relevantním položkám přistupuje.


Funguje to ale zatím pouze v této branši. Musel jsem kvůli tomu dodělat podporu pro zápis aliasů ve vazebních sloupcích. Mergnu to, až k tomu ještě dopíšu pár testů…

Co mám těmi aliasy na mysli? Pokud v příznaku vyjadřujícím vazbu zapíšu nějaký vazební sloupechvězdičkou na začátku (například m:belongsToOne(*previous)), Lean Mapper nesestaví dotaz ... WHERE table.column IN ..., nýbrž ... WHERE column IN. Lze se takto výhodně odkázat na aliasy, které vzniknou v SELECT klauzuli. Využití je hezky patrné z té mé ukázky.

Editoval Tharos (10. 2. 2014 14:19)

před 5 lety

batko
Člen | 219

@Tharos

ta druhá varianta je o poznání přívětivější :-) děkuji moc, jdu to vyzkoušet

před 5 lety

Petr Daňa
Člen | 106

Ahoj, delší dobu jsem LeanMapper neviděl, teď jsem se k němu vrátil a nevím si rady s jednou situací. Mám tabulky Faultbook a User, k nim mám „vazební“ UserUnreadFaultbook. Vazební je v uvozovkách, protože se jedná spíše o příznakovou tabulku, která obsahuje pro konkrétního uživatele seznam Faultbook.id, které ještě neviděl. Ta vazební tabulka má dva sloupce „user_id“ a „faultbook_id“.

Snažil jsem se přidat do entity pro Faultbook vazbu „hasOne“, která by mi v podstatě řekla, jestli záznam existuje nebo ne, zkoušel jsem hasMany, ale bez výsledku. Vždycky vyhodil výjimku. Přikládám definice vazeb a k nim vždy výpis výjimek:

@property-read UserUnreadFaultbook $isUnread m:hasOne(id:user_unread_faultbook) m:filter(user)
Unknown column 'user_unread_faultbook.id' in 'where clause'
SELECT `user_unread_faultbook`.*
FROM `user_unread_faultbook`
WHERE `user_unread_faultbook`.`id` IN (4, 3, 2, 1) AND user_id = 1
@property-read UserUnreadFaultbook $isUnread m:hasOne(faultbook_id:user_unread_faultbook) m:filter(user)
Undefined index: faultbook_id
(v Result->extractIds())

Podobně na tom byly pokusy s hasMany vazbou.

Jak to teda udělat správně?

před 5 lety

tomas.lang
Člen | 54

@Tharos: chtěl jsem se zeptat jak to vypadá se samostatným LM fórem, případně dá se s tím nějak pomoct? Přeci jen dotazy na LM začínají dosti zasahovat do Nette fóra a asi to není úplně žádoucí…

před 5 lety

Pavel Macháň
Člen | 285

@Tharos poslal sem PR – https://github.com/…pper/pull/26 Narazil jsem na problém při registraci filtrů

Pokud není možné registrovat filtr při vytváření connection z důvodu kruhové závislosti je potřeba filter registrovat až za běhu (třeba v repository) a je nutné mít možnost zkontrolovat jeho existenci.

Příklad: potřebujeme zaregistrovat překladové filtry, které využívají LanguageFacade (má za úkol držet seznam jazyků, který načteme při inicializaci a zněhož dále získáváme pro překlady požadovaný jazyk). LanguageFacade (LanguageRepository) má závislost na Connection (kruhová závislost Filter > Connection > Filter). Překladové filtry zaregistruji v Abstract TranslationRepository, který využíváme pro překladové repositáře. TranslationRepository se může použít za 1 request vícekrát a connection by vyhazoval při registerFilter – InvalidArgumentException. Pomocí této vyjímky by se to dalo ošetřit, ale registrace filtru tuto vyjímku vyhazuje při více scénářích.

Editoval EIFEL (17. 2. 2014 12:04)

před 5 lety

batko
Člen | 219

Ahoj,

chtěl bych do selectu formuláře v nette přidat prvky. Jak z pole entit udělám pole $key ⇒ $val existuje na to nějaká metoda nebo je třeba pro iterovat pole entit? děkuji

před 5 lety

Michal III
Člen | 84

@batko:

Ahoj,

odkážu Tě sem, kde se i o pár příspěvků dál nachází rozřešení.

Jenom upozorňuji, že nyní se protected metoda createCollection nachází ve tříde DefaultEntityFactory, kterou sis nejspíše podědil.

před 5 lety

batko
Člen | 219

Michal III napsal(a):

@batko:

Ahoj,

odkážu Tě sem, kde se i o pár příspěvků dál nachází rozřešení.

Jenom upozorňuji, že nyní se protected metoda createCollection nachází ve tříde DefaultEntityFactory, kterou sis nejspíše podědil.

Super dík moc.

Ještě malý dotaz.

Odešlu formulář a mám v něm multiselect který mi vrací pole hodnot. Ale já bych potřeboval, aby vracel pole objektů aby se to mohlo rovnou persistovat.

Mám entitu článek a ta má entity gallery[]. A ty gallery vyberu v multiselectu a odešlu.

Editoval batko (18. 2. 2014 21:50)

před 5 lety

batko
Člen | 219

Začínám být lehce nešťastný :-)

co je špatně na tomto kodu

<?php
    $page = new \Model\Entity\Page();
    $page->inserted = new \DateTime();

//chyba je někde zde
    $gallery = $this->galleryRepository->find(1);
    $page->gallery[] = $gallery;

    $page->user = $this->userRepository->find(1);
    $this->pageRepository->persist($page);
?>

vyhazuje

Missing entity factory in Model\Entity\Page.

před 5 lety

Tharos
Člen | 1042

Ahoj, omlouvám se za trochu zpožděnou reakci, mám teď trochu méně prostoru…

Protože už to tady nechci moc SPAMovat, chci všem nabídnout support na Skype v.kohout nebo na Jabberu tharos@jabber.cz. Do té doby, než bude mít Lean Mapper své vlastní fórum.

Díky!

před 5 lety

Šaman
Člen | 2275

Nebuď měkkej, tu tisícovku dáme :)
Myslím, že Davidovi spíš vadí, když se zakládají nová témata a LM se pak rozlézá po fóru. Když bude všechno v tomhle jednom, tak je fajn, že v případě hledání něčeho ohledně LM to asi bude někde tu. A je šance, že někdo odpoví rychleji, než budeš mít čas ty.
A vím, že to musí byt děsná pruda, něco jako sepisování textu k diplomce, ale – jak to vypadá s dokumentací?
Kdyz ji uděláš na způsob wiki, tak ti buď můžeme zkusit pomoct (i když realita nikdy není tak růžová), nebo třeba hlasovat která kapitola je aktuálně nejdůležitější. Trochu interagovat, ať máš větší motivaci a nepřipadá ti, že si ji píšeš pro sebe…

před 5 lety

castamir
Člen | 631

Zveřejnil jsem rozšíření LM
Jde o přidání Query objectu, vlastního mapperu zohledňující namespace Entit/Repozitářů v submodulech, repozitář pracující s Query objektem a překládajícím názvy property na sloupce databáze.

Readme s popisem všech fičur přidám večer, nejpozději zítra…

Upozorňuji ale, že nepoužívám nejnovější (nezdokumentované) úpravy LM (používám verzi 2.0.1), takže negarantuju, že to pojede na novějších. Pokud budu vědět, jak to zprovoznit pro aktuální verzi LM, tak udělám druhou větev, kde ty změny zapracuju…

před 5 lety

Tharos
Člen | 1042

@castamir: Vypadá to moc hezky, díky za zveřejnění! Sešlo se mi teď nějak moc práce, ale jakmile budu mít trochu volněji, s Tvým rozšířením se rozhodně blíže seznámím.

@Šaman: Tak tisícovku asi jo :). Psaní docky je opravdu děsná pruda. Wiki na GitHubu je zapnutá. Právě už mě někdo prosil, jestli bych ji zapnul, a tak jsem ji zapnul. Čímž to samozřejmě skončilo. :)

Zajímalo by mě, jak by se nejlépe dala komunitně napsat dokumentace. Matně si vzpomínám, že třeba u Nette ta komunita v případě docky vcelku selhala, ale už nevím, co byla primární příčina. Řekněme, že bych nadhodil strukturu dokumentace a ke každé kapitole vypsal body, co by tak ideálně měla obsahovat, aby bylo vše pokryté. Najde se někdo, kdo by to pak pro Lean Mapper napsal? :) Já jsem skeptik, ale tak pro jistotu se ptám. :) Co mohu nabídnout je mentoring, přiznání autorství a taky to, že nebudu moc prudit (i kdyby to nebylo napsáno perfektně, jakékoliv zpracování je lepší než žádné).

před 5 lety

Michal III
Člen | 84

@Tharos: Já bych určitě rád něčím do dokumentace přispěl a doufám, že to není jenom planý slib. Mít k dispozici hlavní témata a nějaké body bude moc fajn → rád plním body, pokud zrovna ten bod není „napsat dokumentaci k LM“ :-).

před 5 lety

castamir
Člen | 631

@Tharos s tou wiki to byla narážka na mě, co? :D
Demo apku, kde můžu ukázat spoustu (ale zdaleka ne vše), mám nachystanou. Řeším tam v podstatě už jen kravinky. Ale nebude to tutoriál vyloženě na LeanMapper, ale na Nette a různé doplňky a LM je tam jako podpora pro práci s db.

před 5 lety

tomas.lang
Člen | 54

@Tharos: pokud by existoval nějaký seznam bodů, také rád k dokumentaci přispěji… :-)

před 5 lety

castamir
Člen | 631

pár věcí, které mě napadají a které stojí za zdokumentování

  • celých changelog (probrat co je ještě aktální a co už ne)
  • injektování služeb do entit (taková ta věc, které vůbec neholduju ale budiž)
  • filtry (důkladně popsat)
  • popsat jak na ‚ruční‘ vazby (např jak získat z vazební tabulky article_tag 10 nejčastějších tagů a jejich počet, aniž bych kvůli tomu musel speciálně vytvářet repozitář)

před 5 lety

Pavel Macháň
Člen | 285

@Tharos: Plánuješ do LeanMapperu přidat Extension pro Nette? (dalo by se to tam jako bridge stylem co ma dibi). Zatím používám svůj nedodělaný (pokud bych ho dodělal tak bych klidně poslal PR do LM repa), který umožnuje více připojení a nastavení mapperu a filtrů pomocí configu.
Momentálně zápis configu vypadá takto (zatím bez přidávání filtrů)

Pouze jedno připojení: @connection

leanmapper:
    mapper: App\Mapper
    entityFactory: App\EntityFactory
    connection:
        driver: mysql
        host: localhost
        username: USERNAME
        password: PASSWORD
        database: DATABASE
        lazy: TRUE

Více připojení: @connection.forum, @connection.content

leanmapper:
    mapper: App\Mapper
    # pokud nenastavím mapper nebo entityFactory tak se pouzije defaultní z LM
    connections:
        forum:
            driver: mysql
            host: localhost
            username: USERNAME
            password: PASSWORD
            database: DATABASE
            lazy: TRUE
            autowired: no
            inject: no
            # pokud nastavím mapper nebo entityFactory v určitém připojení, tak se použije místo globálního
            entityFactory: App\EntityFactory

        content:
            driver: mysql
            host: localhost
            username: USERNAME
            password: PASSWORD
            database: DATABASE
            lazy: TRUE

Editoval EIFEL (27. 2. 2014 14:38)

před 5 lety

castamir
Člen | 631

Nápad dobrej, akorát bych úplně škrtnul tu verzi s pouze jediným připojením. Jestli to odsadím o trochu víc nebo míň a přidám jeden řád je už jedno, ale hlavně to nikoho nebude plést kvůli jednomu „s“

před 5 lety

Tharos
Člen | 1042

Ad dokumentace) Tak super! Díky moc za podporu. Já se tedy ASAP pokusím připravit infrastrukturu. Mám vcelku pokročilou představu, jak by to celé mohlo fungovat, a vystačili bychom si jenom s Gitem (GitHubem). Mám teď opravdu hodně nabito, ale snad se k tomu přes víkend dostanu… Ty cca tři hoďky bych prostě měl najít.

Ad extenze) Já jsem samozřejmě rád za každou, jaká vznikne. :) Pokud by měla existovat nějaká „oficiální“, je k diskuzi, co by měla řešit. Jednou už jsem tu o tom někde psal. Diskuzi o tom už bych ale nechal na samostatné fórum… Tady by to bylo hezké s 1000. příspěvkem tak nějak opustit. ;)

před 5 lety

duke
Člen | 637

Používám následující entitu:

/**
 * @property int       $id
 * @property \DateTime $start m:passThru(sanitizeDate)
 * @property \DateTime $end m:passThru(sanitizeDate)
 */
class Exclusion extends \LeanMapper\Entity
{
    protected function sanitizeDate($date)
    {
        if (!$date instanceof \DateTime) {
            $date = new \DateTime('0000-01-01');
        }
        return $date;
    }
}

Řeším tím problém, kdy mi Dibi v případě nulové hodnoty sloupce datetime v databázi vrací místo objektu typu \DateTime řetězec (což by se bez použití passThru projevilo chybou v očekávaném typu). Dá se toto řešit nějak globálněji, abych to nemusel psát ke všem entitám?

před 5 lety

Šaman
Člen | 2275

A zkoušel jsi tohle?

<?php
/**
 * @property int              $id
 * @property \DateTime|string $start
 * @property \DateTime|string $end
 */
?>

Editoval Šaman (4. 3. 2014 19:02)

před 5 lety

duke
Člen | 637

@Šaman Zkoušel. Způsobí to chybu:
Invalid property definition given: @property \DateTime|string $start in entity …

Nicméně, i kdyby to fungovalo, tak by to ne zcela řešilo můj problém.
PassThru řešení funguje; jen hledám jeho obecnou variantu, abych nemusel psát passThru u všech sloupců typu \Datetime u všech entit.

před 5 lety

Pavel Macháň
Člen | 285

duke napsal(a):

@Šaman Zkoušel. Způsobí to chybu:
Invalid property definition given: @property \DateTime|string $start in entity …

Nicméně, i kdyby to fungovalo, tak by to ne zcela řešilo můj problém.
PassThru řešení funguje; jen hledám jeho obecnou variantu, abych nemusel psát passThru u všech sloupců typu \Datetime u všech entit.

Musel by si upravit repository a tam kde se kontroluje typ mit vyjimku pro \DateTime aby když je null tak vrátit $date = new \DateTime(‚0000–01–01‘) (i když nechápu proč nevracíš NULL… přijde mě to víc logické než 0000–01–01 datum)

před 5 lety

duke
Člen | 637

@EIFEL Ten typ se kontroluje v Entity::__get a to je metoda, kterou není tak snadné přetížit (volá private metody a vyhazuje poměrně obecné výjimky, takže by se musela parsovat message výjimky, atp.). Toto by mělo jít řešit nějak elegantněji.

NULL bych mohl vracet také, avšak někdy je rozdíl mezi nevyplněným datumem a nulovým datumem (použitým např. jako minimální datum).
Striktně bych měl asi použít:

$date = new \DateTime($date);

… což by pro nulové datum vedlo na datum 30.11.-0001 (což odpovídá 0000–00–00, tj. 1 měsíc a 1 den před 1.1. roku 0).

Editoval duke (5. 3. 2014 13:18)

před 5 lety

Tharos
Člen | 1042

@duke: Vůbec jsem netušil, že Dibi v takové situaci (datum „0000–00–00 00:00:00“) vrací string. Nikdy jsem na to nenarazil…

Tohle je souhra dvou „specialit“. Databáze umožní uložit uvedený datum, který je IMHO nesmyslný, a dibi z toho udělá string. Babo raď, co z toho udělat na úrovni entity…

Od stolu mě nenapadá žádné elegantní řešení aplikovatelné v současné verzi. Popravdě řečeno já bych v tomhle případě měl v databázi datum jako nullable a zbytek se pokusil vyřešit nějakou výchozí hodnotou a třeba příznakem, zda už datum byl nebo nebyl nastaven…

Jak by sis představoval optimální řešení? Co třeba nějaký výchozí passThru podle typu položky?

Editoval Tharos (5. 3. 2014 14:52)

před 5 lety

duke
Člen | 637

@Tharos Problém jsi shrnul přesně. Nicméně mít nullable datetime jen kvůli tomu, že se tam může dostat tato speciální hodnota mi jako moc elegantní řešení nepřipadá.

Jinak já na tento problém narazil díky chybě ve validaci hodnot před uložením, takže se mi do databázové tabulky dostala tato speciální hodnota 0000–00–00 a to pak vedlo na chybu v LeanMapperu při následném načítání entit.

Implicitní passThru by toto mohl řešit a asi by našel i uplatnění jinde (když můžeme mít implicitní filtry, proč ne implicitní passThru…). Otázkou je, zda by neměl LeanMapper tento nesoulad dvou specialit řešit automaticky (ať už výchozí implementací tohoto implicitního passThru, či jinak).

před 5 lety

jannek19
Člen | 47

Ahoj, měl bych dotaz ohledně @property-read, potřeboval bych v entitě uchovávat položku created, kterou nechci, aby kdokoli z kódu měnil, ale zároveň tu hodnotu created potřebuji sám před uložením nějak nastavit – jak nejlíp na to? Nastavovat to nějak na začátku persist() metody v repozitáři (jak?), nebo přetížit nějakou metodu v entitě a nastavovat to v ní? Jak něco takového obvykle řešíte? Díky moc.

PS. pokud se to tu někde už řešilo, tak se omlouvám, prohledával jsem celé forum, ale odpověď na toto jsem nikde nenašel.

před 5 lety

Pavel Macháň
Člen | 285

jannek19 napsal(a):

Ahoj, měl bych dotaz ohledně @property-read, potřeboval bych v entitě uchovávat položku created, kterou nechci, aby kdokoli z kódu měnil, ale zároveň tu hodnotu created potřebuji sám před uložením nějak nastavit – jak nejlíp na to? Nastavovat to nějak na začátku persist() metody v repozitáři (jak?), nebo přetížit nějakou metodu v entitě a nastavovat to v ní? Jak něco takového obvykle řešíte? Díky moc.

PS. pokud se to tu někde už řešilo, tak se omlouvám, prohledával jsem celé forum, ale odpověď na toto jsem nikde nenašel.

Řeším to triggerem

CREATE TRIGGER `table_BINS` BEFORE INSERT ON `table` FOR EACH ROW
SET new.created = NOW();

před 5 lety

jannek19
Člen | 47

Vytvořit trigger by taky byla možnost, ale i když to má svou logiku (je to záležitost databáze a ta by se měla o správnost dat postarat), tak se mi vytváření triggeru kvůli nastavení jedné hodnoty příliš nelíbí, navíc tento sloupec mám skoro u všech tabulek, takže řešit to triggery se mi příliš nechce.

Aktuálně jsem to vyřešil přetížením metody getModifiedRowData() v entitě:

public function getModifiedRowData()
{
    $data = parent::getModifiedRowData();
    if ($this->isDetached()) {
        $data['created'] = new \DateTime;
    }
    return $data;
}

Snad v tom není skrytý nějaký zádrhel.

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