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 | 1041
+
0
-

Ahoj,

před pár dny jsem provedl nenápadnou dekompozici třídy LeanMapper\Repository, která jako bonus třídu o osm řádků zkrátila :), ale hlavně Lean Mapperu přinesla velmi zajímavé nové možnosti.

Nyní je možné pomocí protected metod Repository::insertIntoDatabase, Repository::updateInDatabase a Repository::deleteFromDatabase doladit, jak přesně se entita persistuje do databáze. Při persistenci může například dojít k rozdělení entity do více tabulek a nově kvůli něčemu takovému není zapotřebí přepisovat celou metodu Repository::persist. Ta mimochodem dekompozicí také hezky prokoukla. Více než dříve nyní operuje na stejné úrovni abstrakce. :)

BC break je, že zmizely protected metody Repository::beforeCreate, Repository::beforeUpdate a Repository::beforePersist. Ty teď totiž vůbec nejsou zapotřebí – nově zavedené metody je plně nahrazují. A já jsem popravdě za jejich zrušení vděčný, protože se mi moc nelíbily a dvojnásob pak po zavedení událostí, se kterými mírně kolidovaly i názvoslovně.


Abych demonstroval možnosti, které nyní Lean Mapper nabízí, hrál jsem si přes víkend s překlady, které jsou pro tohle ideálním případem užití. Byl jsem nadšen, jak snadno šlo v Lean Mapperu vytvořit univerzální systém překladů.

Jak na překlady

Vytvoříme si mapper (i s rozhraním), který bude oproti vestavěnému umět vrátit i název tabulky s jazyky a také názvy překladových tabulek.

Vytvoříme filtr, který bude umět při načítání dat přijoinovat překlady a následujícím způsobem jej zaregistrujeme.

Dále si vytvoříme abstraktní entitu, která o sobě bude umět prozradit, která její data se mají uložit do hlavní a která do překladové tabulky.

No a na závěr vytvoříme abstraktní repositář, který při persistenci rozdělí a pošle data tam, kam v databázi patří.


No a to je všechno. Vybaveni tímto kódem pak můžeme jednoduše nadefinovat takovéto entity a repositáře:

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

/**
 * @property int $id
 * @property DateTime $created
 * @property bool $active = true
 * @property string|null $name m:translatable
 * @property string|null $description m:translatable
 *
 * @property Content[] $contents m:belongsToMany m:filter(translate)
 */
class Page extends TranslatableEntity
{
}
use Model\Entity\Page;

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

A celé to můžeme používat naprosto přirozeným způsobem:

$langRepository = new LangRepository($connection, $mapper);
$pageRepository = new PageRepository($connection, $mapper, $langRepository);

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

foreach ($pageRepository->findAll($csLang) as $page) {
    write($page->name);
    foreach ($page->contents as $content) {
        write($content->content, 4);
    }
}

Takto jsme vypsali české názvy všech stránek a české varianty všech jejich obsahů. Všimněte si, že to, v jakém jazyce se načítají obsahy, se převzalo ze stránek.

Pokud bychom ale chtěli vypsat české názvy všech stránek, ale například anglické obsahy, je to triviální:

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

foreach ($pageRepository->findAll($csLang) as $page) {
    write($page->name);
    foreach ($page->getContents($enLang) as $content) {
        write($content->content, 4);
    }
}

Samozřejmostí je fungující persistence:

$page = $pageRepository->find(1, $csLang);

$page->name = 'Upravená úvodní stránka';

$pageRepository->persist($page);

A ještě zajímavější je vytváření stránek úplně na zelené louce:

$page = new Page(array(
    'lang' => $csLang,
    'name' => 'Vytvořená stránka',
    'created' => new DateTime,
));

$pageRepository->persist($page);

$page->lang = $enLang;
$page->name = 'Newly created page';

$pageRepository->persist($page);

Aby byla data v databázi za všech okolností konzistentní, už při prvním volání ->persist($page) se v databázi předpřipraví překlad pro všechny dostupné jazyky. Druhé volání ->persist($page) tak vede k SQL příkazu UPDATE.


Připravil jsem plně funkční balíček ke stažení, stačí v něm jen v souboru /app/init.php upravit údaje pro připojení k MySQL databázi (a poskytnout nějakou takovou, ve které mohou být vytvořeny ukázkové tabulky a data).

Na závěr bych chtěl podotknout, že lokalizace je jen jeden z mnoha scénářů, při kterém se dají uvedené principy elegantně využít. Užití je mnohem širší.

Enjoy!

Editoval Tharos (19. 8. 2013 10:35)

před 5 lety

Casper
Člen | 253
+
0
-

@Tharos:
Bylo by možné implementovat podporu pro isset a empty u vztahových atributů entity? (mám dojem, že tu podobný požadavek už byl)

/**
 * @property int $userId
 * @property Payment[] $payments m:belongsToMany
 */
class User extends BaseEntity {}

echo count($user->payments); // vrátí například 3
echo isset($user->payments); // nyní vrátí FALSE, což je matoucí
echo empty($user->payments); // nyní vrátí TRUE, což je matoucí

před 5 lety

Tharos
Člen | 1041
+
0
-

@Casper: Přesně tohle už mám na programu. Doplním to, jakmile bude čas. :)

před 5 lety

Filip111
Člen | 244
+
0
-

@Tharos:
Jsem moc rád, že nezapadl příklad s překlady někam pod stůl a dál s ním pracuješ. Sám jsem ještě neměl čas vše přepsat, takže používám LM zatím v app bez překladů.
Na zbývající vychytávky se podívám jindy.

Mohl bys mi poradit s filtrem?
Zkouším seřadit entity ve vazbě belongsToMany a nemůžu se dostat přes hlášku
Unsupported filter usage given: CommonFilter2::orderSchedule.

/**
* @property int $id
* @property CourseSchedule[] $schedule m:belongsToMany(course:gaudeo_courses_schedule) m:filter(CommonFilter2::orderSchedule)
*/
class Course extends BaseEntity {

}

class CommonFilter2 {
    public static function orderSchedule(DibiFluent $statement) {
        $statement->orderBy('day, time_from');
    }
}

Díky.

před 5 lety

Tharos
Člen | 1041
+
0
-

@Filip111: Koukni na tenhle můj post, filtry jsem nedávno přepracoval (a zdokonalil).

Filtry nově nejsou jenom „tupá statická volání“, ale pojmenované callbacky, které se registrují do LeanMapper\Connection. V příznaku m:filter se pak používají jména těch filtrů.

před 5 lety

Filip111
Člen | 244
+
0
-

@Tharos:
Díky, už to běží.

Už je tu těch užitečných informací v tomhle vláknu moc a než člověk něco najde…
šel jsem poctivě na mikrodokumentaci filtrů odkazovanou na webu, která už bohužel není aktuální :)
http://www.leanmapper.com/…ntace/filtry

před 5 lety

tomas.lang
Člen | 54
+
0
-

Ahoj, je možné dostat přes @property ven nějakou třídu? Pracuji se sloupečkem Polygon který ve výchozím stavu vrací data v binárním formátu. Pomocí helperu si urpavuji dotaz tak abych ven dostal pěknou stringovou reprezentaci, kterou bych pak rád předal nějaké třídě, ale popis stylu @property \GPS\Polygon $polygon mi nefunguje – jak by bylo nejlepší postupovat?


Momentálně to řeším pomocí setPolygon/getPolygon, ovšem s tímto je nov problém, neboť LM mi ve výchozím stavu se to pokouší uložit jako string

UPDATE `region`
SET `polygon`='GeomFromText(\'POLYGON((15.523682 48.912392,15.5896 48.959302,15.834045
48.854053,15.523682 48.912392))\')'
WHERE `id` = 1

což samozřejmě vede ke špatnému chování – jak prosím případně můžu přesvědčit LM aby zachoval ten string co mu předávám?

Editoval tomas.lang (19. 8. 2013 17:45)

před 5 lety

Tharos
Člen | 1041
+
0
-

@tomas.lang: Nepomohl by příznak m:passThru? Před vrácením by z nízkoúrovňové hodnoty vyrobil instanci a před zápisem by z té instance vytvořil potřebná binární data.

Pak je ještě otázkou, jak to zvládne navázat dibi. Pokud bys chtěl navést ještě lépe, přibliž mi, jak bys to řešil čistě v dibi. Z toho už bych byl schopen navrhnout řešení najisto.

před 5 lety

tomas.lang
Člen | 54
+
0
-

@Tharos: Binární data určitě nechci vyrábět na straně PHP – komunikace by měla probíhat na základě těch příkazů typu GeomFromText('POLYGON((....

Z hlediska dibi bych v podstatě potřeboval pro sloupeček polygon použít modifikátor %sql přes který bych tam ten příkaz GeomFromText('POLYGON((15.523682 48.912392,15.5896 48.959302,15.834045 48.854053,15.523682 48.912392))') vrazil, jenže do toho jak se skládá dotaz v Repository::updateInDatabase či Repository::insertIntoDatabase asi nejsem schopen zasáhnout (myšleno hlavně získání $values skrz $entity->getModifiedRowData(); – zas nerad bych si udržoval kopii celé metody)?


Ono ve chvíli kdybych byl schopný v tom updateInDatabase či insert... udělat nad $valus něco ve stylu \Nette\Utils\Arrays::renameKey($values, 'polygon', 'polygon%sql');, pak bych neměl problém, jenžo to mi příjde trochu over-kill…

Editoval tomas.lang (19. 8. 2013 20:59)

před 5 lety

Tharos
Člen | 1041
+
0
-

@tomas.lang: Ty metody Repository::updateInDatabase a Repository::insertIntoDatabase jsou právě z podobných důvodů protected. Nestačilo by je přetížit a předat data dibi „ručně“? Pak by nebyl problém nějakou hodnotu předat s modifikátorem %sql… Třeba tak, jak píšeš.

To přesně Ti přijde jako over-kill? Ta následná úprava hodnot získaných pomocí Entity::getModifiedRowData()? Já bych se tohohle třeba vůbec nebál. :) Já s nimi v tom překladovém příkladu také pracuji – rozděluji je do více tabulek.

Jinak, kdybys to chtěl zobecnit, šlo by to úplně zautomatizovat. Šlo by ty metody napsat tak, aby nízkoúrovňové hodnoty z položek typu Polygon byly předávány s modifikátorem %sql.

před 5 lety

tomas.lang
Člen | 54
+
0
-

@Tharos: tak nakonec jsem si volání getModifiedRowData obalil ve svém výchozím Repository tak aby si ty data jednotlivé repozitáře mohly libovolně modifikovat – každopádně jsem se ještě chtěl zeptat, na které úrovni by jsi imlementoval to automatické upravování pro Polygon položky – jestli by jsi zasáhl do Repository a upravil to podobným způsobem a nebo jsi to udělal na úrovni Entity či hlouběji (což se mi zase moc nezdá, neboť entita o způsobu implementace ukládání vlastně neví…)

před 5 lety

Tharos
Člen | 1041
+
0
-

@tomas.lang: No, já bych to asi řešil jako takovou souhru:

  • Pomocí filtru bych při načítání dat volal helper (předpokládám, že ten je uložený v databázi), který převede binární hodnotu na nějakou textovou.
  • Následně bych pomocí příznaku m:passThru (nebo pomocí setPolygon/getPolygon metod) při čtení tu textovou hodnotu převáděl na instanci GPS\Polygon a při zápisu bych instanci GPS\Polygon převáděl na nějakou tu textovou hodnotu.
  • V repositáři bych přetížil metody insertIntoDatabase a updateInDatabase a upravil je tak, aby se low-level hodnoty (to jsou ty textové) z položek typu GPS\Polygon navazovaly s modifikátorem %sql a zároveň aby se ta hodnota obalila do volání metody GeomFromText.

Tady je škoda, že dibi neumí vrátit a přijmout GPS\Polygon rovnou tak, jako umí třeba DateTime. Výše uvedený popis to v podstatě supluje.

Jinak by to celé mělo jít zobecnit, pokud bys měl položky typu GPS\Polygon v hodně entitách. V konkrétních repositářích a entitách by to pak vypadalo, jako kdyby to umělo přímo dibi, tj. nemusel bys už řešit vůbec nic (jako například u DateTime).

Editoval Tharos (20. 8. 2013 8:37)

před 5 lety

tomas.lang
Člen | 54
+
0
-

@Tharos: tak nakonec jsem v podstatě takové popisované řešení udělal – děkuji moc za rozbor, i za práci co tu odvádíš :-)

Každopádně jsem se chtěl zeptat jestli do budoucna nehodláš přidat podporu pro příznaky typu @property SomeObject $property mimo entity, s tím že u takovéhoto objektu vy se počítalo, že jako vstup v constructu bude brát string a bude mít implementováno __toString – právě mi příjde škoda, že to v podstatě funguje jen pro DateTime – není problém si to naimplementovat pomocí get/set metod – ale spíše mě hlavně zajímá jak to s ohledem na to vidíš ty? ;-)

před 5 lety

Ripper
Člen | 56
+
0
-

Zdravím,

ještě bych měl jeden menší dotaz. Jak nejefektivněji udělat editování řádků v databázi pokud jsem odeslal formulář. Tedy získám si hodnoty z odeslaného formuláře přes getValues() a s těmi bych chtěl dále pracovat.

Původně jsem myslel že to udělám nějak takhle –

$userEntity = Model\Entity\User($dataFromSubmittedForm);
$userRepository->persist($userEntity);

Ale to odpadá kvůli tomu, že tam není přenášeno id upravovaného řádku, šlo by tam sice dát, ale musí to jít i jinak ne? Opět děkuji předem za pomoc.

před 5 lety

tomas.lang
Člen | 54
+
0
-

@Ripper:

// ziskani entity - prepokladame parametr `id` v URL
$userEntity = $this->userRepository->find($this->getParameter('id'));
// nastaveni dat z formu
$userEntity->assign($dataFromSubmittedForm);
// ulozeni
$this->userRepository->persist($userEntity);

Pokud máš id toho usera přímo někde ve formu, tak akorát stačí změnit ten find – tak snad alespoň trochu pomůžu… :-)

před 5 lety

Tharos
Člen | 1041
+
0
-

@Ripper: Jak naznačil tomas.lang, fígl je v tom, že když pracuješ s editačním formulářem, typicky máš upravovanou entitu už načtenou z repositáře. Jak jinak bys totiž formuláři nastavil defaultní hodnoty? Flow poté velmi zjednodušeně může být následující:

$author = $authorRepository->find($id);

$form->setDefaults($author->getData());

$form->onSuccess[] = function ($form) use ($author, $authorRepository) {
    $author->assign($form->getValues());
    $authorRepository->persist($author);
}

před 5 lety

Tharos
Člen | 1041
+
0
-

Ahoj,

tak jsem včera vytvořil na GitHubu release větev, což znamená, že vydání stable verze je za dveřmi.

Verze ponese označení 2.0, protože od poslední stable verze (1.4) došlo k řadě významých architektonických změn: byl oddělen mapper, byly přepracovány filtry, byly zavedeny události… Skok mezi verzemi 1.3, 1.4 a 1.5 by tak byl příliš nepoměrný.

Těsně před vydáním jsem si troufl udělat ještě jeden BC break: metody Entity::markAsCreated, Row::markAsCreated a Result::markAsCreated jsem přejmenoval na markAsAttached – lépe to vystihuje podstatu věci a lépe to koresponduje se souvisejícími metodami Entity::isDetached a Entity::detach.


V release větvi už budu jen upravovat případné bugy a za pár dní ji mergnu do masteru a verzi 2.0 formálně vydám. API už se ale nezmění, takže kdo můžete, aktualizace je už teď vřele doporučená.

Věci, které jsem doteď nestihl naimplementovat a mám je v plánu (metody hasIs<Name>, removeAll<Name>, __isset…) doplním možná hned v nějaké setinkové verzi – nepřinesou totiž žádné BC breaky.

Aktuální RC verze ke stažení.

Díky vám všem za přízeň a také za poskytnutou podporu!

Editoval Tharos (24. 8. 2013 1:27)

před 5 lety

Casper
Člen | 253
+
0
-

Ahoj,
mám dotaz k těm výjimkám jak jsem psal dříve. Tenhle commit má být zmíněným přepracováním chybových zpráv? Pokud ano, rád bych věděl důvody k nevyužití get_called_class. Přijde mi totiž poměrně otravné se prohrabávat call-stackem, abych zjistil, kde je cca chyba, když bych si to mohl přečíst v prvních znacích chybové hlášky.

před 5 lety

Ripper
Člen | 56
+
0
-

Ahoj, děkuji za rady, ale nebyl bych to já kdybych se nezeptal ještě jednou.

Tento kód –

$userId = $this->getId();

$userEntity = $this->userRepository->find($userId);
$userEntity->assign($data);

$this->userRepository->persist($userEntity);

Vyhazuje hlášku „ID can only be set in detached rows.“ a já nemám tušení z jakého důvodu.

před 5 lety

enumag
Člen | 2128
+
0
-

@Ripper: Zřejmě máš id v $data.

před 5 lety

Ripper
Člen | 56
+
0
-

@enumag pravda, neumím přemýšlet. Díky!

před 5 lety

Tharos
Člen | 1041
+
0
-

@Casper: Ještě to není úplně ono. V tom commitu jsem jen hlášky obecně trochu učesal a stylisticky sjednotil.

Použití get_called_class u entit dává smysl a na 99 % na něj ještě dojde.


S chybovými hláškami je to vcelku věda… Nyní například skrze entitu probublává spousta chyb z nižších vrstev, jejichž text možná není bez znalosti „vnitřností entity“ úplně dokonale srozumitelný. Řeřešním by samozřejmě bylo všechny tyto výjimky v entitě odchytávat a překládat na vysokoúrovňovější (nejčistější způsob), ale problém je, že těch výjimek je tolik, že by snad polovina kódu LeanMapper\Entity nakonec řešila jenom tohle předávání výjimek…

Na Lean Mapper fóru, až bude, bych asi založil vlákno, kde by bylo možné reportovat situace, kdy Lean Mapper zahlásil nějakou těžko srozumitelnou chybovou hlášku (v daném kontextu). Ty konstelace, které reálně nastávají, bych pak řešil.

před 5 lety

froggy
Backer | 17
+
0
-

Ahoj, mám dotaz k následující situaci.

Definice entity A:

<?php
/**
 * @property int $id
 * @property Template $template m:hasOne(template_id:template)
 */
class Plan extends \LeanMapper\Entity
{
}
?>

Definice entity B:

<?php
/**
 * @property int $id
 * @property int|null $template_id
 * @property Template $template m:hasOne(template_id:template)
 */
class Plan extends \LeanMapper\Entity
{
}
?>

Použití 1:

<?php
$plan = new Plan;
$plan->template_id = $template->id;
?>

Použití 2:

<?php
$schedule[$plan->template->day.'-'.$plan->template->order] = $plan;
?>

V případě definování entity podle A, je vyhozena výjimka Cannot access undefined property 'template_id'. při použití 1.

V případě definování entity podle B, použití 1 proběhne v pořádku. Při použití 2 je ale vyhozena výjimka Mapping collision on property 'template' (column 'template_id').
Please fix mapping or make chosen properties read only (using property-read).

Už mně nenapadá, jak toto řešit. Mohu poprosit o radu? Případně pošlu další části kódu.

Děkuji za pomoc.

Editoval froggy (21. 8. 2013 12:40)

před 5 lety

daniel.mejta
Člen | 21
+
0
-

froggy napsal(a):

V případě A máš property pojmenovanou pouze $template, takže přiřazení a volání hodnoty bude:
$plan->template = 1;
nebo
$plan->template = $templateRepository->find(1);

před 5 lety

froggy
Backer | 17
+
0
-

daniel.mejta napsal(a):

froggy napsal(a):

V případě A máš property pojmenovanou pouze $template, takže přiřazení a volání hodnoty bude:
$plan->template = 1;
nebo
$plan->template = $templateRepository->find(1);

Super, funguje. Děkuji. :)

před 5 lety

Šaman
Člen | 2262
+
0
-

@froggy:

  1. Nevidím rozdíl, mezi plánem 1 a 2.
  2. Podle té chybové hlášky v případě B to vypadá, že celá entita Plan se ukládá do tabulky template a mapperu se nelíbí, že máš dvě property pro jeden sloupec databáze. Nechceš ukázat víc kódu, třeba ti to pomůžeme trochu vyladit?
  3. Když pracuješ s entitama, tak tě vazební sloupce nezajímají. Nadefinuješ je v anotacích, nebo mapperu a od té doby o nich nevíš. Z toho vyplývá i to, že nemáš žádné property ve stylu template_id. Ty máš vazbu na entitu Template, pokud pracuješ na úrovni entit, tak při volání $plan->template dostaneš entitu (tedy objekt) a fakt, že vazba je realizovaná pomocí sloupce template_id, ve kterém je id číslo templaty, to zajímá jen mapper. Chceš-li zjistit id navázané templaty, tak $plan->template->id.

Editoval Šaman (21. 8. 2013 12:31)

před 5 lety

Filip111
Člen | 244
+
0
-

Ahoj, narazil jsem na drobnou překážku a nevím, jak ji správně vyřešit.
Mám entitu Student a ta má vazbu hasMany na přiřazené kurzy. Nicméně vazební tabulka obsahuje kromě id kurzu a id studenta ještě příznak, zda vazba platí nebo patří do archivu.
Jak se můžu dostat k této hodnotě platnosti vazby?

V příkladech:
Entita studenta

/**
* @property int $id
* @property string $nameFirst (name_first)
* @property Course[] $courses m:hasMany(student:students_courses:course:courses)
*/
class Student extends BaseEntity {

}

Entita kurzu:

/**
 * @property int $id
 * @property string $code
 * @property string $title
 * @property Student[] $students m:hasMany(course:students_courses:student:students)
 */
class Course extends BaseEntity {

}

Vazební tabulka:

CREATE TABLE students_courses` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `student` int(10) unsigned NOT NULL,      -- reference na studenta
  `course` int(10) unsigned NOT NULL,       -- reference na kurz
  `status` tinyint(1) NOT NULL DEFAULT '1', -- tahle hodnota mě zajímá
  PRIMARY KEY (`id`),
  ...
);

V tomto případě mě zajímá právě sloupec students_courses.status, ale obecně vzato občas do těchto vazebních tabulek strkám informace o řazení a další. Jedná se o informace, které jsou relevantní právě jen pokud jsou tyto entity spojené a uvnitř entity nemají žádný smysl.

Doplnění: další příklad na který jsem narazil je spojení kurzu a učitele, kdy každý učitel může mít pro daný kurz jinou sazbu. Tedy ve vazební tabulce mezi kurzem a učitelem je id kurzu, id učitele a hodinová sazba.

Díky za inspirativní návrhy.

Editoval Filip111 (21. 8. 2013 21:37)

před 5 lety

Tharos
Člen | 1041
+
0
-

@Filip111: Ahoj,

mám jenom pár minut na odpověď, ale alespoň ve stručnosti… Je to velmi zajímavé téma a shodou okolností to mám v jedné své aplikaci vyřešené myslím vcelku hezky.

Popíšu to alespoň ve stručnosti a v případě nejasností bych někdy později připravil nějakou ukázku.


Mějme cca následující entity:

/**
 * @property int $id
 * @property string $name
 * @property StudentsCourse[] $courses m:belongsToMany m:filter(joinDetail)
 */
class Student extends BaseEntity
{
}
/**
 * @property int $id
 * @property string $title
 * @property string $code
 */
class Course extends BaseEntity
{
}
/**
 * @property bool $status
 */
class StudentsCourse extends Course
{
}

Ten filtr joinDetail dělá to, že k hodnotě z vazební tabulky při-joinuje hlavní informace z tabulky courses. Tím je vyřešené čtení:

$student = $studentRepository->find($id);

foreach ($student->courses as $course) {
    echo $course->status; // $course instanceof StudentsCourse
}

No a pak už je stačí vyřešit persistenci. Entity Course a StudentsCourse obě persistuji přes repository CourseRepository a podle toho, jaká entita mi při persistenci přijde, data sypu buďto jen do tabulky courses, anebo i do tabulky students_courses.

Fungují tak oba následující scénáře:

$course = new Course(array(
    'title' => 'Some course',
    'code' => 'A001',
));

$courseRepository->persist($course); // vloží data pouze do tabulky courses

$studentsCourse = new StudentsCourse(array(
    'title' => 'Some course',
    'code' => 'A001',
    'student' => $student,
    'status' => true,
));

$courseRepository->persist($studentsCourse); // vloží data do tabulek courses i students_courses

Celé se to dá poměrně úspěšně abstrahovat, takže se to pak používá opravdu hezky a pohodlně.

Pokud spojovací tabulky nesou jen nějaké doplňkové informace k vazbě, dalo by se říct, že je tohle takové best practice.

před 5 lety

Tharos
Člen | 1041
+
0
-

@Filip111: No a ještě takový malý dovětek…

Pokud bys chtěl ke všem informacím přistupovat i z entity Course (tzn. „z druhé strany“), lze to úplně analogicky:

/**
 * @property bool $status
 */
class CoursesStudent extends Student
{
}

Entita Course pak může vypadat následovně:

/**
 * @property int $id
 * @property string $title
 * @property string $code
 * @property CoursesStudent[] $students m:belongsToMany m:filter(joinDetail)
 */
class Course extends BaseEntity
{
}

Ukázka použití:

$course = $courseRepository->find($id);

foreach ($course->students as $student){
    echo $student->status; // $student instanceof CoursesStudent
}

Opět to lze i z této strany persistovat (tentokrát přes CourseRepository).

Lze využít úplně stejný filtr – on si pomocí injektnutého mapperu může sám zjistit, z jaké tabulky má v tom či onom konkrétním případě při-joinovat informace.


Ono už je to taková Lean Mapper „vyšší dívčí“. Btw, jak se vůbec tohle řeší v jiných ORM? Zajímalo by mě (pro inspiraci) třeba takové řešení v Doctrine 2.

před 5 lety

Tharos
Člen | 1041
+
0
-

Takže, jenom bleskově jsem tak po diskuzích koukal, jak tohle řeší Doktrína, a narazil jsem na tohle a tohle …

S tím do jisté míry souhlasím, vlastně i v současném Lean Mapper quickstartu upozorňuji na to, že pro takové „vazební tabulky“ (které už nejsou tak úplně vazebními tabulkami) je vhodné vytvořit samostatnou entitu.

Bez diskuze to je třeba v případě, kdy „vazební tabulka“ udržuje doplňujících informací ještě kdo ví kolik…

Nicméně v případě, že tam je třeba jeden sloupec navíc (zda je vazba aktivní, výše výplaty zaměstnance v oddělení atp.), přijde mi příjemné, že Lean Mapper nabízí jako alternativu k vytváření samostatné entity i řešení popsané v mých předchozích dvou příspěvcích.


Vlastně mi přijde jako dost nedořešené, že v takovém případě musím mít tři plnokrevné entity… V reálném světě mám prostě zaměstnance, který pracuje v nějakém oddělení. A to, co za to má, je přece vlastnost toho vztahu mezi jím a oddělením, nikoliv další objekt… Nějak se mi tam tu třetí entitu nedaří najít. :–P

Editoval Tharos (22. 8. 2013 0:56)

před 5 lety

David Ďurika
Člen | 341
+
0
-

Ahoj,
neplanujes do Entity pridat metodu __isset() ?

pouzivam hrachov datagrid a on tam ma https://github.com/…Datagrid.php#L328 a ked $row je entita tak to isset($row->$column) vrati stale FALSE

dik

před 5 lety

Tharos
Člen | 1041
+
0
-

@achtan: Plánuji. Plánuji doplnit všechny tyhle aspekty magického přístupu, které teď Lean Mapper neřeší.

před 5 lety

froggy
Backer | 17
+
0
-

@Šaman:

  1. Omlouvám se, špatně jsem opsal kód. Opravil jsem to v původním příspěvku.
  2. Entita Plan se ukládala do správné tabulky, v tom potíž nebyla.
  3. Moje chyba byla v tom, že jsem zároveň definoval property template a template_id. Díky za vysvětlení, jak to funguje a že není třeba definovat ten vazební sloupec.

před 5 lety

Tharos
Člen | 1041
+
0
-

@Casper:

Takže metoda Entity::__isset je naimplementovaná.

Tebou uvedené ukázky už by měly fungovat podle očeávání:

/**
 * @property int $userId
 * @property Payment[] $payments m:belongsToMany
 */
class User extends BaseEntity {}

echo count($user->payments); // vrátí například 3
echo isset($user->payments); // nyní vrátí TRUE
echo empty($user->payments); // nyní vrátí FALSE

@achtan: Mohlo by to zabrat i v tom datagridu. Ale nevím to jistě, protože mi přijde, že hrach v něm není konzistentní s PHP – přijde mi, že pomocí isset zjišťuje, zda v $row existuje daný sloupec (něco jako array_key_exists), zatímco já v souladu s PHP v Lean Mapperu zjišťuji, zda daný sloupec existuje a zda se současně nerovná null.

Editoval Tharos (22. 8. 2013 23:05)

před 5 lety

pepakriz
Člen | 246
+
0
-

Tharos: isset v PHP nelze rozumně implementovat tak, aby se to chovalo jako u běžného pole. Už se to řešilo zde: https://github.com/…te/pull/1100.

Podle dokumentace by isset() mělo u objektu vracet TRUE i pro NULL hodnotu.

před 5 lety

Tharos
Člen | 1041
+
0
-

@pepakriz: Tak si schválně vyzkoušej spustit následující kód v aktuálním PHP :).

$arrayObject = new ArrayObject(array(
    'first' => null,
    'second' => 0,
));

var_dump(isset($arrayObject['first']));
var_dump(isset($arrayObject['second']));

Výsledek je, světe div se, následující:

bool(false)
bool(true)

V PHP byl bug, díky kterému se isset nad ArrayObject chovalo jinak, než všude jinde v PHP. Vtipné je, že ten bug byl nareportován dávno před tím, než vůbec proběhla ta diskuze na GitHubu.

Jestli NDB zavádí matoucí chování na základě bugu v PHP, je to jeho problém, ale já to fakt dělat nebudu. :)

Přes následující kód se prostě nedokážu přenést:

if (isset($entity->foo) and $entity->foo !== null) {
    // ...
}

Takže si za svou implementací stojím a tvrdím, že je logická a konzistentní se zbytkem PHP.

Editoval Tharos (23. 8. 2013 11:29)

před 5 lety

pepakriz
Člen | 246
+
0
-

Tharos: Ano, ArrayObject funguje správně, ale zkus si naimplementovat objekt implementující ArrayAccess se stejným chováním. To prostě v PHP IMHO nelze, proto ta diskuze. Nebo že by na tom PHP 5.5 už bylo lépe?

před 5 lety

Tharos
Člen | 1041
+
0
-

@pepakriz: Fakt že jo, máš pravdu. Je to bug v SPL, který v ní, co hůř, ještě asi chvíli bude … :(

Nicméně, v Lean Mapperu nikde ArrayAccess neimplementuji, u entit se přes array zápis přistupovat k položkám nedá. Takže se mě naštěstí tenhle bug netýká a isset v Lean Mapperu se může chovat konzistentně s isset tak, jak jsme na něj v PHP zvyklí:

Determine if a variable is set and is not NULL.

Díky za rozšíření obzorů. Rád si na tohle vzpomenu, až zase budu nějaký ArrayAccess implementovat.

Editoval Tharos (23. 8. 2013 15:36)

před 5 lety

castamir
Člen | 631
+
0
-

@Tharos Díky za všechnu tu práci, kterou tomto projektu věnoval – LeanMapper je vážně povedený.

Po vydání dlouho očekávané stable verze :p by to chtělo zapracovat na dokumentaci. Když ji zmákneš, tak podle ní udělám pár komplexnějších ukázek. Myslím, že jsem schopen pokrýt vše podstatné. Chci si novou vezi osvojit a porovnat řešení problémů v nové a staré verzi, než ji nasadím na novej projekt. Co ty na to?

Zejména bych potřeboval vědět, jak použít rozšířený fluent LeanMapper-connection, filtry, události a mapper – vše tak, jak to je a bude ve verzi 2.0.

před 5 lety

Šaman
Člen | 2262
+
0
-

Taky díky za tento nástroj, něco podobného mi dlouhou dobu chybělo. Sice existovaly ORMy nad Dibi, ale vesmés špatně zdokumentované a bez dokumentace se to nedá. (To je jeden z důvodů, proč raději nepoužívám NDb.)

Jestli se ti podaří sepsat v rozumné době dokumentaci, tak LM začnu učit ve škole – je dostatečně jednoduchý, abychom se nemuseli zabývat tématy mimo mísu (např. DQL), ale přitom se na něm dá ukázat filosofie modelu, jaká se pak používá i ve velkých projektech a v Doctrine. Taky na něj hodlám předělat TodoList z QuickStaru, nebo podobnou ukázkovou miniaplikaci.

Co se obecnějších milníků týče, neplánuješ někdy upravit LM i pro NDb? Je trochu škoda, že tento nástroj používá jen Dibi, čímž přestává být pro mnoho lidí zajímavý (určitě to bude bránit masovějšímu použití).

// skoro offtopic zamyšlení nahlas: I když z filosofického hlediska by takové použití (LM nad Ndb) nedávalo smysl – NDb vycházející z NotORMu se snaží o ‚objektový, jakoby entitní‘ přístup k tabulkám, zatímco LM je klasické ORM, které má na jedné straně plnohodnotné entity a na druhé čisté SQL. A studenti už znají SQL a učí se pracovat s entitama – nevidím důvod je učit NDb syntax, kterou nikde jinde nepoužijí. Je to vlastně stejná ztráta výukového času, jako učit je to DQL. Takže já bych už stejné zůstal u Dibi. Jen mě mrzí, že není nativně v Nette a tím je pro spoustu nováčků braný jako deprecated, přestože by pro ně bylo snadněji použitelné, než NDb (která tak trochu simuluje ORM, tedy něco navíc).

před 5 lety

Jan Tvrdík
Nette guru | 2547
+
0
-

Šaman wrote: neplánuješ někdy upravit LM i pro NDb? Je trochu škoda, že tento nástroj používá jen Dibi, čímž přestává být pro mnoho lidí zajímavý.

To jako proč? Znáš někoho takového? Odkud se bere ta utkvělá představa, že nehotový nástroj (ndb) je lepší než hotový (dibi)?

před 5 lety

Šaman
Člen | 2262
+
0
-

Jan Tvrdík napsal(a):

Šaman wrote: neplánuješ někdy upravit LM i pro NDb? Je trochu škoda, že tento nástroj používá jen Dibi, čímž přestává být pro mnoho lidí zajímavý.

To jako proč? Znáš někoho takového? Odkud se bere ta utkvělá představa, že nehotový nástroj (ndb) je lepší než hotový (dibi)?

Bohužel znám – naprostá většina nováčků začíná logicky stáhnutím Sandboxu, nebo Quickstartu. Obojí používá NDb a tak se ji snaží používat taky. Ale často jim to nejde, protože filosofie NDb je jiná, než čisté SQL, které už znají. Je to pro ně další překážka při přechodu z (v horším případě) procedurálního programování na Nette. Jako by nestačilo, že se musí poprat s MVC, linkováním, předáváním parametrů, konfigurací atd. Oni ale najednou nejsou schopni ani vytáhnout data z databáze, resp. sestavit žádný jen trochu složitější dotaz.

Ostatně já bych se choval stejně, kdybych zkoušel nový framefork – stáhnu si framework, nějaký Skeleton/Sandbox a zkouším. Přece nebudu zjišťovat, jestli náhodou neexistuje dva roky neaktualizovaná knihovna, která je hotová a vyladěná, ale nikde v dokumentaci se o ni nepíše, namísto té, která je nativně ve frameworku a píše se o ní všude na fóru (bohužel většina dotazů je jak udělat jednoduchou věc).

před 5 lety

Tharos
Člen | 1041
+
0
-

Ahoj,

díky moc za vřelou odezvu.

Dokumentace je teď věc číslo jedna, toho jsem si plně vědom. Stále postupuji podle roadmapy, kterou jsem sepsal na tu donate stránku.

Rád bych jako první věc doplnil podrobný changelog – zmapoval bych všechny změny a k důležitým změnám/novinkám bych dal přímé odkazy na relevantní příspěvky v tomto vlákně. Zde jsem totiž v průběhu vývoje psal snad úplně o všem. To bych chtěl udělat tento víkend. Myslím, že to bude dobrý odrazový můstek do nové dokumentace – takový changelog bude svým způsobem obsahovat vše klíčové, byť ne v tak pohodlném formátu.

U hlavní dokumentace jsem nyní ve fázi návrhu její „informační architektury“. Je poměrně klíčové promyslet, za jaký konec ji uchopit a jak optimálně postupně provést nově příchozího všemi možnostmi Lean Mapperu (kterých už není úplně málo).

Přiznám se, že ji píšu rovnou i anglicky. Troufale si myslím, že si Lean Mapper nevede vůbec špatně i vedle „profláknutých“ PHP ORM – Eloquent, RedBeanPHP, Idiorm a v jistém směru snad i vedle doktríny (kterou ale chovám v úctě a rozhodně nechci svůj marketing založit na vymezování se proti ní). Počet uživatelů Lean Mapperu by při rozšíření za hranice mohl narůst hned o několik řádů a to by byl teprve ten správný křest ohněm. ;)

Ad NDB) Ono by to nějak překlopit možná i šlo (i když vím minimálně o několika možných problémech), ale já v tom nevidím absolutně žádný smysl. Dibi mi přijde snad úplně ve všem lepší – je stabilní a nemění se pod rukama, je bohatě prověřené, není závislé na žádném full-stack PHP frameworku, má dokumentaci, je interně jednoduché, jasně srozumitelné pro nově příchozí uživatele vybavené znalostí SQL (což se o NDB, která vyžaduje jistý styl myšlení, tak úplně říci nedá)…

Pokud bych chtěl s Lean Mapperem do světa, už tuplem není co řešit – dibi je klasické DBAL a mít ORM jako vrstvu nad DBAL je naprosto logické (viz Doctrine). NDB není DBAL v pravém slova smyslu, je to DBAL s něčím… co se mi těžko nějak kategorizuje. Navíc Hrach se vůči Lean Mapperu začal aktivně vymezovat, takže já ani nevěřím, že by ta symbióza dělala dobrotu… Zkrátka tohle téma je pro mě uzavřené.

Editoval Tharos (24. 8. 2013 9:07)

před 5 lety

Šaman
Člen | 2262
+
0
-

Chápu. Z hlediska LM to nemá žádný smysl, podpora různých databází je díky Dibi dobrá.

Nevím, jak přesně si David představuje plánované rozbalíčkování Nette, ale možná by pak bylo možné vytvořit tutoriály a Sandbox bez NDb s Dibi. Dokud je Sandbox součástí distribuce a dokud ten Sandbox používá NDb, tak se NDb bere jako oficiální best practise na přístup k databázi. Osobně si myslím, že to není krok správným směrem, ale rozhodnutí, co bude součástí Nette je na Davidovi. Dobré je to, že Nette nás tu NDb nenutí používat.

před 5 lety

hrach
Člen | 1801
+
0
-

Nevim co je umyslem Samana, ale Nette\Database primarne obsahuje klasickou ->query metodu, ktera funguje temer stejne jako v dibi. Nemeni se pod rukama a nevyzaduje naprosto jine mysleni, jak je tu prezentovani. Je treba odlisit NDB a NDBT.

před 5 lety

Šaman
Člen | 2262
+
0
-

@hrach:

  1. Mým úmyslem je v ideálním případě mít možnost používat LM nad čistou distribucí Nette
  2. Soudě podle dotazů začátečníků se jich většina snaží používat Table. Přesně jak je to ukázáno v Sandboxu a QS. Ta má ovšem ty mouchy, o kterých je řeč výše. Možná je to jen chyba dokumentace a ukázkových příkladů, že nalákají neznalého uživatele na NDBT ukázkou nejjednoduššího selectu a nechají ho pak plácat se s problémy při netriviálním použití. V každém případě nováčci s tím problém mají, ber to jako fakt. A dokumentace k NDBT prakticky neexistuje, přestože už je ve stabilní distribuci Nette déle, než rok. Z toho všeho mi vychází názor, že NDB do stabilní verze dospělého frameforku zkrátka nepatří.
  3. Už jsme tu předtím byli offtopic a nechci zabředávat ještě víc mimo téma. Pokud chceme začít debatu o výhodách NDb a proč je lepší, než Dibi, tak by bylo lepší založit nové téma. (Skutečně nevím o jiných výhodách, než je Table, ale ta má spoustu problémů o kterých byla už řeč.) Jinak bych to nechal utichnout s tím, že si zkrátka každý může vybrat nástroj, který mu víc vyhovuje a u mě je to holt Dibi a momentálně LM.

Editoval Šaman (24. 8. 2013 21:55)

před 5 lety

Tharos
Člen | 1041
+
0
-

Ahoj,

tak jsem právě formálně vydal verzi 2.0.0. Mohlo by vás zajímat:

A na podrobné dokumentaci se už pracuje.

Díky za přízeň!

před 5 lety

Jan Suchánek
Backer | 403
+
0
-

Jak řešíte stránkování s filtry LM a případně s https://doc.nette.org/cs/pagination

Chci znát nejprve počet řádek

SELECT count(application.*) FROM application WHERE application.name LIKE 'a%'

Až podle počtu řádek, chci použít stejný WHERE a vypisovat či nevypisovat Entity.

SELECT application.* FROM application WHERE application.name LIKE 'a%' LIMIT 20 OFFSET 20

Editoval jenicek (27. 8. 2013 5:55)

před 5 lety

Casper
Člen | 253
+
0
-

@jenicek:

Přepokládám, že vytvořený a zaregistrovaný filter již máš. A že je také uvedený u entity u vztahového atributu. Pak stačí jen v Repository napsat metodu count pro daný filter. O filtrech podrobně zde.

// ApplicationRepository
public function getCountLike($like){
    return $this->connection->select("COUNT(*)")
                    ->from($this->getTable())
                    ->applyFilter("likeFilter", $like) // povšimni si použití filtru
                    ->fetchSingle();
}

A pak jen použiješ v Paginatoru jak jsi zvyklý.

$paginator->setItemCount($repository->getCountLike($like));
$paginator->setItemsPerPage(10);
$paginator->setPage(1);

EDIT: Nebo pokud ti šlo o použití offset a limit jako dalšího filtru k onomu likeFilter, pak je možné použít více filtrů. Více v tomto vlákně dole. Přičemž jako parametr filterů bude zapotřebí zmíněný query object.

@property Application[] $applications m:belongsToMany m:filter(limitAndOffset,likeFilter)

Editoval Casper (27. 8. 2013 9:01)

před 5 lety

Jan Suchánek
Backer | 403
+
0
-

@Casper: Díky moc, je to k studiu LM uplně ideální.

Editoval jenicek (27. 8. 2013 9:23)

Stránky: Prev 1 … 9 10 11 12 13 … 23 Next RSS tématu