Správný postup odstranění řádku z databáze

Upozornění: Tohle vlákno je hodně staré a informace nemusí být platné pro současné Nette.
David Kregl
Člen | 52
+
0
-

Zdravím všechny,

v Nette už jsem se dostal poměrně daleko, avšak včera jsem se rozhodl své aplikace přepsat za použití modelu, který jsem tak dlouho opomíjel. Zajímá mě, jaký požíváte způsob pro odstranění jednoho řádku z tabulky. Existuje nějaký best practice?

Od řešení očekávám:

  • ověření, zda-li záznam vůbec existuje;
  • informovat uživatele v případě, že byl záznam odstraněn (flashMessage);

Používám Nette Database s využitím modelu.

Současná situace:

BaseModel.php

/**
 * @param $id
 * @return int
 */
public function deleteById($id)
{
    return $this->db->table($this->table)->where('id', $id)->delete();
}

OrderPresenter.php

/**
 * @param $id
 */
public function handleDeleteOrder($id)
{
    if ($this->orderModel->findById($id)) {
        $this->orderModel->deleteById($id);
        $this->flashMessage('Objednávka byla úspěšně odstraněna.', 'success');
    }
}

Editoval David Kregl (8. 11. 2015 0:14)

hitzoR
Člen | 51
+
+3
-

delete() vrací počet ovlivněných řádků, takže stačí v modelu zkontrolovat, jestli je 1 a pokud ne, tak vyhodit výjímku.

Model:

/**
 * @param $id
 */
public function delete($id)
{
    $query = $this->db->table($this->table)->where('id', $id)->delete();
	if ($query !== 1) throw new Exception();
}

Presenter:

/**
 * @param $id
 */
public function handleDeleteOrder($id)
{
    try {
		$this->orderModel->delete($id);
		$this->flashMessage('...', 'success');
	} catch (Exception $e) {
		$this->flashMessage('...', 'danger'); // informování uživatele o chybě
	}
}
David Grudl
Nette Core | 8239
+
+4
-
  • slovo Model v názvu tříd, které jsou součástí modelu, je zbytečná vycpávka. Spíš bych to použil ve jmenném prostoru
  • pokud BaseModel reprezentuje (jestli dobře chápu) databázovou tabulku, měl by to mít v názvu, takže třeba Model\Table. Ano, i to Base mi připadá zbytečné. Třída pak může mít potomka např. Model\OrderTable.
  • pracovat v prezenterech přímo s tabulkami (resp. se třídami reprezentujícími tabulky) nemusí být ideální. Třeba pokud by po smazání objednávky mělo následovat odeslání potvrzujícího emailu, měl by to zajistit model (protože to je chování vnitřní logiky, modelu, nikoliv prezentační vrstvy), ale nepatří to do pravomoci třídy reprezentující tabulku. Vytvořil bych si další třídu, například Model\OrderFacade, jejíž metoda by uměla smazat záznam i odeslat email, a tuhle bych si injektoval a používal v presenteru.
David Kregl
Člen | 52
+
0
-

David Grudl napsal(a):

  • slovo Model v názvu tříd, které jsou součástí modelu, je zbytečná vycpávka. Spíš bych to použil ve jmenném prostoru
  • pokud BaseModel reprezentuje (jestli dobře chápu) databázovou tabulku, měl by to mít v názvu, takže třeba Model\Table. Ano, i to Base mi připadá zbytečné. Třída pak může mít potomka např. Model\OrderTable.
  • pracovat v prezenterech přímo s tabulkami (resp. se třídami reprezentujícími tabulky) nemusí být ideální. Třeba pokud by po smazání objednávky mělo následovat odeslání potvrzujícího emailu, měl by to zajistit model (protože to je chování vnitřní logiky, modelu, nikoliv prezentační vrstvy), ale nepatří to do pravomoci třídy reprezentující tabulku. Vytvořil bych si další třídu, například Model\OrderFacade, jejíž metoda by uměla smazat záznam i odeslat email, a tuhle bych si injektoval a používal v presenteru.

Děkuji za konstruktivní názor, OrderFacade je skvělý nápad, už jsem ji implementoval. Velice si Vážím vyjádření právě od Vás.

S přáním pohodového zbytku pracovního „večera“,
David

Editoval David Kregl (8. 11. 2015 1:18)

David Kregl
Člen | 52
+
0
-

hitzoR napsal(a):

delete() vrací počet ovlivněných řádků, takže stačí v modelu zkontrolovat, jestli je 1 a pokud ne, tak vyhodit výjímku.

Model:

/**
 * @param $id
 */
public function delete($id)
{
    $query = $this->db->table($this->table)->where('id', $id)->delete();
	if ($query !== 1) throw new Exception();
}

Presenter:

/**
 * @param $id
 */
public function handleDeleteOrder($id)
{
    try {
		$this->orderModel->delete($id);
		$this->flashMessage('...', 'success');
	} catch (Exception $e) {
		$this->flashMessage('...', 'danger'); // informování uživatele o chybě
	}
}

Věděl jsem, že to jde udělat elegantně. Děkuji.

Editoval David Kregl (8. 11. 2015 11:33)

yrow
Člen | 12
+
0
-

David Grudl napsal(a):

  • slovo Model v názvu tříd, které jsou součástí modelu, je zbytečná vycpávka. Spíš bych to použil ve jmenném prostoru
  • pokud BaseModel reprezentuje (jestli dobře chápu) databázovou tabulku, měl by to mít v názvu, takže třeba Model\Table. Ano, i to Base mi připadá zbytečné. Třída pak může mít potomka např. Model\OrderTable.
  • pracovat v prezenterech přímo s tabulkami (resp. se třídami reprezentujícími tabulky) nemusí být ideální. Třeba pokud by po smazání objednávky mělo následovat odeslání potvrzujícího emailu, měl by to zajistit model (protože to je chování vnitřní logiky, modelu, nikoliv prezentační vrstvy), ale nepatří to do pravomoci třídy reprezentující tabulku. Vytvořil bych si další třídu, například Model\OrderFacade, jejíž metoda by uměla smazat záznam i odeslat email, a tuhle bych si injektoval a používal v presenteru.

Zdravím,
takový dotaz, pokud implemetuji OrderFacade, který může obsahovat různé jiné metody (odesílání emailu), tak samotný model (OderModel) implementuji do této Facade třídy, nebo ta Facade třídá je zároveň i modelem, a nebo si předám v presentru ten model přes seter do Facade?

Šaman
Člen | 2666
+
+3
-

Modelem je všechno, model je celá vrstva aplikace na kterou se odkazuješ z presenterů a většinou je to to hlavní, co programátor programuje (v presenterech to v ideálním případě jen prováže a nasype do šablon). Takže i repozitář, tabulka, fasáda, vše patří do modelu.

Znáš přednášku 5 vrstev modelu?
Sice už je starší, ale tohle by ti mohla docela objasnit. To, čemu David říká Facade je v přednášce Service a tomu, čemu ty asi říkáš Model je tam Repository.

Obecně jde o to, že repository je většinou jeden na tabulku a obsluhuje jí poměrně nízkoúrovňově (já tam mám třeba specifické metody jako (articles) findNewest($count) která vrátí požadovaný počet nejnovějších článků). Repository obsluhuje jednu entitu a vzhledem k principu jediné odpovědnosti by neměl dělat nic dalšího.

Zatímco fasád může být víc a už nemají tak těsný vztah k jednotlivým entitám. Jedna může třeba po vydání článku načíst všechny emaily (pomocí jednoho repozitáře), na které se má článek poslat (ten načte z jiného repozitáře), a provede rozeslání (pomocí služby typu IMailer). A SRP je zachovaný, protože tahle třída dělá konkrétní a jednu věc – rozesílá novinky. Na další práci se články budou další fasády/service.

P.S.
Když už jsem teda odkázal na starší článek, tak ještě upřesním jak to vypadá v Nette\Database

  • úložiště je SQL databáze
  • mapperu zhruba odpovídá Nette\Database(\Table). Nebo Dibi pokud používáš starší knihovnu.
  • entita jako samostatný objekt neexistuje, ale s výsledkem dotazu se jako s entitou dá do určitě míry pracovat (je to opravdu jen přepravka na data)
  • repository je nejspíš to, čemu jsi říkal Model – reprezentuje jednu tabulku (resp. stará se o načítání jedné entity – v určitých případech může být entita rozložená třeba do dvou tabulek). Pokud nechceš žádné metody specifické pro danou tabulku (např. zmíněné findNewest($count), nebo u uživatele getByEmail($email)), tak ti samotné Nette\Database\Table stačí jako primitivní generický repozitář.
  • service/facade – vyšší vrstvy modelu, které už jsou od databáze odstíněné – získají entity z jednotlivých repozitářů a něco s nimi provádějí. Ale pro všechny CRUD operace používají repository.

A poslední dodatek:

yrow napsal(a):

… a nebo si předám v presentru ten model přes seter do Facade?

Presenter z toho úplně vynech, předávej to pomocí konstruktoru a fasádu vytvářej v DIC kontejneru (jako službu, zapiš ji do configu). Správný repozitář se jí pak dodá automaticky. V mém příkladu výše si presenter načte jen nějakou SendNewsFacade a hned ji může používat. Nepatří do odpovědnosti presenteru, aby ji taky správně vytvořil, to je odpovědnost DIC kontejneru. (Když mi to kdysi opravdu dělal presenter, tak byl plný závislostí které sám nepotřeboval a jen je předával. V tomto případě by si musel načíst ArticleRepository, EmailRepository a Mailer a presenter byl pak ošklivě nabobtnalý. A hlavně přidat pak jednu závislost – SendNewsFacade je strašně moc psaní.)

Editoval Šaman (11. 11. 2015 12:43)

yrow
Člen | 12
+
0
-

Šaman napsal(a):

Modelem je všechno, model je celá vrstva aplikace na kterou se odkazuješ z presenterů a většinou je to to hlavní, co programátor programuje (v presenterech to v ideálním případě jen prováže a nasype do šablon). Takže i repozitář, tabulka, fasáda, vše patří do modelu.

Znáš přednášku 5 vrstev modelu?
Sice už je starší, ale tohle by ti mohla docela objasnit. To, čemu David říká Facade je v přednášce Service a tomu, čemu ty asi říkáš Model je tam Repository.

Obecně jde o to, že repository je většinou jeden na tabulku a obsluhuje jí poměrně nízkoúrovňově (já tam mám třeba specifické metody jako (articles) findNewest($count) která vrátí požadovaný počet nejnovějších článků). Repository obsluhuje jednu entitu a vzhledem k principu jediné odpovědnosti by neměl dělat nic dalšího.

Zatímco fasád může být víc a už nemají tak těsný vztah k jednotlivým entitám. Jedna může třeba po vydání článku načíst všechny emaily (pomocí jednoho repozitáře), na které se má článek poslat (ten načte z jiného repozitáře), a provede rozeslání (pomocí služby typu IMailer). A SRP je zachovaný, protože tahle třída dělá konkrétní a jednu věc – rozesílá novinky. Na další práci se články budou další fasády/service.

P.S.
Když už jsem teda odkázal na starší článek, tak ještě upřesním jak to vypadá v Nette\Database

  • úložiště je SQL databáze
  • mapperu zhruba odpovídá Nette\Database(\Table). Nebo Dibi pokud používáš starší knihovnu.
  • entita jako samostatný objekt neexistuje, ale s výsledkem dotazu se jako s entitou dá do určitě míry pracovat (je to opravdu jen přepravka na data)
  • repository je nejspíš to, čemu jsi říkal Model – reprezentuje jednu tabulku (resp. stará se o načítání jedné entity – v určitých případech může být entita rozložená třeba do dvou tabulek). Pokud nechceš žádné metody specifické pro danou tabulku (např. zmíněné findNewest($count), nebo u uživatele getByEmail($email)), tak ti samotné Nette\Database\Table stačí jako primitivní generický repozitář.
  • service/facade – vyšší vrstvy modelu, které už jsou od databáze odstíněné – získají entity z jednotlivých repozitářů a něco s nimi provádějí. Ale pro všechny CRUD operace používají repository.

A poslední dodatek:

yrow napsal(a):

… a nebo si předám v presentru ten model přes seter do Facade?

Presenter z toho úplně vynech, předávej to pomocí konstruktoru a fasádu vytvářej v DIC kontejneru (jako službu, zapiš ji do configu). Správný repozitář se jí pak dodá automaticky. V mém příkladu výše si presenter načte jen nějakou SendNewsFacade a hned ji může používat. Nepatří do odpovědnosti presenteru, aby ji taky správně vytvořil, to je odpovědnost DIC kontejneru. (Když mi to kdysi opravdu dělal presenter, tak byl plný závislostí které sám nepotřeboval a jen je předával. V tomto případě by si musel načíst ArticleRepository, EmailRepository a Mailer a presenter byl pak ošklivě nabobtnalý. A hlavně přidat pak jednu závislost – SendNewsFacade je strašně moc psaní.)

Super díky moc, pěkná odpověď,

takže si do presenteru normálně injektnu pouze fasádu, (ta fasáda třeba vyžaduje dva repository) potřebné repository si fasáda obstará sama ve svém konstruktoru.
To jsem chtěl vědět. Že ta fasáda by tedy neměla být zároveň tím základním repository který obstarává data ani se to nepředává nějak jiank.

Šaman
Člen | 2666
+
+1
-

Přesně tak.

Ještě jedna myšlenka:
Jsou různě názory, jak přistupovat i k takhle jasně definované struktuře modelu. Někdo si dělá fasády i pro běžný přístup k entitám (idea je taková, že presenter dostane vždy jen fasády/service, nikdy repozitář), ale takové fasády většinou nedělají nic než delegují požadavek na repozitář. Pak jsou běžné fasády typu UserFacade, nebo ArticleFacade.

Já dávám přednost tomu, že pokud presenter potřebuje jen jednoduché CRUD operace, dostane přímo repozitář.
A fasády (dávám přednost označení service) řeší všechny požadavky, které potřebují víc závislostí, než ten jeden repozitář. A těch může být spousta a většinou se už nejmenují tak genericky. Takže třeba nikoliv UserService, ale třeba RegisterUserService (zaregistruje uživatele do db, vygeneruje mu heslo a odešle email), nebo ta SendNewsService.

Lépe se vyznává i ve větším počtu služeb, pokud bude každá dělat jen jednu věc, než hledat, jestli odesílání novinek je nacpáno do EmailFacade, nebo do ArticleFacade, nebo je to naprogramované přímo v presenteru.

Editoval Šaman (11. 11. 2015 17:17)

Oli
Člen | 1215
+
+1
-

zaregistruje uživatele do db, vygeneruje mu heslo a odešle na email

Chápu, že to je jen příklad, ale takhle ne. Nebo se ti tam to na dostalo omylem? :-)

Šaman
Člen | 2666
+
0
-

Oli napsal(a):

zaregistruje uživatele do db, vygeneruje mu heslo a odešle na email

Chápu, že to je jen příklad, ale takhle ne. Nebo se ti tam to na dostalo omylem? :-)

Odešle na email aktivační odkaz, nebo tak nějak :) Tohle byl jen příklad z hlavy, kam jsem chtěl nacpat víc, než triviální CRUD operaci. Osobně hesla zásadně negeneruji, ty si má nastavit uživatel sám. Když mu vygeneruji supersilné heslo, tak ho buď zapomene, nebo si ho nalepí na monitor a bezpečnost je v kelu :D

Ale radši jsem to upravil, aby to někdo tak opravdu neudělal :)

Editoval Šaman (11. 11. 2015 17:17)