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

Casper
Člen | 253

@castamir:

  1. Zrovna tohle tu teď řeším, jestli jsi mé příspěvky četl. Pokud bych se dal na zmíněnou první metodu, tenhle problém kompletně odpadá. Nicméně dá se říci, že ano, vytváří se hůře.
  2. Tohle je obecně problém ORM typu Lean Mapper. Tak je prostě nastavená jeho architektura, že neperzistovaná entita se nemůže dožadovat vztahových entit. Stejně tak to teď bude s těmi závislostmi, pokud nebudeš vše vytvářet přes faktory.
  3. Tohle není ale problém vývojáře. To, že někdo nechápe principy architektury kterou potažmo využívá je jeho problém. Rozhodně je podle mě blbost si říct: „tuhle skvělou feature raději nebudu vyvíjet, protože neznalí programátoři s tím napáchají akorát škody“. To bych podle tvé logiky mohl říct, že DI je taky špatná. Vždyť přeci neznalý programátor pak může kombinovat DI se statickým voláním a to už je teprve peklo, kdyby to po něm někdo přebral a domníval se, že předáním závislostí je vše vyřešené.

Editoval Casper (13. 11. 2013 14:46)

před 6 lety

Michal III
Člen | 84

Ještě bych se chtěl zeptat, jak nejlépe přistoupit k vysokoúrovňovým datům tak, aby se vazební property nenahrazovaly za instance Tříd, ale zůstaly jen jejich syrové hodnoty. Typicky při naplňování formuláře pro úpravu.

Tedy například entita Book by měla property author odkazující na Author a já bych chtěl naplnit formulář pro úpravu knihy tak, aby na pozici author nebyla instance Author, nýbrž jen jeho id, které pak můžu dát jako defaultní hodnotu selectu?

Nebo takovéhle věci řešíte úplně nějak jinak?

před 6 lety

Casper
Člen | 253

@Michal III:

Tharos má ve svém skeletonu na githubu metodu pro naplnění formuláře v BaseEntity. Osobně se mi více líbí si napsat podobnou, ale obecnější metodu getPreviewData, která jen potřebná data získá. Tu pak můžu využít nejen pro fomuláře.

před 6 lety

Michal III
Člen | 84

@Casper: Děkuji.

před 6 lety

Tharos
Člen | 1042

@Casper, @Michal III: Mně se ta má metoda v BaseEntity vůbec nelíbí, sám bych ji určitě nikde neventiloval. :)

Nejde ani tak o její vnitřnosti, ty nejsou špatné, ale hlavně o její umístění – entita by neměla o žádných formulářích nic vědět. To mé řešení ve skeletonu je prostě dirty ad-hoc solution.

V jednom projektu mám rozpracované řešení, které mi přijde mnohem lepší – potřebnou logiku zapouzdřuje formulář a já pak jenom volám něco jako $form->setDefaults($entity). Akorát to taky ještě není v takovém stavu, abych to mohl s čistým svědomím pustit ven… :(

Editoval Tharos (14. 11. 2013 2:00)

před 6 lety

Jan Suchánek
Backer | 403

@Tharos: Ahoj, mimo Translating a skeletonu máš ještě najaké další příklady řešení ze kterých se dá učit, nebo spíš alespoň nad nimi bádat? Translating je moc pěkný.

před 6 lety

Tharos
Člen | 1042

@castamir:

krom toho, že to narušuje single responsibility principle

Vezmeme-li v úvahu tuto definici SRP:

A class or module should have one, and only one, reason to change.

pak neplatí, že existence metody Entity\Order::notifyAdmin je automaticky porušením SRP. Záleží na tom, jestli ta entita tu práci sama dělá, anebo ji jen deleguje. Moc doporučuji tento článek od René Steina, se kterým osobně naprosto souhlasím.

1. mnohem hůř vytváří vytváří instance (rozumněj, stále bude možné vytvořit instanci pouze přes new a kdo to pak ošetří, když ta instance nebude mít připojené patřičné služby?)

To je prostě cena dependency injection…

to, že si pár lidí nejspíš bude schopno pohlídat, co za závislosti s entitou propojí, aby z toho nevznikla totální prasečina je jedna věc, druhá ale je, že spoustu dalších vyloženě naočujete tímto použitím a oni si to pak budou uplatňovat za všech okolností a ten guláš jim v tom vznikne…

Tohle je přesně důvod, proč mi anémický model u data-centric aplikací nepřijde jako vůbec špatný. Je prostě blbuvzdorný, neklade vysoké nároky na programátora. Jasně separuje data a práci s nimi, takže programátor hned ví „co kam šoupnout“. Nevyužívá se naplno možností OOP, ale, mezi námi… u většiny webových aplikací je to víceméně fuk.

Rich-domain model je rozhodně těžší navrhnout a pokud to programátor udělá špatně, výsledek bude pravděpodobně hůře použitelný a udržovatelný než anémický model řešící stejné zadání.

před 6 lety

Tharos
Člen | 1042

@jenicek: Bohužel nemám… Zatím v nějaké hezké ucelené podobě neexistuje nic jiného, než části kódu, které se tady povalují na fóru…

před 6 lety

Tharos
Člen | 1042

Ahoj,

Lean Mapper má od včerejšího dne jednu zajímavou novou funkcionalitu, které říkám implicitní filtry.


Vezměme si například toto zadání. Lze to velmi přímočaře vyřešit pomocí filtru, který do SQL dotazu doplní restrikci deleted = 0. Problémem ale je, že takový filtr musíme explicitně použít všude tam, kde se daná entita načítá – v repositáři a ve všech entitách, které se na tu zmíněnou odkazují (mají na ní vazbu). Jinak bychom se někde mohli například dotraverzovat i k odstraněným entitám. Obrovskou nevýhodou je, že zapomenout někde ten filtr uvést je velmi snadné.

No a tohle řeší ony implicitní filtry. Stačí v mapperu říct, že při načítání té či oné entity se vždy mají použít vybrané filtry. Slouží k tomu nová metoda IMapper::getImplicitFilters, která vrací buďto pole s názvy těch filtrů, anebo instanci ImplicitFilters která kromě těch názvů obsahuje i parametry, které se jim mají předat. Fungují podobně jako pátý parametr konstruktoru třídy Filtering.

Ukažme si, jak lze pomocí tohoto konceptu vyřešit výše odkázané zadání:

/**
 * @property int $id
 *
 * @property Category $category m:hasOne
 *
 * @property string $title
 * @property DateTime $published
 * @property string $text
 */
class Article extends \LeanMapper\Entity
{
}

/**
 * @property int $id
 *
 * @property Article[] $articles m:belongsToMany
 *
 * @property string $name
 */
class Category extends \LeanMapper\Entity
{
}

class Repository extends \LeanMapper\Repository
{

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

}

class ArticleRepository extends Repository
{

    protected function deleteFromDatabase($arg)
    {
        $primaryKey = $this->mapper->getPrimaryKey($this->getTable());
        $idField = $this->mapper->getEntityField($this->getTable(), $primaryKey);

        $id = ($arg instanceof Entity) ? $arg->$idField : $arg;
        $this->connection->query(
            'UPDATE %n SET [deleted] = 1 WHERE %n = ?', $this->getTable(), $primaryKey, $id
        );
    }

}

class CategoryRepository extends Repository
{
}

class Mapper extends TestMapper
{

    public function getImplicitFilters($entityClass, $caller = null)
    {
        if ($entityClass === 'Article') {
            return new ImplicitFilters(array('living'), array(
                'living' => array($this->getTable($entityClass))
            ));
        }
        return parent::getImplicitFilters($entityClass, $caller);
    }

}

////////////////////

$mapper = new Mapper;

$connection->registerFilter('living', function ($statement, $table) {
    $statement->where('%n.[deleted] = 0', $table);
});

To je v podstatě vše. Všimněte si následujících záležitostí v kódu:

1. Repository obsahuje novou protected metodu createFluent, jejíž implementace je myslím snadno pochopitelná. Jednak zase o něco usnadňuje život (nemusíte datlovat ani obligátní $this->connection->select(...)->from($this->getTable())->fetchAll(), ale hlavně hlídá, aby byly ihned aplikovány všechny potřebné implicitní filtry, takže na ně nelze zapomenout.

2. Při definici vazby z entity Category na entitu Article není nutné uvádět žádné filtry, protože díky implicitnímu filtru je už zaručené, že se vždy načtou pouze nesmazané články. Takže jsme si zase ušetřili nějaké psaní a také jsme eliminovali riziko, že nějaký filtr uvést zapomeneme.

3. Repositář ArticleRepository velmi jednoduše namísto trvalého odstraňování záznamů jen nastavuje deleted = 1.


Zavoláme-li pak v aplikaci $articleRepository->findAll(), položí se dotaz:

SELECT * FROM `article` WHERE `article`.`deleted` = 0

A provedeme-li pak v aplikaci následující aktivitu:

foreach ($categoryRepository->findAll() as $category) {
    echo $category->name, "\n";
    foreach ($category->articles as $article) {
        echo "  ", $article->title, "\n";
    }

}

Položí se dotazy:

SELECT * FROM `category`
SELECT `article`.* FROM `article` WHERE `article`.`category_id` IN (1, 2) AND `article`.`deleted` = 0

Všimněte si, že se korektně načítají pouze neodstraněné články a nemusíme na to téměř nijak myslet.


Celý tento koncept má velmi široké uplatnění. Například ten systém překladů, který jsem zde na fórum už kdysi dával, by se mohl ještě zjednodušit: ty filtry m:filter(translate) by se vůbec nemusely uvádět, čímž by zároveň odpadlo riziko, že takový filtr někde uvést zapomenete. Asi ten příklad ještě zaktualizuji. :)

Enjoy!

Editoval Tharos (15. 11. 2013 15:00)

před 6 lety

Pavel Macháň
Člen | 285

Kde registrujete filtry pro leanmapper v nette?

@Tharos: implicitní filtry vypadají dobře :)

Editoval EIFEL (15. 11. 2013 13:44)

před 6 lety

Michal III
Člen | 84

@Tharos: Paráda! Děkuji.

Editoval Michal III (15. 11. 2013 12:45)

před 6 lety

llook
Člen | 412

Nad soft deletem jsem se už něco nanadával… Mám k této implementaci dva dotazy:

  • Dají se ty implicitní filtry přebít explicitními? Pokud bych někde použil takovýto soft delete, je nějak možné přimět LeanMapper, aby vracel i ty smazané položky?
  • Používají se implicitní filtry u vazby hasOne? Pak by bylo by možné smazat položku, na které jiná položka závisí a tím narušit integritu dat.

před 6 lety

Tharos
Člen | 1042

@llook: Samozřejmě, že lze takové filtry „přebíjet“. Ony se kombinují s těmi explicitně uvedenými. Platí, že se nejprve volají ty implicitní a pak ty explicitní, ale s tím, že pokud je uveden v explicitních filtrech (třeba v příznaku m:filter) nějaký filtr, který je obsažen i v implicitních, tak se v těch implicitních ignoruje a volá se až později (v rámci zpracování těch explicitních). Tak lze efektivně ovlivnit pořadí volání filtrů a také lze v tom příznaku explicitně přetížit „fixní parametry“, které se filtru předají.

Ono se to fakt kostrbatě popisuje, v dokumentaci tohle asi budu muset polopaticky ukázat na nějakých příkladech… Ale mělo by to být velmi pružné.

Pokud bys chtěl implicitně ten filtr living používat, ale zároveň bys chtěl mít možnost vypisovat i smazané záznamy, je hned několik možností, jak to vyřešit. Já bych asi nepohrdl následujícím řešení (i když by záleželo na kontextu):

Upravuji výše uvedený příklad.

const EXCLUDE_DELETED = 0; // tyhle mrchy bych určitě nenechal v globálním prostoru…
const INCLUDE_DELETED = 1;

$connection->registerFilter('living', function ($statement, $table, $mode = EXCLUDE_DELETED) {
    if ($mode === EXCLUDE_DELETED) {
        $statement->where('%n.[deleted] = 0', $table);
    }
});
// kdesi v relevantním repository

public function findAllIncludingDeleted()
{
    return $this->createEntities(
        $this->createFluent(INCLUDE_DELETED)->fetchAll() // zde lze předat filtrům dynamický parametr tak, jako při volání Entity::getXyz($arg1, $arg2, ...)
    );
}
foreach ($articleRepository->findAllIncludingDeleted() as $article) {
    echo $article->title, "\n";
}
// SELECT `article`.* FROM `article`
foreach ($categoryRepository->findAll() as $category) {
    echo $category->name, "\n";
    foreach ($category->getArticles(INCLUDE_DELETED) as $article) {
        echo "  ", $article->title, "\n";
    }
}
// SELECT `category`.* FROM `category`
// SELECT `article`.* FROM `article` WHERE `article`.`category_id` IN (...)

Samozřejmě by bylo možné i nadefinovat přímo getter getArticlesIncludingDeleted bez potřeby předávání parametru atp. Ale jak říkám, ono by to šlo řešit hned více způsoby. Pokud by Ti například createFluent(INCLUDE_DELETED) přišlo příliš magické, šlo by tu část nahradit ručním vytvořením dotazu.


Co se té hasOne vazby týče, při traverzování přes ní se implicitní filtry samozřejmě aplikují. Jak přesně bys chtěl smazat položku, na které je nějaká jiná závislá? Máš na mysli situaci, kdy bys měl třeba načtený článek, který je navázaný na smazaného autora, a k tomu autorovi traverzoval? Lean Mapper jej z databáze nenačte a vrátí pro tu property hodnotu null (musí tedy být typu Author|null, ale to myslím dává smysl a je žádoucí).

Anebo jsi měl na mysli ještě něco jiného? :)

Editoval Tharos (15. 11. 2013 22:44)

před 6 lety

Tharos
Člen | 1042

Casper napsal(a):

Předpokládám tedy, že máš nějaké hezčí řešení :) Nebo máš prostě ten můj kód v initEvents?

Já ho mám opravdu v initEvents. Popravdě jsem se s tím moc nedělal, možnosti neonu jsem v téhle věci zatím nezkoumal…

před 6 lety

Michal III
Člen | 84

@Tharos: Myslím, že u toho traverzovaní přes vazvu hasOne je mnohdy právě žádoucí, aby se vrátil i soft smazaný autor – tedy situace, kdy bychom „living“ autory používali jako autory, se kterými můžeme dál pracovat (přidávat jim knihy), ale zároveň bychom pro knihy se smazanými autory stále chtěli autora vypisovat. (Tohle je zrovna trochu nešikovný příklad, ale představme si tedy něco podobného s mobilními operátory a tarify. Tarify bychom časem chtěli odstranit z nabídky, přičemž by ale ještě existovali lidé, kteří by ho měli aktivovaný…

před 6 lety

llook
Člen | 412

Když mažeš položku, na které je jiná položka závislá, tak se musíš nejdřív té závislosti nějak zbavit.

Například smazání uživatele na fóru – měl bys smazat i všechny jeho příspěvky, nebo je převést pod nějaký anonymní účet. Když to zapomeneš ošetřit, tak ti databáze nedovolí toho uživatele smazat. Ale soft delete klidně provést můžeš a potom každá stránka, kde je nějaký příspěvek od smazaného uživatele, bude házet server error.

Soft delete nese podobná rizika, jako SET foreign_key_checks = 0;.

před 6 lety

Tharos
Člen | 1042

@Michal III, @llook: Jasně, už to chápu. :) V podstatě jde o to, jak nasimulovat to, co za nás u standardního delete běžně řeší cizí klíče: SET NULL, RESTRICT, CASCADE

Troufám si tvrdit, že to lze v Lean Mapperu vcelku přímočaře vyřešit. To, jak lze na vyžádání načíst i záznamy označené jako smazané, jsem nastínil. Pokud jde o zachování konzistence při označování záznamů jako smazaných, šlo by postupovat následovně:

  1. Navěsil bych si handler na událost AFTER_DELETE (pro entitu, která se reálně nemaže).
  2. V tom handleru bych řešil ony závislé entity. Ty bych našel třeba pomocí RobotLoaderu – naindexoval bych si všechny entity a za použití EntityReflection bych vyfiltroval ty, které potenciálně mají na mazanou entitu vazbu.
  3. Načetl bych ty, které tu vazbu reálně mají (podle ID mazané entity), a podle potřeby bych se s tou vazbou vypořádal.

Vznikl by tak plně automatizovaný systém bez potřeby dodatečné konfigurace po přidání nových entit nebo vazeb mezi nimi.

před 6 lety

Tharos
Člen | 1042

Jak jsem naznačil, aktualizoval jsem ukázku s překlady tak, aby naplno využívala nejnovějších schopností Lean Mapperu, zejména implicitních filtrů.

Řekl bych, že se celá věc zase zjednodušila. Máme-li takovouto implementaci getImplicitFilters:

public function getImplicitFilters($entityClass, $caller = null)
{
    if (is_subclass_of($entityClass, 'Model\Entity\TranslatableEntity')) {
        if ($caller instanceof Entity) {
            return array('translateFromEntity');
        } else {
            return new ImplicitFilters(array('translate'), array(
                'translate' => array($this->getTable($entityClass)),
            ));
        }
    }
    return parent::getImplicitFilters($entityClass, $caller);
}

Naše entity pak mohou vypadat následovně:

namespace Model\Entity;

use DateTime;

/**
 * @property string $id
 */
class Lang extends \LeanMapper\Entity
{
}

/**
 * @property Lang $lang m:hasOne m:translatable
 */
abstract class TranslatableEntity extends \LeanMapper\Entity
{
}

/**
 * @property int $id
 * @property Content[] $contents m:belongsToMany
 *
 * @property DateTime $created
 * @property bool $active = true
 * @property string|null $name m:translatable
 * @property string|null $description m:translatable
 */
class Page extends TranslatableEntity
{
}

/**
 * @property int $id
 * @property Page $page m:hasOne
 *
 * @property string $code
 * @property string|null $content m:translateble
 */
class Content extends TranslatableEntity
{
}

Všimněte si, že úplně zmizely příznaky m:filter – filtry pro přijoinování překladů už není vůbec zapotřebí explicitně uvádět.

Použití v aplikaci zůstává nezměněné:

$csLang = $langRepository->find('cs');
$enLang = $langRepository->find('en');

foreach ($pageRepository->findAll($csLang) as $page) {
    echo $page->name; // vypíše český název stránky
    foreach ($page->contents as $content) {
        echo $content->content; // vypíše český obsah stránky (jazyk převzat z entity $page)
    }
}

foreach ($pageRepository->findAll($csLang) as $page) {
    echo $page->name; // vypíše český název stránky
    foreach ($page->getContents($enLang) as $content) {
        echo $content->content; // vypíše anglický obsah stránky (jazyk explicitně uveden při volání getContents)
    }
}

Editoval Tharos (18. 11. 2013 2:34)

před 6 lety

Pavel Macháň
Člen | 285

Tharos napsal(a):

Jak jsem naznačil, aktualizoval jsem ukázku s překlady tak, aby naplno využívala nejnovějších schopností Lean Mapperu, zejména implicitních filtrů.
\--

@Tharos Super :) zrovna nedávno sem se do těch překladů pustil a todle ušetří další čas :)
btw kdy plánuješ vydat další Releases?

Editoval EIFEL (18. 11. 2013 15:48)

před 6 lety

Michal III
Člen | 84

@Tharos: Možná došlo k nedorozumění v mém příspěvku, ale mně by opravdu šlo o to (a také by se mi to zrovna hodilo), aby se implicitní filtry aplikovaly jen na repozitáře a jen určité druhy vazeb při traverzování – tedy vyjma hasOne, u které bych chtěl, aby filtry použity nebyly a nebylo mi navráceno NULL, nýbrž soft smazaná položka. Lze tohoto nějak docílit?

před 6 lety

llook
Člen | 412

Michal III napsal(a):

@Tharos: Možná došlo k nedorozumění v mém příspěvku, ale mně by opravdu šlo o to (a také by se mi to zrovna hodilo), aby se implicitní filtry aplikovaly jen na repozitáře a jen určité druhy vazeb při traverzování – tedy vyjma hasOne, u které bych chtěl, aby filtry použity nebyly a nebylo mi navráceno NULL, nýbrž soft smazaná položka. Lze tohoto nějak docílit?

Tohle chceš u filtrů, které nějak filtrují výsledek. Ale taky můžou být filtry, které třeba přidávají vypočítané hodnoty, nebo joinují další tabulku a takové filtry chceš aplikovat vždycky.

Vidím dvě možnosti – buďto ty filtrovací implicitní filtry budeš u každé hasOne vazby přebíjet explicitním filtrem, nebo naopak ty filtrovací filtry nebudou implicitní, ale explicitně uvedené u každé vazby, na kterou se mají aplikovat.

před 6 lety

Michal III
Člen | 84

llook napsal(a):

Michal III napsal(a):

@Tharos: Možná došlo k nedorozumění v mém příspěvku, ale mně by opravdu šlo o to (a také by se mi to zrovna hodilo), aby se implicitní filtry aplikovaly jen na repozitáře a jen určité druhy vazeb při traverzování – tedy vyjma hasOne, u které bych chtěl, aby filtry použity nebyly a nebylo mi navráceno NULL, nýbrž soft smazaná položka. Lze tohoto nějak docílit?

Tohle chceš u filtrů, které nějak filtrují výsledek. Ale taky můžou být filtry, které třeba přidávají vypočítané hodnoty, nebo joinují další tabulku a takové filtry chceš aplikovat vždycky.

Vidím dvě možnosti – buďto ty filtrovací implicitní filtry budeš u každé hasOne vazby přebíjet explicitním filtrem, nebo naopak ty filtrovací filtry nebudou implicitní, ale explicitně uvedené u každé vazby, na kterou se mají aplikovat.

Asi ano. Ještě mě napadla možnost, že by funkce getImplicitFilters dostávala více informací, aby se to dalo vyřešit přímo v mapperu, pokud by to tedy bylo možné a žádoucí. Tolik jsem se do jádra fungování této věci neponořoval.

před 6 lety

Tharos
Člen | 1042

@EIFEL: Z featur byly ty implicitní filtry asi tím posledním, co jsem ve verzi 2.1 ještě vyloženě chtěl mít. Takže cca do konce týdne otevřu release větev. Finální verze 2.1 by tedy měla spatřit světa velmi brzy.

Editoval Tharos (18. 11. 2013 21:26)

před 6 lety

Tharos
Člen | 1042

@Michal III, @llook: Llook to popsal úplně přesně.

Už nyní z toho lze vybruslit ven – explicitním uváděním filtrů, vlastními gettery/settery… Ale faktem je, že to nejsou kdo ví jak pohodlné způsoby.

IMapper::getImplicitFilters dostává jako druhý parametr ($caller) instanci čehosi, co se chystá načíst pro cílovou entitu ($entityClass) data z databáze. Nyní jde buďto o instanci Repository, anebo o instanci Entity.

To má svůj význam – pokud je tím volajícím entita, lze se spolehnout na to, že Entity i Reflection\Property budou filtru k dispozici (samozřejmě pokud je filtr zaregistrován s patřičným autowiring schématem). Využívá se toho například v té mé ukázce s překlady – pokud je původcem entita, lze jazyk přebírat z ní, pokud je původcem repositář, musí se jazyk uvést explicitně.

Nabízí se tedy jedno možné řešení, a to jest, že by „caller“ byl zapouzdřen do instance následující třídy:

namespace LeanMapper;

/**
 * @author Vojtěch Kohout
 */
class Caller
{

    /** @var mixed */
    private $instance;

    /** @var mixed */
    private $complement;


    /**
     * @param mixed $instance
     * @param mixed|null $complement
     */
    public function __construct($instance, $complement = null)
    {
        $this->instance = $instance;
        $this->complement = $complement;
    }

    /**
     * @return mixed
     */
    public function getInstance()
    {
        return $this->instance;
    }

    /**
     * @return mixed|null
     */
    public function getComplement()
    {
        return $this->complement;
    }

    /**
     * @return bool
     */
    public function hasComplement()
    {
        return $this->complement !== null;
    }

}

S tím, že pokud by byla „caller“ entita, oním „doplňkem“ (complement) by byla Reflection\Property, přes kterou se traverzuje. U repositáře by ten doplněk nebyl využit.

Pak by šlo velmi jednoduše docílit přesně toho, co Michal III chce – implicitní filtr vybírající pouze nesmazané záznamy by se mohl používat všude, kromě hasOne vazeb.


Co si myslíte o takovém řešení? Já si nejsem jist s jednou věcí. Ono to dává do ruky poměrně hodně možností – ono totiž s pomocí tohoto (a pořádné dávky ifů) by někdo mohl úplně všechny filtry (rozumějte i „explicitní“) nadefinovat přes mapper, což bych viděl jako bad practice a nerad bych k tomu někoho vybízel.

Editoval Tharos (19. 11. 2013 9:21)

před 6 lety

Pavel Macháň
Člen | 285

Tharos napsal(a):

Jak jsem naznačil, aktualizoval jsem ukázku s překlady tak, aby naplno využívala nejnovějších schopností Lean Mapperu, zejména implicitních filtrů.

Řekl bych, že se celá věc zase zjednodušila. Máme-li takovouto implementaci getImplicitFilters:

public function getImplicitFilters($entityClass, $caller = null)
{
  if (is_subclass_of($entityClass, 'Model\Entity\TranslatableEntity')) {
      if ($caller instanceof Entity) {
          return array('translateFromEntity');
      } else {
          return new ImplicitFilters(array('translate'), array(
              'translate' => array($this->getTable($entityClass)),
          ));
      }
  }
  return parent::getImplicitFilters($entityClass, $caller);
}

Nebylo by lepší používat defaultEntityNamespace než mít natvrdo nastaven namespace entit?
Beru zpět :)

if (is_subclass_of($entityClass, $this->defaultEntityNamespace . '\TranslatableEntity')) {...}

Tharos napsal(a):

@EIFEL: Z featur byly ty implicitní filtry asi tím posledním, co jsem ve verzi 2.1 ještě vyloženě chtěl mít. Takže cca do konce týdne otevřu release větev. Finální verze 2.1 by tedy měla spatřit světa velmi brzy.

Super :)

Editoval EIFEL (20. 11. 2013 0:14)

před 6 lety

Tharos
Člen | 1042

Ahoj,

tak jsem v boční větvi vyzkoušel rozšířit ten parametr $caller v IMapper::getImplicitFilters – nyní se tedy předává instance třídy LeanMapper\Caller.

Abych ukázal, k čemu je to tedy dobré… Nahraďme mapper z této mé ukázky tímto:

class Mapper extends TestMapper
{

    public function getImplicitFilters($entityClass, Caller $caller = null)
    {
        if ($entityClass === 'Article' and ($caller->isRepository() or !($caller->getComplement()->getRelationship() instanceof BelongsToMany))) {
            return new ImplicitFilters(array('living'), array(
                'living' => array($this->getTable($entityClass))
            ));
        }
        return parent::getImplicitFilters($entityClass, $caller);
    }

}

Výsledek bude takový, že při načítání entit Article se vždy (ať už z repositáře nebo nějaké související entity) aplikuje filtr living, s výjimkou přístupu přes property nesoucí belongs to many vazbu. Celé jsem to přizpůsobil té své již existující ukázce, analogicky by samozřejmě šel vyřešit požadavek Michala III odlišující vazbu has one.

Pokud proti tomuto řešení nikdo nic nenamítne /anebo to někdo nevymyslí lépe :)/, mám v plánu to brzy mergnout.

Editoval Tharos (20. 11. 2013 13:49)

před 6 lety

Michal III
Člen | 84

@Tharos: +1

před 6 lety

Pavel Macháň
Člen | 285

Hrál sem si s překlady a narazil sem na jednu věc co se mě u toho moc nelíbí(tedle pidi SQL dotaz bude hned proveden, ale vnitřně mě štve, že se volá i když nemusí) a to neustálé dotazování databáze na jazyk (SELECT language.* FROM language WHERE language.id_language IN (xzy)) pro každý překlad.

V presenteru si načtu po startu všechny jazyky pomocí LanguageFacade a uložím si je do privátní proměné této fasády. Pokud chci najít jazyk dle jeho kodu (cs,en, atd) sahnu do fasády a najdu si je v již načtených jazycích (je tam možnost si vyžádat i aktuální data z DB). Pokud persistuju data byl bych rád kdyby bylo možné použít data z LanguageFacade aby odpadlo dotazování, které už by nebylo potřeba.

Jde mě o to hlavně u jazyků, přeci jen, jak často přidáváte nebo mažete jazyky za životní cyklus presenteru na frontendu.

Lze toto chování změnit bez zásahu do Leanmapperu?

před 6 lety

Tharos
Člen | 1042

@EIFEL: Samozřejmě, že je to možné. :) Není to ani nic složitého.

Upravil jsem Ti tu ukázku tak, aby v po prvotním načtení všech jazyků nedošlo už k žádnému dalšímu dotazu do tabulky s jazykyhttp://www.leanmapper.com/…lations2.zip

Pointou je, že ty musíš do všech míst, kde jsou zapotřebí, předat kolekci načtených jazyků (v praxi se jedná pouze o Translator a o potomky TranslatingRepository). Já ji v té upravené ukázce předávám jako asociativní pole, ale v reálu bych si z toho samozřejmě udělal službu s nějakým jasně daným API a ve výsledku by veškeré předávání vyřešil auto wiring.

Mimochodem s tímto řešením ty jazyky klidně ani nemusí být v databázi, mohou existovat třeba jen v nějakém konfiguračním souboru.

před 6 lety

Pavel Macháň
Člen | 285

Tharos napsal(a):

@EIFEL: Samozřejmě, že je to možné. :) Není to ani nic složitého.

Upravil jsem Ti tu ukázku tak, aby v po prvotním načtení všech jazyků nedošlo už k žádnému dalšímu dotazu do tabulky s jazykyhttp://www.leanmapper.com/…lations2.zip

Pointou je, že ty musíš do všech míst, kde jsou zapotřebí, předat kolekci načtených jazyků (v praxi se jedná pouze o Translator a o potomky TranslatingRepository). Já ji v té upravené ukázce předávám jako asociativní pole, ale v reálu bych si z toho samozřejmě udělal službu s nějakým jasně daným API a ve výsledku by veškeré předávání vyřešil auto wiring.

Mimochodem s tímto řešením ty jazyky klidně ani nemusí být v databázi, mohou existovat třeba jen v nějakém konfiguračním souboru.

@Tharos Super, díky moc :). Mě nějak nedocvaklo, že je ten dotaz kladen v Translatoru (a kde jinde že :D )

Editoval EIFEL (21. 11. 2013 16:08)

před 6 lety

mrtnzlml
Člen | 143

Ahoj, na stránkách jsem se dočetl, že „Název tabulky lze upřesnit pomocí anotace @table a třídu entity lze upřesnit pomocí anotace @entity (obě anotace patří nad třídu repositáře).“ a protože nedodržuji konvence, tak bych konkrétně anotaci @entity potřeboval použít. To však nefunguje a nikde jsem se nedočetl, že by fungovat neměla. Musím ručně používat $entityClass v createEntity() což se mi ale nelíbí. Jak je to tedy?

Druhá věc. Do teď jsem používal Nette\Database a po přechodu na Dibi se mi nelíbí to, že neumí predikci SELECTů, ale všude v příkladech se používá SELECT *, což se mi nelíbí. Dá se toto v Leanu nějak pěkně vyřešit?

Díky.

před 6 lety

Pavel Macháň
Člen | 285

mrtnzlml napsal(a):

Ahoj, na stránkách jsem se dočetl, že „Název tabulky lze upřesnit pomocí anotace @table a třídu entity lze upřesnit pomocí anotace @entity (obě anotace patří nad třídu repositáře).“ a protože nedodržuji konvence, tak bych konkrétně anotaci @entity potřeboval použít. To však nefunguje a nikde jsem se nedočetl, že by fungovat neměla. Musím ručně používat $entityClass v createEntity() což se mi ale nelíbí. Jak je to tedy?

Druhá věc. Do teď jsem používal Nette\Database a po přechodu na Dibi se mi nelíbí to, že neumí predikci SELECTů, ale všude v příkladech se používá SELECT *, což se mi nelíbí. Dá se toto v Leanu nějak pěkně vyřešit?

Díky.

@mrtnzlml:

  1. Mám takovej dojem (nejsem si tím opravdu jistej), že se anotace @entity a @table odstranila. Toto se řeší pomocí mapperu, takže pokud nedodržuješ konvence základního mapperu tak si vytvoř svůj, který ti bude vyhovovat
  2. Todle je zase otázka repozitářů. Jak si složíš SQL dotaz takovej ho máš, jen nevím jak se bude chovat persistování entity.

Editoval EIFEL (22. 11. 2013 0:33)

před 6 lety

mrtnzlml
Člen | 143

EIFEL napsal(a):

  1. Mám takovej dojem (nejsem si tím opravdu jistej), že se anotace @entity a @table odstranila. Toto se řeší pomocí mapperu, takže pokud nedodržuješ konvence základního mapperu tak si vytvoř svůj, který ti bude vyhovovat
  2. Todle je zase otázka repozitářů. Jak si složíš SQL dotaz takovej ho máš, jen nevím jak se bude chovat persistování entity.

Tak to by bylo matoucí, protože @table funguje, to také využívám. Na mapper jsem úplně zapomněl. To by asi šlo. Díky.

U Nette\Database jsem si právě zvykl nepoužívat SELECT vůbec, což je maximálně pohodlné. Každopádně zatím se do tohoto návrhu dostávám, takže to asi nebudu teď řešit…

před 6 lety

Pavel Macháň
Člen | 285

mrtnzlml napsal(a):

Tak to by bylo matoucí, protože @table funguje, to také využívám. Na mapper jsem úplně zapomněl. To by asi šlo. Díky.

U Nette\Database jsem si právě zvykl nepoužívat SELECT vůbec, což je maximálně pohodlné. Každopádně zatím se do tohoto návrhu dostávám, takže to asi nebudu teď řešit…

@mrtnzlml: Asi sem si to splet s nějakou jinou anotací, už vtom mám zmatek jak je to roztroušené na 16 stránkách vlákna.

Select nemusíš používat. Stačí nově(dev verze) zavolat v repozitáři

$this->createFluent();
// což je toto + aplikace implicitních filtrů
$this->connection->select('%n.*', $table)->from($table);

Editoval EIFEL (22. 11. 2013 0:54)

před 6 lety

Michal III
Člen | 84

Umí si LeanMapper poradit s use statementy předků? Tj:

namespace Model\Entity;
use DateTime;

/**
 * @property DateTime $date
 */
class Predek extends \LeanMapper\Entity
{

}

//v jiném souboru

namespace Model\Entity;

class Potomek extends Predek
{

}

Totiž mně to při používání tímto způsobem pak hází:

LeanMapper\Exception\InvalidValueException

Property ‚date‘ is expected to contain an instance of Model\Entity\DateTime, instance of DibiDateTime given.

pokud přistupuji k date instance Potomek ($potomek->date).

před 6 lety

Tharos
Člen | 1042

@Michal III: Už by měl. Prosím o otestování. Stačilo jenon změnit hodnotu jednoho parametru. :)

Díky za upozornění!

Editoval Tharos (24. 11. 2013 0:23)

před 6 lety

Tharos
Člen | 1042

@mrtnzlml:

Ahoj, na stránkách jsem se dočetl, že „Název tabulky lze upřesnit pomocí anotace @table a třídu entity lze upřesnit pomocí anotace @entity (obě anotace patří nad třídu repositáře).“ a protože nedodržuji konvence, tak bych konkrétně anotaci @entity potřeboval použít. To však nefunguje a nikde jsem se nedočetl, že by fungovat neměla. Musím ručně používat $entityClass v createEntity() což se mi ale nelíbí. Jak je to tedy?

Je to tak, že ty anotace byly tak trochu reliktem z doby, kdy ještě neexistoval samostatně stojící mapper. Chvíli jsem je udržoval i po představení mapperu, ale jakmile Lean Mapper začal podporovat single table inheritance, ty anotace přestaly fungovat dobře. Uměly se s mapperem „tlouct“ (mohly tvrdit něco jiného než mapper), a proto jsem je odstranil. Respektivě odstranil jsem anotaci @entity, která skrývala více potenciálních problémů. Anotace @table se stále ještě bere v potaz.

Mapper by je měl plně nahrazovat. Vím, že mi píšeš na Gitu – odpovím i tam. :)

Druhá věc. Do teď jsem používal Nette\Database a po přechodu na Dibi se mi nelíbí to, že neumí predikci SELECTů, ale všude v příkladech se používá SELECT *, což se mi nelíbí. Dá se toto v Leanu nějak pěkně vyřešit?

V ORM je obecně žádoucí mít entity plně inicializované, byť to s sebou nese nějakou režii. Pokud bys ale někde opravdu chtěl načíst jenom „část entity“, můžeš se podívat na tento můj prastarý příspěvek. Dnes by to díky implicitním filtrům mělo být ještě snazší.

Přímo koncept „předbíhání budoucnosti“ není v Lean Mapperu naimplementován.

před 6 lety

mrtnzlml
Člen | 143

Tharos napsal(a):

Je to tak, že ty anotace byly tak trochu reliktem z doby, kdy ještě neexistoval samostatně stojící mapper. Chvíli jsem je udržoval i po představení mapperu, ale jakmile Lean Mapper začal podporovat single table inheritance, ty anotace přestaly fungovat dobře. Uměly se s mapperem „tlouct“ (mohly tvrdit něco jiného než mapper), a proto jsem je odstranil. Respektivě odstranil jsem anotaci @entity, která skrývala více potenciálních problémů. Anotace @table se stále ještě bere v potaz.

Jak už psal EIFEL, pokoušel jsem se napsal v mapperu metodu getEntityClass tak, aby uměla přistoupit k dokumentačnímu komentáři repozitáře, ale to neumím. Jak na to, když mám BlogArticleRepository (obecně jakýkoliv repozitář), který dědí od ARepository, který dědí od LeanMapper\Repository, který volá metodu getEntityClass z mapperu? Pokud se dostanu nějakým rozumným způsobem k tomu komentáři, tak napsat si vlastní anotaci už není problém…

V ORM je obecně žádoucí mít entity plně inicializované, byť to s sebou nese nějakou režii. Pokud bys ale někde opravdu chtěl načíst jenom „část entity“, můžeš se podívat na tento můj prastarý příspěvek. Dnes by to díky implicitním filtrům mělo být ještě snazší.

Přímo koncept „předbíhání budoucnosti“ není v Lean Mapperu naimplementován.

Díky, to že je běžné entity plně inicializovat jsem nevěděl.

před 6 lety

Pavel Macháň
Člen | 285

mrtnzlml napsal(a):

Jak už psal EIFEL, pokoušel jsem se napsal v mapperu metodu getEntityClass tak, aby uměla přistoupit k dokumentačnímu komentáři repozitáře, ale to neumím. Jak na to, když mám BlogArticleRepository (obecně jakýkoliv repozitář), který dědí od ARepository, který dědí od LeanMapper\Repository, který volá metodu getEntityClass z mapperu? Pokud se dostanu nějakým rozumným způsobem k tomu komentáři, tak napsat si vlastní anotaci už není problém…

Nepřijde mě to jako moc dobrej nápad zatahovat do mapperu repozitáře.

Editoval EIFEL (24. 11. 2013 12:11)

před 6 lety

mrtnzlml
Člen | 143

EIFEL napsal(a):

Nepřijde mě to jako moc dobrej nápad zatahovat do mapperu repozitáře.

Ok, pak se ale vracím k problému, který ještě nikdo nedokázal konkrétně zodpovědět. Tím co jsem zde psal jsem navazoval na https://github.com/…per/issues/8#…, konkrétně na „you can even parse @entity annotation above repository class from your custom mapper“

Nebráním se jinému řešení, ale zatím opravdu jako nejlepší vychází při psaní repozitáře specifikovat jaká entita se má používat. Jenže když bych neměl zatahovat repozitář do mapperu, pak padla také myšlenka, že by mělo existovat v mapperu něco jako překladová tabulka table ⇒ entity a zase mi přijde dost nelogické při psaní repozitáře zasahovat do nějakého mapperu, který ani není vidět, že se používá a přidávat novou hodnotu do slovníku…

Nebo existuje ještě jiné maximálně elegantní řešení, jak specifikovat se kterou entitou a tabulkou má repozitář pracovat?

před 6 lety

Pavel Macháň
Člen | 285

mrtnzlml napsal(a):

EIFEL napsal(a):

Nepřijde mě to jako moc dobrej nápad zatahovat do mapperu repozitáře.

Ok, pak se ale vracím k problému, který ještě nikdo nedokázal konkrétně zodpovědět. Tím co jsem zde psal jsem navazoval na https://github.com/…per/issues/8#…, konkrétně na „you can even parse @entity annotation above repository class from your custom mapper“

Nebráním se jinému řešení, ale zatím opravdu jako nejlepší vychází při psaní repozitáře specifikovat jaká entita se má používat. Jenže když bych neměl zatahovat repozitář do mapperu, pak padla také myšlenka, že by mělo existovat v mapperu něco jako překladová tabulka table ⇒ entity a zase mi přijde dost nelogické při psaní repozitáře zasahovat do nějakého mapperu, který ani není vidět, že se používá a přidávat novou hodnotu do slovníku…

Nebo existuje ještě jiné maximálně elegantní řešení, jak specifikovat se kterou entitou a tabulkou má repozitář pracovat?

Udělal bych si překladovej slovník v mapperu. Sice to nebudeš mít ihned po ruce, když budeš psát repozitář ale budeš to mít všechno na jednom místě v mapperu a nic tě nebrání si ho rozšířit (pokud implementujes IMapper) o ten překladovej slovník.

Editoval EIFEL (24. 11. 2013 13:30)

před 6 lety

jannek19
Člen | 47

ahoj, zkouším teď LeanMapper na jednom projektu, chystám se implementovat jednu konkrétní funkcionalitu v mém systému, jedná se o ukládání jednotlivých revizí pro každý článek. Mám určitou představu, jak bych to v LeanMapperu udělal, ale zajímalo by mě, jestli na danou situaci nenapadne někoho něco lepšího, nějaké best practise. Pokud se tu něco takového už řešilo, tak se omlouvám, tohle vlákno je fakt hrozně dlouhé, je možné že jsem to přehlédl (fakt by chtělo zpracovat jednotlivá řešení z tohoto topicu do dokumentace na webu). Takže, mám to zatím navrženo následovně:

  1. DB tabulka article uchovává společné informace o jednotlivých článcích, přes sloupec revision_id odkazuje na aktuální revizi
  2. DB tabulka revision uchovává jednotlivé revize článků – data, která se revizi od revize liší – text článku, titulek, autor revize, datum vytvoření revize, komentář k revizi, atd. Zároveň má sloupec article_id, ve kterém je informace, ke kterému článku vlastně tato revize patří a sloupec parent_id, který se buď odkazuje na rodičovskou revizi, nebo je nastaven na NULL.

Ukládání a publikování jednotlivých revizí má na starosti DB procedura saveArticle, které se jen předají data z formuláře (text, titulek, autor,…) a ona se automaticky na základě předaných dat (autor, typ uložení (autosave, draft, final version), atd.) rozhodne, jestli přepíše některou dřívější revizi, nebo jestli vytvoří zcela novou revizi a také jestli má danou revizi publikovat (jestli má nastavit article.revision_id na ID právě uložené revize). Zároveň taky kontroluje, jestli nedošlo k potenciální kolizi, jestli někdo jiný mezitím nepublikoval, nějakou jinou revizi. Informaci o možné kolizi předává aplikaci pomocí DB erroru, který nám do aplikace díky dibi probublá ve formě exception. Pokud k chybě nedojde, vrátí ta DB procedura do aplikace odpovídající article_id a revision_id, co s nimi aplikace udělá je na ní (přesměruje na detail dané revize, na detail článku, cokoli). Co jsem si s tím tak různě při vývoji hrál, fungovalo to ukládání podle mě moc pěkně, ale teď to nějak potřebuju napasovat na entity a repozitáře v LeanMapperu :)

Co se entit týče napadlo mě, udělat v DB view (articleview), který by dával dohromady informace z tabulky article a odpovídající záznamy z tabulky revision, a v aplikaci entitu ArticleviewEntity (a k ní odpovídající repositář). V repozitáři bych pak přepsal metodu persist, aby místo INSERT/UPDATE volal tu proceduru saveArticle, možný problém, který tam vidím, je to automatické vytváření nových revizí (nekonzistence dat v entitě?) a to vyhazování té Exception při kolizi. Jsou to ale reálné problémy, nebo si jen vytvářím zbytečná strašidla?

Časem taky budu potřebovat navázat na jednotlivé revize další data (tagy, fotografie, …). To jsem ale zatím nepromýšlel.

Tak co, jdu na to dobře, nebo by to šlo lépe? Nějaké rady, poznámky, nápady?

Díky.

před 6 lety

Tharos
Člen | 1042

@mrtnzlml:

Ještě jednou jsem jsi přečetl to Tvé zadání:

I have database, with namespaced tables (blog_articles, set_options, sys_issues, sys_logins and so on). But I want to specify, that entities are BlogArticle, Issue, Login…

Neřešme nyní repositáře. Máme tedy prefixované tabulky blog_articles, set_options, sys_issues atp. a potřebujeme vyjádřit, že v tabulce blog_articles jsou data pro entity BlogArticle, v sys_issues jsou data pro Issue a tak dále.

Přesně k tomu slouží metody IMapper::getEntityClass($table, Row $row = null) a IMapper::getTable($entityclass) – tyto metody prostě musí vracet správné hodnoty pro různé vstupy. Jestli jejich implementace bude využívat nějakého výčtu přímo v kódu, nějakého konfiguračního souboru, nějaké speciální tabulky v databázi… už je vedlejší.

Pak zde máme repositáře. Řekněme, že budeme mít repositáře BlogArticleRepository, IssueRepository a tak podobně. Ty lze „namapovat“ buďto pomocí anotace @table (@table blog_articles nad BlogArticleRepository, @table sys_issues nad IssueRepository atp.), anebo implementací metody IMapper::getTableByRepositoryClass. Já bych v tomto případě spíše využil anotaci @table – na tohle se přesně hodí a takové řešení neskrývá žádná skrytá rizika.

Je to řešení Tvého problému? :)


Důvodem, proč mapování sys_issuesIssue a podobné musí být v mapperu a ne jen v nějaké anotaci nad repositářem, je to, že tyhle detaily nezajímají jenom repositáře. Architektura Lean Mapperu je taková, že entita pro načtení těch entit, na které má vazbu, nepotřebuje odpovídající repositáře, nýbrž se k nim umí dotraverzovat „svépomocí“. K tomu ale potřebuje znát detaily O/R mapování a pokud by byly zapsané jen někde v anotaci nad nějakým repositářem, nebyly by jí nic platné (entity nemají na repositářích vůbec žádnou závislost, vůbec o nich neví). Ona se ptá mapperu.

Tohle je přesně důvod, proč anotace @entity nefungovala dobře a proč byla zrušena.

Editoval Tharos (25. 11. 2013 0:37)

před 6 lety

mrtnzlml
Člen | 143

Tharos napsal(a):

Přesně k tomu slouží metody IMapper::getEntityClass($table, Row $row = null) a IMapper::getTable($entityclass) – tyto metody prostě musí vracet správné hodnoty pro různé vstupy. Jestli jejich implementace bude využívat nějakého výčtu přímo v kódu, nějakého konfiguračního souboru, nějaké speciální tabulky v databázi… už je vedlejší.

Je to řešení Tvého problému? :)

Díky, tomuto všemu myslím rozumím, ale možná jsem to celou dobu popisoval blbě, takže mi všichni odpovídají na něco jiného. Za to se omlouvám. Mě právě spíš zajímala implementace toho co je vedlejší, tedy jak správně vracet hodnoty pro různé vstupy. Přivedlo mě k tomu to, že napsat to do repozitáře pomocí anotace @entity bylo dost elegantní, například oproti využívání konfiguračního souboru, spec. tabulky v DB, atd… Proto jsem se snažil v mapperu doprogramovat reflexi repozitáře, abych si tam tu anotaci dodělal sám, ačkoliv v Leanu tato funkce již není. Každopádně to neumím udělat, takže zřejmě zatím zůstanu u nějakého méně elegantního řešení, jako je třeba:

/** @var array */
protected $translate = array( //FIXME: stupid solution
    //table_name => entity
    'blog_articles' => 'BlogArticle',
    'odr_orders' => 'Order',
    'examplehashedname' => 'Author', //cannot resolve entity name from table_name
);

//...

public function getEntityClass($table, LeanMapper\Row $row = null) {
    //zde bych rád provedl reflexi nad repozitářem, který je aktuální, ale to neumím
    if(array_key_exists($table, $this->translate)) {
        return $this->defaultEntityNamespace . '\\' . $this->translate[$table];
    } else {
        return $this->defaultEntityNamespace . '\\' . ucfirst($table);
    }
}

Každopádně díky za tvůj čas, nesmírně si toho vážím. Třeba časem přijdu na to jak elegantně rovnou při psaní repozitáře specifikovat entitu a nebudu muset zasahovat do mapperu, nebo konfiguračního souboru. Nebo možná řeším blbosti a opravdu se to takto normálně řeší. Pak se již není o čem bavit… (-:

před 6 lety

Tharos
Člen | 1042

@jannek19: V Tvém případě se budeš muset rozhodnout, co necháš na databázi (v podobě pohledu, uložených procedur a tak podobně) a co necháš v „objektech“. Pravděpodobně by to s Lean Mapperem celé šlo vyřešit v „objektech“, jsem přesvědčen, že by ses obešel i bez té uložené metody saveArticle. Ale vůbec není na překážku – lze ji využít z repositáře při persitenci, přesně, jak píšeš.

Osobně bych asi vynechal ten pohled pro join dat z article a revision, protože to lze v Lean Mapperu velmi snadno propojit pomocí filtru. Psáno z hlavy by to mohlo vypadat nějak takhle:

// CustomMapper.php
public function getImplicitFilters($entityClass, Caller $caller = null)
{
    if ($entityClass === 'Model\Entity\Article') {
        return new ImplicitFilters(function ($statement) { // tzv. anonymní filtr, novinka, o které se akorát chystám napsat na fórum :)
            $statement->select('[revision].]')->join('revision')->on('[task.revision_id] = [revision.id]')
        });
    }
}

Entita Model\Entity\Article pak bude zapoudřovat už ta spojená data. Úplně stejně, jako by zapouzdřovala data z Tvého view.

Problémy s automatickým vytvářením nových revizí bys mít neměl, protože ty můžeš v repositáři v rámci persistence libovolně upravit data entity. Prostě když zjistíš, že update vedl například k vytvoření nového záznamu v tabulce revision, tak jen nastavíš jeho ID do entity Article a na závěr zavoláš ještě v repositáři $article->markAsUpdated(), aby se při příští persistenci neměl tendenci volat update toho sloupce s cizím klíčem.

Osobně v tom pak už nevidím žádné zádrhely. Jinými slovy: pokud ti při persistenci uložená procedura saveArticle změní stav dat, není problém ty změny reflektovat do entity ještě před tím, než ji vrátíš z repositáře ven.

Mám můj popis hlavu a patu a je srozumitelný? :)

před 6 lety

Tharos
Člen | 1042

@mrtnzlml: No, hele, popíšu Ti jeden trochu bláznivý způsob, jak toho docílit. :) Byl jsem línej se o tom rozepisovat, ale noc je ještě mladá…

Ty si tu překladovou tabulku můžeš totiž vygenerovat právě z těch anotací:

  1. Vytvoř si vlastní mapper (ten už asi máš)
  2. Vytvoř si repositáře a využij v nich anotace @table@entity
  3. Do konstruktoru mapperu si předej RobotLoader a vytáhni si z něj hned všechny repositáře, které má naskenované
  4. Ty projdi a vytáhni si z každého (třeba pomocí parseru anotací z Nette) hodnotu anotace @table@entity
  5. No a výsledkem bude přesně ta překladová tabulka, kterou bys jinak psal a udržoval ručně :)

Důležité je tuhle analýzu provádět maximálně jednou (při vytváření instance mapperu) a ne při každém dotazu na IMapper::getEntityClass nebo IMapper::getTable, protože jinak by to mohlo být hodně líné.


Robot loader je na takovéto hrátky super věc. Já takhle skenuji entity, z jejichž reflexí pak generuji schéma databáze a v jednom projektu částečně i sestavuji formuláře.

Editoval Tharos (25. 11. 2013 0:35)

před 6 lety

mrtnzlml
Člen | 143

Díky, takto mi to stačí. Vždy jsem se pokoušel v mapperu získat aktuálně používaný repozitář, což se ale ukázalo jako docela náročný úkol. Toto řešení mě nenapadlo a je to konečně něco u čeho již mohu říct ano, to jsem hledal. (-: Díky za pomoc a inspiraci…

před 6 lety

Michal III
Člen | 84

Tharos napsal(a):

@Michal III: Už by měl. Prosím o otestování. Stačilo jenon změnit hodnotu jednoho parametru. :)

Díky za upozornění!

Otestováno, funguje. Díky za rychlou opravu :-).

před 6 lety

Tharos
Člen | 1042

Ahoj,

píšu nový quick start (který bude součástí nové dokumentace) a vymyslel jsem si zadání s „nenulovou“ doménovou logikou. Představený model bude někde na pomezí mezi anémickým a rich domain.

Jak tak quick start píšu, napadají mě různé nápady, které se většinou ihned snažím převést v realitu. Na GitHubu teď přibylo pár novinek, které bych zde postupně rád představil. A začnu tím, čemu říkám:

Anonymní filtry

Jde o úplně jednoduchý koncept. Mějme nějaký absolutně jednoúčelný filtr, který má v celém modelu smysl použít jen na jednom jediném místě. Například máme knihu s přiřazenými tagy a chceme mít v entitě Book read only položku isEbook, která vrátí boolean hodnotu vyjadřující, zda kniha má přiřazen jeden vybraný tag (třeba s názvem „ebook“).

Lze na to napsat krásný stručný filtr. Doteď ale bylo nutné ten filtr pojmenovat (otrava), zaregistrovat v Connection (další otrava) a teprve pak jej bylo možné využít.

Nově instance Filtering a ImplicitFilters přijímají jako první argument nejen pole s názvy filtrů, ale i s instancemi Closure, kterým se předává Fluent a dynamicky předané parametry. Pole může obsahovat klidně i mix názvů filtrů s instancemi Closure a dále také platí, že pokud chcete použít jenom jeden filtr (ať už daný jako název nebo Closure), může se předat přímo (nemusí se předávat v rámci pole s jedním prvkem), viz ukázka v tomto postu.

Těmto anonymním filtrům nejde autowirovat entitu ani property a nejde jim ani předat žádné targeted args (což je pochopitelné, nelze na ně odkazovat názvem), ale to vůbec nevadí, protože vše lze do filtru dostat pomocí use při vytváření Closure.

Takto tedy může takový anonymní filtr vypadat v praxi:

/**
 * @property int $id

 * @property Tag[] $tags m:hasMany

 * @property string $name
 * @property string|null $description
 * @property string|null $website
 * @property-read bool $isEbook m:useMethods(isEbook)
 */
class Book extends \LeanMapper\Entity
{

    /**
     * @return bool
     */
    public function isEbook()
    {
        $filtering = new Filtering(function (Fluent $statement) {
            $statement->join('tag')->on('[book_tag.tag_id] = [tag.id]')
                    ->where('[tag.name] = "ebook"');
        });
        $rows = $this->row->referencing('book_tag', null, $filtering);
        return !empty($rows);
    }

}

Enjoy!

Editoval Tharos (25. 11. 2013 14:26)

před 6 lety

jannek19
Člen | 47

@Tharos: Díky za rady, tohle se hodí, zkusím to tak udělat a uvidím.

Stránky: Prev 1 … 14 15 16 17 18 … 23 Next