Modely pro práci s databází

Rudolf Svátek 2017-08-18 13:28

V naší aplikaci máme 4 databázové tabulky. Vytvoříme si tedy 4 složky, do kterých budeme dávat třídy pro manipulaci s daty. Půjde třeba o vkládání dat, mazání, aktualizaci, výběry podle různých kritérií apod. Vytvoř tedy složky

  • app\model\Clients
  • app\model\PasswordTypes
  • app\model\Passwords
  • app\model\Users

Abychom nemuseli pokaždé ručně psát názvy tabulek na různých místech, vytvoříme si třídu, která bude obsahovat jen definice názvů tabulek. Pokud by se v budoucnu stalo, že se název tabulky změní, stačí jej přepsat na jednom místě.

Soubor se bude jmenovat app\model\TableNames.php a jeho obsah je:

namespace {

    /**
     * Class TableNames
     */
    class TableNames
    {

        /** @var string */
        public static $viewPrefix = 'vw';

        /** @var string */
        public static $clients = 'clients';

        /** @var string */
        public static $users = 'users';

        /** @var string */
        public static $passwordTypes = 'password_types';

        /** @var string */
        public static $passwords = 'passwords';
    }
}

Výběr všech záznamů

Jako příklad si vezmeme tabulku uživatelů. Pro výběr všech záznamů stačí jednoduchý příkaz select '*'. Třída sice vypadá složitě a je to dost psaní, ale jistě to pochopíš:

<?php
namespace App\Model\Users;

use Dibi\Connection;

class GetAllUsers
{

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

    /**
     * GetAllUsers constructor.
     *
     * @param Connection $db
     */
    public function __construct(Connection $db)
    {
        $this->db = $db;
    }

    /**
     * Method get
     *
     * @return \Dibi\Fluent
     */
    public function get()
    {
        return $this->db->select('*')
            ->from(\TableNames::$users);
    }
}

Všimni si, že tam používám třídu \TableNames pro identifikaci názvu tabulky s uživateli. Možná se ptáš, proč na to jdu tak složitě, že pro výběr všech záznamů píšu zvlášť třídu. Stačilo by mít nějakou Base třídu a tam metodu SelectAll() a tu pak jen dědit. Mně se ale zalíbil princip Single Responsibility. Ten říká, že jedna třída má dělat jen jednu věc. Takže pokud chci všechny záznamy z tabulky uživatelů, budu na to mít jednu třídu a na získání všech záznamů z tabulky klientů zase další třídu. Líp se to pak spravuje, když něco nefunguje.

Zřejmě to není potřeba, ale malý popis třídy: v konstruktoru získám připojení k databázi a je tu jen jedna metoda get(), která vykoná jeden dotaz a vrátí výsledek. Pracujeme ve jmenném prostoru \App\Model\Users.

Podobně pokud chci získat detail uživatele podle jeho ID, vytvořím třídu GetUserDetail:

<?php

namespace App\Model\Users;

use Dibi\Connection;

class GetUserDetail
{

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

    /**
     * GetUserDetail constructor.
     *
     * @param Connection $db
     */
    public function __construct(Connection $db)
    {
        $this->db = $db;
    }

    /**
     * Method get
     *
     * @param int $id
     *
     * @return \Dibi\Row|FALSE
     */
    public function get($id)
    {
        return $this->db->select('*')
            ->from(\TableNames::$users)
            ->where('id = %i', $id)
            ->fetch();
    }
}

Znovu zdůrazním že vím, že to jde napsat úsporněji, ale já nechci :-D.

A tak pořád dokola. Jakmile zjistím, že potřebuji po databázi nějakou akci, vytvořím si na to třídu a tam to vyřeším. Výhora Single Responsibility přístupu je hlavně v tom, že do hotových a funkčních věcí se vůbec nemusí hrabat a cokoli nového mi nemůže zbořit už odladěnou věc. Taky si nemusím spousty zbytečných věcí dědit. Na začátek je to sice víc psaní, ale vyplatí se to později.

Výběry jsou jednoduché - databáze buď něco vrátí, nebo taky ne. Ale jakmile máme snahu něco do databáze vložit, můžou nastat komplikace - konflikt unikátních indexů je nejčastější. Musíme proto vkládání dat ošetřit pomocí výjimek.

Pravidlo říká, že v jednom souboru by měla být definována jen jedna třída. A taky je dobré vyhazovat výjimky z vlastního jmenného prostoru a nespoléhat se, na knihovny frameworku. Výjimky jsou v zásadě dvojího typu:

  • Runtime Exceptions, které vlastně nemůžeme nijak ovlivnit. Například spadne databázový server, někdo smaže soubor s konfigurací apod.
  • Logic Exceptions, které můžeme brát v úvahu a ty představují nesprávné použití naší třídy. Třeba špatný typ předávaného parametru

Vyrobíme si tedy 2 soubory, kdy každý bude obsahovat jeden typ výjimky.

<?php
namespace App\Model\Users;

class RuntimeException extends \RuntimeException
{
}

a

<?php
namespace App\Model\Users;

class LogicException extends \LogicException
{
}

Všimni si, že obě třídy pracují ve jmenném prostoru App\Model\Users. Taky každá dědí od obecné PHP výjimky.

Teď už jem musíme ošetřit různé situace, kdy by mohla aplikace zkolabovat. V případě insertu je to třeba konflikt unikátních klíčů. V tabulce Users máme nastavena 2 pole jako unikátní - username a email. Když se pokusíme vložit uživatele 'admin' (kterého už v databázi máme), aplikace skončí na výjimce.

Zkusíme si tedy vytvořít třídu pro insert uživatele. Zatím ji neošetříme:

<?php

namespace App\Model\Users;

use Dibi\Connection;

class InsertUser
{

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

    /**
     * InsertUser constructor.
     *
     * @param Connection $db
     */
    public function __construct(Connection $db)
    {
        $this->db = $db;
    }

    /**
     * Method insert
     *
     * @param \Dibi\Fluent $data
     */
    public function insert($data)
    {
        $this->db->insert(\TableNames::$users, $data)
            ->execute();
    }
}

Při pokusu o vložení uživatele 'admin' dostaneme hlášku:

To je pro ladění sice fajn, ale lepší je mít to úplně pod kontrolou. Vidíme, že Dibi má problém s provedením příkazu. Ale spoléhat se na to, že tam tato výjimka bude i po aktualizaci není dobré. Lepší je vytvořit si vlastní výjimku, která ošetří duplicitní klíč:

<?php
namespace App\Model\Users;

class UniqueConstraintViolationException extends RuntimeException
{
}

Opět se to děje ve jmenném prostoru App\Model\Users. Jde o situaci, kterou programátor ovlivnit nemůže - uživatel aplikace prostě zvolí již obsazené jméno. Jde proto o RuntimeError a tak naše výjimka dědí od App\Model\Users\RuntimeException.

No a kód metody pro insert upravíme takto:

    /**
     * Method insert
     *
     * @param $data
     *
     * @throws UniqueConstraintViolationException
     * @throws UnknownException
     */
    public function insert($data)
    {
        $this->db->begin();
        try {
            $this->db->insert(\TableNames::$users, $data)
                ->execute();
            $this->db->commit();
        } catch (\Dibi\UniqueConstraintViolationException $e) {
            $this->db->rollback();
            throw new UniqueConstraintViolationException($e->getMessage());
        } catch (\Dibi\Exception $e) {
            $this->db->rollback();
            throw new UnknownException($e->getMessage());
        }
    }

Dáme pokus o vložení do try catch bloku. pokud se vložení nepovede kvůli duplicitnímu klíči, vyhodí se výjimka z našeho jmenného prostoru. Použil jsem i transakce, takže kdyby bylo vkládání složitější a skládalo se z několika operací, rollback vrátí všechno zpátky jak to bylo před pokusem o vložení.

Může taky nastat jakákoli další výjimka, tak ji odchytávám jako \Dibi\Exception a vyhazuji vlastní UnknownException.

Snad to takto stačí jako inspirace. Možná si troufneš na napsání třídy pro update záznamů včetně ošetření možných výjimek. V aplikaci asi budeme potřebovat vybírat uživatele i podle username nebo emailu. Principy zůstávají stejné - pro každou akci zvláštní třída.

Věřím tomu, že modely pro ostatní databázové tabulky si už napíšeš ;-).

Pokud ale váháš, stáhnout si je můžeš tady: modely pro práci s databází

Redakční systém RS::RS Předchozí kapitola

Stažení sandboxu Nette

Redakční systém RS::RS Celý seriál

Aplikace na správu hesel

Redakční systém RS::RS Následující kapitola

Výpis dat v gridu

Komentáře (0)

Přidej svůj komentář

O mně

Jmenuji se Rudolf Svátek. Jsem lektor výpočetní techniky a PHP programátor. Stavím firemní stránky a eshopy. Aby se mi to dělalo pohodlně, vytvořil jsem redakční systém RS::RS, který ti tu nabízím k použití.

Rychlý kontakt na mně

  • Rudolf Svátek
  • Telefon:
    +420 777 828 353
  • Email:
  • Adresa:
    Josefa Hory 1097/5
    736 01 Havířov
    ČR



Tyto stránky používají Cookies. Používáním stránek s tím souhlasíte Další informace