tiny ‘n’ smart
database layer

Odkazy: dibi | API reference

Oznámení

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

Lean Mapper – tenké ORM nad dibi

před 5 lety

Tharos
Člen | 1042

@mirdič Ahoj, přesně tohle řeší (mimo jiné) nadstavba Lean Query. V ní by řešení vypadalo následovně:

$categories = $domainQueryFactory->createDomainQuery()
    ->select('c')
    ->from(Category::class, 'c')
    ->join('c.products', 'p')->select('p')
    ->where('p.price > %i', $price)
    ->getEntities();

Velmi důležitá je tam ta část ->select('p'), díky které se v rámci toho jednoho dotazu získají i produkty (které jsou dražší než daná cena). Získaná data se nahydratují tak, že při traverzování se už žádné dotazy znovu nepokládají.

A díky inner joinu jsou i kategorie vyfiltrované požadovaným způsobem.

Best practice je tedy použít Lean Query, byť by to mělo být na tuhle jedinou věc v celé aplikaci. V jiném případě musíš udělat to, co píšeš: provádět restrikci dvakrát.

Editoval Tharos (27. 6. 2014 0:57)

před 5 lety

Tharos
Člen | 1042

@mirdič Mimochodem tenhle use-case mi byl vlastně největší motivací k tomu něco jako Lean Query psát. Viz tenhle můj popis úplně ekvivalentního problému.

před 5 lety

medhi
Bronze Partner | 189

Tharos napsal(a):

@medhi: Super, díky za přiblížení.

Automatické filtrování je strašně pohodlné. Nemusíš u všech vazeb, které vedou na konkrétní Student nebo Teacher, myslet na to, že je třeba do toho traverzování nějak zasáhnout. Proto Ti chci právě takové automatické řešení nastínit :). Dají se k tomu využít tzv. implicitní filtry v mapperu:

public function getImplicitFilters($entityClass, Caller $caller = null)
{
  if ($entityClass === Student::class) {
      return new ImplicitFilters(function (Fluent $statement) {
          $statement->where('[user.type] = %s', 'student');
      });
  }
  if ($entityClass === Teacher::class) {
      return new ImplicitFilters(function (Fluent $statement) {
          $statement->where('[user.type] = %s', 'teacher');
      });
  }
  return parent::getImplicitFilters($entityClass, $caller);
}

Pak se při každém traverzování na studenty nebo učitele z libovolné entity bude hlídat, aby ve výsledku byly správné entity.


Mimochodem, s těmi implicitními filtry pracují i repositáře. S výše uvedeným filtrem můžeš mít nejen UserRepository, kde si vytažení správných dat musíš hlídat sám, ale například i StudentRepository a TeacherRepository, ve kteréch se při zavolání protected metody createFluent ten filtr automaticky použije, takže už se o to taky nemusíš více starat.

Persistovat přes ty StudentRepository a TeacherRepository je také možné, stačí tam jen při vkládání do databáze vložit do low-level dat požadovanou hodnotu do sloupce type (v mém ukázkovém případě). Asi bych si na to přetížil metodu Repository::insertIntoDatabase… U persistence hodně záleží na tom, jak to celé spravuješ, jaký máš backend, jaké jsou případy užití atp. Je více možností, jak takové entity persistovat.

Kdyby Ti bylo cokoliv dalšího nejasného, klidně se ptej. :)

Ještě bych se rád vrátil k tomuto řešení. Funguje v klasickém případě, ale tyto implicitní filtry se asi nespouští v tomto případě:

$students = $this->getValueByPropertyWithRelationship('students');

Co s tím? Díky

EDIT: Už jsem na to asi přišel, takto:

$students = $this->getValueByPropertyWithRelationship('students', new Filtering(function (Fluent $statement) {
            $statement->where("role = 'student'");
        }));

Editoval medhi (27. 6. 2014 10:45)

před 5 lety

mirdič
Člen | 41

@Tharos děkuji za odpověď a názornou ukázku.

Pokouším se o implementaci, ale narazil jsem na problém:

Pokud mám v entitě toto

public function getDate_from()
{
    $date = new PlaceInTime($this->row->date_from);
    return $date->format("d.m.Y");
}

Skončí s chybou

LeanMapper\Exception\InvalidArgumentException #1 Missing 'date_from' column in row with id 2.

EDIT: tak je to pouze, když není entita definována v anotaci, pokud je i v anotaci, tak to funguje.

Editoval mirdič (27. 6. 2014 14:51)

před 5 lety

mirdič
Člen | 41

Tak jsem narazil na problém, který nevím jak řešit a musím se poradit :)

Mám entitu exkurze, každá exkurze má několik termínů (term) a odjezdových míst (departure).

/**
 * @property int $id
 * @property string $title
 * @property Excursion_departure[] $departure m:belongsToMany() m:filter(order)
 * @property Excursion_term[] $term m:belongsToMany()
 */

class Excursion extends Entity
{
}

Termíny

/**
 * @property int $id
 * @property Excursion $excursion m:hasOne()
 * @property bool $open
 * @property string $date_from
 * @property string $date_to
 */

class Excursion_term extends Entity
{
}

Místo odjezdu

/**
 * @property int $id
 * @property Excursion $excursion m:hasOne
 */

class Excursion_departure extends Entity
{
}

Chtěl bych vypsat exkurze, které mají alespoň jeden nadcházející termín a alespoň jedno místo odjezdu.

Bez LeanQuery mi funguje následující řešení:

$query = $this->connection->select('excursion.*')
        ->from('excursion')
        ->join('excursion_term')
            ->on('excursion_term.excursion_id = excursion.id')
        ->join('excursion_departure')
            ->on('excursion_departure.excursion_id = excursion.id')
    ->where("excursion_term.date_from >= NOW()")
->groupBy("excursion_term.id");

return $this->createEntities($query->fetchAll());

Ale pokud chci to samé s LeanQuery

$query = $domainQuery->select('e')
                     ->from('\Model\Entity\Excursion', 'e')
                     ->join('e.term', 't')->select('t')
                     ->join('e.departure', 'd')
                     ->where("t.date_from >= NOW()");

return $query->getEntities();

Tak skončím s LeanMapper\Exception\InvalidArgumentException

/**
 * @param string $alias
 * @param Relationship|string $relationship
 * @throws InvalidArgumentException
 */
public function addRelationship($alias, $relationship)
{
    if (array_key_exists($alias, $this->relationships)) {
        throw new InvalidArgumentException;
    }
    $this->relationships[$alias] = $relationship instanceof Relationship ? $relationship : Relationship::createFromString($relationship);
}

Co dělám špatně? Btw pokud zakomentuji throw new InvalidArgumentException; tak to funguje.

Editoval mirdič (28. 6. 2014 18:14)

před 5 lety

Šaman
Člen | 2275

Ahoj, po pročtení většiny tohohle vlákna jsem nenašel jednoduchý způsob, jak vyřešit tohle:
Potřebuji vrátit počet navázaných property a nechci sčítat počet záznamů na hotové kolekci (není to efentivní). Vytvořil jsem si proto filtr:

<?php
public function count(Fluent $statement)
{
    $statement->removeClause('select')->select('count(*)');
}
?>

Ale jak ho teď mám použít? Ideálně v anotaci, ale už jsem pochopil, že to asi nepůjde:

<?php
/**
 * @property-read Task[] $tasksCount m:belongsToMany m:filter(count) <- odešle správný SQL dotaz, ale výsledek se snaží napasovat do kolekce
 * @property-read int    $tasksCount m:belongsToMany m:filter(count) <- belongsToMany nemůže vracet int
?>

Ok, druhá možnost je napsat si vlastní getTasksCount(), ale tady začalo peklo. Nízkoúrovňový přístup není zdokumentovaný ani v těch střípkách v tomto vláknu. Filtr mám připravený a zaregistrovaný, proč mám tedy znovu vytvářet instanci Filtering? A jak jí předat parametr? Callback nefunguje, název filtru nefunguje, trvá to na Filtering objektu.

Za nějakou podporu efektivního countování, ideálně na úrovni anotace bych se velmi přimlouval. Je to myslím docela častý požadavek a pokud to nejde efektivně, většina lidí to bude dělat hrubou silou dokud nezačne mít problémy s rychlostí.

Vím, že tohle by dobře řešila LeanQuery, ale zatím mám postavený projekt na LM a query teď přidávat nechci. Díky.

před 5 lety

Tharos
Člen | 1042

@Šaman: Ahoj, je to úplně jednoduché:

/**
 * @property int $id
 */
class Book extends Entity
{
}

/**
 * @property int $id
 * @property-read int $booksCount m:useMethods
 */
class Author extends Entity
{

    public function getBooksCount()
    {
        $rows = $this->row->referencing('book', 'author_id', new Filtering(function (Fluent $statement) {
            $statement->removeClause('select')->select('COUNT(*) [count], [author_id]')
                ->groupBy(['author_id']);
        }));
        return empty($rows) ? 0 : reset($rows)->count;
    }

}
$authorRepository = new AuthorRepository($connection, $mapper, $entityFactory);

foreach ($authorRepository->findAll() as $author) {
    echo $author->id, "\n";
    echo $author->booksCount, "\n";
}
SELECT [author].* FROM [author]
SELECT COUNT(*) [count], [author_id] FROM [book] WHERE [book].[author_id] IN (1, 2, 3, 4, 5) GROUP BY [author_id]

Pokud bys nechtěl mít názvy sloupců takhle hard-coded, samozřejmě by sis je mohl zjišťovat z reflexe entity atp. Také by to šlo určitě zobecnit do nějaké BaseEntity.

Důležité je nezapomenout na tu GROUP BY klauzuji, protože bez ní není možné získat počty pro všechny entity z kolekce, nad kterou se traverzuje.

Editoval Tharos (15. 8. 2014 9:29)

před 5 lety

Šaman
Člen | 2275

Díky, tohle funguje. Ale abych řekl pravdu, doufal jsem v jednodušší řešení, ideálně na úrovni anotace.

Je možné se nějak dostat na Fluent jiné property? Třeba v mém případě ty tasky projdou nejprve filtrováním, potřeboval bych dostat z názvu property její připravený $statement. V něm bych jen vyměnil část select a přidal groupBy a bylo by. Z toho už by se dala udělat nějaká zkratka v abstraktní Entitě.

před 5 lety

Tharos
Člen | 1042

Pokud bys to chtěl nějak zobecnit, postupoval bych osobně cca následovně:

  • Pomocí __call v BaseEntity bych odchytával metody getCountOf<Name>.
  • Pri vlastním získávání počtu bych použil metodu getValueByPropertyWithRelationship s tím, že bych jí předával potřebné filtry. Určitě bych při tom využíval metodu createImplicitFilters, aby byly do hry zapojeny i implicitní filtry.
  • Metoda getValueByPropertyWithRelationship musí navrátit entitu, a proto bych provedl takový drobný fígl. Pomocí filtru bych vykouzlil například takovýto SELECT: SELECT COUNT(*) `count`, 1 `isCountQuery` s tím, že bych si pro takový výsledek vytvořil vlastní entitu a v mapperu bych ji v metodě getEntityClass vrace pro takový Row, které obsahuje ten sloupec isCountQuery. :)

To by mělo být slušně univerzální řešení. Stačí Ti to takhle popsat, anebo mám napsat ten kód? :)

Editoval Tharos (15. 8. 2014 23:05)

před 5 lety

Šaman
Člen | 2275

Ahoj, narovinu – nejsem z toho moc moudrý.
Tohle nespěchá, zatím používam předchozí řešení, ale až budeš někdy něco podobného řešit, dej sem prosím ten kód. :)

před 5 lety

Matey
Člen | 137

Zdravím,

čítal som https://forum.dibiphp.com/…orm-nad-dibi?p=16 a zapáčilo sa mi také riešenie ktoré @Tharos ukázal, mať celú entitu pokope a preloženú do potrebného jazyka a dokonca aj fuknčné persistovanie, ale nerozchodil som to pre najnovší LM 2.2.0

Problém bol v prepisovaní metod ktoré boli v tých časoch iné ako sú v LM 2.2

šlo by prosím túto ukážku aktualizovať? bol by som veľmi vďačný

před 5 lety

Šaman
Člen | 2275

Jestli chceš už připravený aktuální LM balíček, tak si natáhni přes composer saman/leanmodel.
Ukázka natavení extension je tady, plus samozřejmě informace o databázi. Pokud budeš dědit od mých entit a repository, máš k dispozici nějaké fičury navíc, ale všechno by mělo být velmi kompatibilní s původním LM.

Pokud bys chtěl čistý LeanMapper, tak si do konfigurace doplň původní LeanMapper EntityFactory a jako base třídy používej zase ty původní (LeanMapper si composer stáhne taky, stejné tak Dibi).

Editoval Šaman (18. 8. 2014 12:24)

před 5 lety

Matey
Člen | 137

@Šaman áno ja už saman/leanmodel používam, už sme sa o tom bavili na nette fore, len mám problém napasovať do toho tie preklady, či už použijem mapper, entity, repository z leanmodelu alebo leanmapperu vždy narazím na to že potrebujem prepísať niečo čo je v leanmapperi ako protected, takže predpokladám že sa od tohoto času https://forum.dibiphp.com/…orm-nad-dibi?p=16 zmenilo niečo v LM

edit: translate entity som už rozbehol

Editoval Matey (4. 9. 2014 1:59)

před 5 lety

Šaman
Člen | 2275

Ajo, sorry, nějak mi nedošlo, komu odpovídám. S překladama nevím, ty nepoužívám, takže asi budeš muset počkat na Tharose.

před 5 lety

sitnarf
Člen | 27

Chtěl bych se zeptat, mam v databazi sloupec roles, ktery obsahuje string roli oddelenych carkou ve tvaru „admin,salesman“, ja to ale v entite chci mit jako pole, kde mam volat explode? Děkuji.

před 5 lety

Etch
Člen | 404

Jednoduše v getteru

public function getRoles(){
    return explode(',', $this->row->roles);
}

před 5 lety

sitnarf
Člen | 27

Díky, a když to budu chtít hodit zpátky do db z array na CSV, tak přes filter?
Jen menší rýpalství: Je to úplně správné z hlediska návrhu? Pochopil jsem, že Entity má být nezávislá na persistentní vrstvě, tzn. že kdybych entity exportoval pomocí jiného repository např. do JSONu a tam už to bude čistě v array, rozsype se mi to, kvůli explode.

Etch napsal(a):

Jednoduše v getteru

public function getRoles(){
    return explode(',', $this->row->roles);
}

Editoval sitnarf (27. 11. 2014 10:18)

před 5 lety

Etch
Člen | 404

@sitnarf

Přes setter, protože filter nelze pustit na basic typ.

K tomu rýpalství…

Je z hlediska návrhu databáze správné mít v „databazi sloupec roles, ktery obsahuje string roli oddelenych carkou ve tvaru “admin,salesman”“?? :)

před 5 lety

Etch
Člen | 404

@sitnarf

A ještě co se toho getteru týče, tak nikdo neříká, že musí být naimplementován takto jednoduše. Já ho napsal takto triviálně jen na základě „požadavku“. Stejně tak to může vypadat třeba takto:

public function getRoles($asArray = false){
    if($asArray === true){
        return explode(',', $this->row->roles);
    }
    return (string)$this->row->roles;
}

public function setRoles($roles){
    if(is_array($roles)){
        $roles = implode(',', $roles);
    }
    $this->row->roles = (string)$roles;
}

následně pak getter funguje takto:

dump($user->roles);

/* vrací: */
"admin,salesman" (14)

dump($user->getRoles());

/* vrací: */
"admin,salesman" (14)

dump($user->getRoles(true));

/* vrací: */
array (2)
    0 => "admin" (5)
    1 => "salesman" (8)

a setter dokáže přijnout jak pole, tak string

$user->roles = 'admin,salesman';
$user->setRoles('admin,salesman');
$user->roles = array('admin','salesman');
$user->setRoles(array('admin','salesman'));

Editoval Etch (27. 11. 2014 14:02)

před 5 lety

Tharos
Člen | 1042

Také lze použít příznak m:passThru, vypadalo by to například takhle:

m:passThru(decodeFromJson|encodeToJson)

Alternativou je samozřejmě vlastní getter a setter. Pokud bys nechtěl přímo pracovat s Row, můžeš jej napsat takhle:

public function getRoles()
{
    return Json::decode($this->get('roles'));
}

public function setRoles(array $roles)
{
    $this->set('roles', Json::encode($roles));
}

Tenhle zápis jen vyžaduje mít i anotaci @property array $roles, na kterou se ty interní metody Entity::get a Entity::set odvolávají.

Editoval Tharos (1. 12. 2014 12:29)

před 5 lety

Joe Kolář
Člen | 13

Zdravím,
v projektu mám entitu zápasu, která dvě vazby hasOne na entity družstva, pro domácí a hostující tým. U každého družstva bych chtěl přistupovat k jeho zápasům, jak na to?

<?php
/**
 * @property int            $id
 * @property Team           $homeTeam m:hasOne(home_team_id:team)
 * @property Team           $awayTeam m:hasOne(away_team_id:team)
 */
class Match extends Entity
{

}


/**
 * @property int           $id
 * @property-read Match[]       $matches m:useMethods
 */
class Team extends Entity
{

    public function getMatches()
    {
        // jak zde implementovat pristup k zapasum pres home_team_id i away_team_id?
        return [];
    }

}
?>

Díky za vaše případné rady.

Editoval Joe Kolář (26. 12. 2014 10:25)

před 5 lety

Etch
Člen | 404

@JoeKolář:

Takhle z patra mě napadají dvě možnosti.

  1. Můžeš property zápasů prohnat filtrem a do statementu si přidat podmínku pro druhý sloupec.
  2. Můžeš si na to udělat view.

Oboje má své výhody i nevýhody.

před 5 lety

Joe Kolář
Člen | 13

@Etch:
Pokusil jsem se to vyřešit přes filtr:

<?php
    public function allMatches(Fluent $statement, Team $team, Property $p)
    {
        $statement->removeClause('where')->where('[home_team_id] = ', $team->id, 'OR [away_team_id] =', $team->id);
    }
?>

Do databáze jde upravený dotaz s podmínkou pro oba sloupce, který vrátí správný počet řádků. Ale průchodem přes BelongsToMany vazbu mi to nazpět vrátí jen entity (řádky), u kterých sedí id s hodnotou ve sloupci uloženým v BelongsToMany::columnReferencingSourceTable. Dočasně jsem to vyřešil separátní metodou v MatchRepository, ale moc se mi to řešení nezdá. Jak obejít to omezení v BelongsToMany?

před 5 lety

JuniorJR
Člen | 181

@JoeKolář Ahoj, zkusil bych neco ve stylu:

<?php
class Team extends Entity
{
    public function getAllMatches()
    {
        $matchTableName = $this->mapper->getTable('Match');

        // @see https://github.com/Tharos/LeanMapper/blob/develop/LeanMapper/Row.php#L233
        $allMatches = array();
        foreach ($this->row->referencing($matchTableName, 'home_team_id') as $homeMatch) {
            $allMatches[$homeMatch->id] = $homeMatch;
        }
        foreach ($this->row->referencing($matchTableName, 'away_team_id') as $awayMatch) {
            $allMatches[$awayMatch->id] = $awayMatch;
        }
        return $allMatches;
    }
}
?>

Editoval JuniorJR (30. 12. 2014 9:57)

před 5 lety

Joe Kolář
Člen | 13

@JuniorJR: Chtěl jsem to vyřešit na jeden dotaz, takhle je to na dva, ale běží to. Musí se teda ještě prohrat přes Entity Factory (referencing vrací jen Row) a oživit, ale ok. Díky moc.

Editoval Joe Kolář (30. 12. 2014 15:43)

před 4 lety

Felix
Nette Core | 898

Udelal jsem fork Davidova db-benchmark repa a udelal par vylepseni.

Muj fork:
https://github.com/…db-benchmark

Knihovny:

  • NDB 2.0
  • NDB 2.1
  • NDB 2.2
  • NDB 2.3
  • NDB 2.4-dev
  • NotORM
  • LeanMapper
  • Doctrine2

Mam i aktualni benchmarky:
https://github.com/…nchmark-logs

před 4 lety

martin.knor
Člen | 23

Ahoj, resili jste nekdo u translate repository metodu findBy? Jde o to ze pokud potrebuju filtrovat pomoci sloupce ktery je v translate tabulce, tak mi to samozrejme hleda podle sloupce v puvodni – prekladane tabulce.

Mam to vyresene takto, ale nevim jestli je spravne vytvaret prazdnou entitu kvuli tomu abych zjistil ktere pole jsou translated.

public function findBy(array $options, $fetch = \Collection::ANY, $lang = null)
    {
        $table = $this->getTable();
        $query = $this->createFluent($lang);
        $entityClass = $this->mapper->getEntityClass($table);

        $langCol = array();
        if (is_subclass_of($entityClass, 'Entity\TranslatableEntity')) {
            $langCol = $this->getTranslatableColumns($this->createEmptyEntity($entityClass));
        }

        foreach ($options as $col => $val) {
            $query->where('%n.%n = %s', in_array($col, $langCol) ? $this->mapper->getTranslationTable($table) : $table, $col, $val);
        }

        $collection = $this->createEntities($query->fetchAll());

        return $fetch === \Collection::ONE
            ? $collection->first()
            : $collection;
    }

A dalsi vec, pokud bych chtel prochazet vsechny preklady pro jeden radek (napriklad pro edit form), jak na to? Mam vytvorit entitu pro preklady a pripojit ji k prekladane entite pomoci belongsToMany? Ikdyz to mi prijde celkem pracne pro kazdou prekladanou entitu tohle delat.

Editoval martin.knor (22. 3. 2015 16:25)

před 4 lety

Pavel Janda
Člen | 801

@Tharos Předpokládám, že neplánuješ upgrade LeanMapperu pro Dibi ~3.0? …

Jsem si jist, že by to mnozí uvítali. :) Nepovažuji LeanMapper, natož Dibi za mrtvý projekt. (https://packagist.org/…mapper/stats)

Editoval Pavel Janda (9. 11. 2015 8:59)

před 4 lety

castamir
Člen | 631

@PavelJanda co presne je tam za problem? Ja LM aktivne pouzivam na vice projektech…

před 4 lety

Pavel Janda
Člen | 801

castamir napsal(a):

@PavelJanda co presne je tam za problem? Ja LM aktivne pouzivam na vice projektech…

Vždyť jsem to právě (haha, před více jak 2 měsíci) napsal.

před 3 lety

castamir
Člen | 631

Vydal jsem verzi 3-RC1, ktera je kompatibilni s dibi v3.x

před 3 lety

Šaman
Člen | 2275

Ahoj, řeším práci s vazebními tabulkami. Mám tam jeden sloupec navíc (kromě dvou sloupců se idčkama spojovaných entit). Čtení mám zvládnuté přes filtr, ale zjistil jsem, že to toho sloupce neumím zapisovat. Zkoušel jsem ohnout LM aby mi vzal takovýto zápis:

<?php
// Přidání místnosti osobě, ale s nastavením sloupce 'hidden' na TRUE
$this->person->addToRooms($roomId, ['hidden' => TRUE]);
?>

To se mi ale nepodařilo. Je nějaký jednodušší způsob, než si vytvářet novou entitu?
Díky.

před 3 lety

Pavel Janda
Člen | 801

@castamir Nechceš implementovat něco jako psal @Šaman výše? Určitě by to použil každý druhý člověk, který používá LeanMapper. Já bych to též ocenil.

před 3 lety

Šaman
Člen | 2275

Jen resume, jak jsem to řešil a nepřišel na jiny způsob:

Musel jsem to upravit tak, abych vytvořil pravé entity. Problém je za prvé že jim neodpovídá entita v ER diagramu a hlavně musím dodatečně k již existujícím spojovacím tabulkám doplnit sloupec id.

Čtení ze spojovacích tabulek s dodatečným sloupcem je vykoušené přes filtry, takže jde opravdu jen o možnost zápisu. I za cenu toho, že „editace“ by znamenala odstranit starou vazbu a přidat novou, jinak nakonfigurovanou.

před 3 lety

Pavel Janda
Člen | 801

Taky to tak občas řeším, ale neuvěřitelně se mi to nelíbí. Vazba, která má vlastnost (sort, locale, whatever), ještě nemusí být nutně další entita. Není to entita, je to vazba.

před 3 lety

Ravenss
Člen | 12

Jakým způsobem docílím v entitě aby mohla mít ze stejné tabulky další „nadřazenou“ entitu?

(Potřebuji generovat kategorie a z nich hierarchické menu)

před 3 lety

janpecha
Člen | 58

@Ravenss myslíš něco takového?

/**
 * @property int $id
 * @property string $name
 * @property Category|NULL $parent m:hasOne(parent_id)
 */
class Category extends Entity
{
}
Stránky: Prev 1 … 21 22 23