Autor Zpráva
Johny.5
Profil
Ahoj,
potřebuju exportovat a importovat data z cizí databáze na svůj web.
Mám třeba články, komentáře,...
Cizí databáze má jinak pojmenované sloupce než potřebuju já.
A teď mi jde o co nejčistější návrh tříd.

Vytvořím si tedy třídy Article, Comment,.. - do nich budu vkládat data pro export a zároveň z nich načítat importovaná data.
Pak potřebuju např. setArticle() a getArticle() pro uložení a načtení ze vzdálené databáze. To je jiná třída (a zvlášť pro články, zvlášť pro komentáře).
Potřebuju převést data při ukládání z objektu do pole, nemůžu jen přetypovat, ale potřebuju je přejmenovat. Pro přehlednost asi oddělit do zvláštní třídy. Kam je nejlepší ale umístit definice převodu (konstanty s názvy sloupců)? Do této třídy a nebo přímo do Article? Kam umístit třeba konstantu s názvem tabulky ve vzdálené databázi? Všechno na jedno místo a nebo něco tam a něco jinam?

Klidně to po mně předělejte znovu od úplného začátku. Stačí jenom schématicky nebo popsat v pár větách. Děkuji.
Alphard
Profil
A fakt se to liší jen názvy sloupců, jinak je to 1:1 jedné? Trochu bych se divil, ale kdyby to tak bylo, nebylo by třeba vytvářet konkrétní třídy Article, Comment, ale stačila by jedna obecná a mapa.
Johny.5
Profil
Napsal jsem to trochu zjednodušeně, ty sloupce odpovídají z 95%, občas se něco musí při tom převodu upravit, spojit apod.
To už ale není z hlediska návrhu podle mě až tak podstatné.
Alphard
Profil
Podle mě zásadní, na tom závisí volba, jestli se budou vytvářet konkrétní implementace pro jednotlivé tabulky, nebo jen mapy. Když je reálný důvod vytvořit konkrétní implementaci pro každou tabulku, je nejvhodnější místo pro umístění názvu tabulky a mapy sloupců právě daná třída.
Johny.5
Profil
Ano, musí být konkrétní implementace. Na 100%.

Je to umístění do Article ale správně? Je to jenom úložiště dat, které samotné vůbec neřeší nějakou vzdálenou databázi.. V podstatě návrhový vzor repository.
Alphard
Profil
O jak velkou databázi (počet tabulek a objem dat) jde?
Abych pravdu řekl, já bych se hned nehnal do této velké vlastní implementace, ale zvážil bych, že si vytvořím zrcadlovou databázi (1:1, pro export a import lze využít standardní nástroje) a z této zrcadlové databáze budu přesypávat data do té své pomocí SQL dotazů.
Johny.5
Profil
Těch tabulek je řádově třeba 20. Záznamů jsou tisíce, jsou tam vazby mezi tabulkami, i když hodně zprasené.
Article a Comment byl jen příklad, abych nemusel vysvětlovat souvislosti. Ve skutečnosti je to nějaké CRM běžící na MSSQL. A já používám MySQL a Doctrine.
Z důvodu dalších úprav bych rád minimálně každý logický celek oddělil zvlášť (např. report jako záznam + položky reportu)

Pro mě je tedy ideální, když vstupem i výstupem bude vždycky instance konkrétní třídy, se kterou budu moct dál pracovat.

Pracuju na tom celý den a neustále to upravuju a přepisuju a už sám nevím, co je správně. Takže bych spíš jen potřeboval nakopnout / potvrdit, že je to dobře tak a tak a nebo to dělám celé úplně špatně.
Alphard
Profil
Tak jak to teď chápu je to složitější než se zdálo, v tuto dobu bych se do také diskuse nepouštěl :-) Nevím, jak tam jsou řešené závislosti a services. Buď má samotná třída Article závislost na databázi a pak by zřejmě měla sama obsahovat data a metody load() a save(). Nebo třída Article čistě reprezentuje článek a je ukládána pomocí nějaké repository třídy, např. $articles->saveArticle($article), to je obecnější návrh a samozřejmě významně mění situaci.
Já tady přes den nebývám, třeba zítra někdo odpoví detailněji.
Tori
Profil
Johny.5:
A ten import z cizí DB budete provádět opakovaně (tedy kromě insertů i update existujících dat), anebo jde o jednorázovou transformaci z jednoho schématu do druhého?
Johny.5
Profil
Tori: import bude pravidelný (cronem spouštěná aktualizace dat z CRM)

Nechtěl jsem psát komplet řešení, že třeba uslyším rovnou lepší návrh, ale tady je.

Article je entita. Neměnný objekt, na který se dá "spolehnout" i třeba v případě aktualizace CRM, kdy se změní nějak struktura DB (ne, že bych si to přál)
Kromě toho v MSSQL je spousta sloupců, které se aktuálně nenačítají, jsou tam hotnoty NULL a v praxi se s nimi u mně vůbec nepracuje. Takže jim nastavím výchozí hodnotu na NULL. Když se ale později klient rozhodne, že chce to a to používat, jednoduše použiju už připravený getter / setter.

ArticleRepository je třída, která se mi stará o manipulaci s daty (saveArticle / getArticle / getArticles / getArticlesSince($date)...).
Správně by měl být ještě asi mapper, jenže tady to jaksi ztrácí smysl, když je to psané pro jednu konkrétní MSSQL databázi, klient určitě nepřejde na MySQL, PgSQL apod.. Takže tím bych nedělal nic jiného, než opakoval ty samé metody..

Pak potřebuju 2 funkce, které mi převedou objekt Article na array pro vložení do MSSQL a to samé v opačném směru. Jak jsem říkal, během toho převodu jsou občas i drobné úpravy. V praxi to znamená vzít v poli položku po položce a ručně to nastavit. Tohle bych asi oddělil do zvláštní třídy a použil 2 statické metody.

A teď kam s nastavením sloupců a názvů tabulky z MSSQL? Nechci to dávat do žádné konfigurace, ale taky to nechci mít rozstrkané na 10 místech.
Entita je jenom obálka na data, ta je na tom nezávislá, takže je asi ze hry venku. (Bylo by to tam hezky na jednom místě, ale jinak to postrádá logiku).
V repository potřebuju znát primární klíč (jednou je to ID, jindy id, pak zase Id - autor to zrovna neřešil), název tabulky a případně sloupce, kam se ukládá datum poslední změny.
V té třídě pro převod potřebuju znát názvy všech sloupců.
Tohle nechci mít přímo inline v kódu, kdyby se měnila náhodou DB, abych to pak neměnil na více místech (a taky pro přehlednost, kdyby to po mně převzal někdo jiný).

Je lepší to všechno dát do té převodní třídy? Do repozitáře? A nebo na to použít úplně speciální třídu např. ArticleSettings, která bude sloužit jen pro nastavení těchto konstant?
Tori
Profil
Johny.5:
Pak potřebuju 2 funkce, které mi převedou objekt Article na array [...] a to samé v opačném směru.
Můžou být součástí nějaké abstraktní BaseEntity (toArray a statická createFromArray), potomci (Article,...) si to buď implementují podle vlastních proměnných, anebo můžete mít proměnné pro DB v nějakém poli (private Article.data) a getAsArray si je vezme odtamtud (= odpadá extra implementace u potomků, gettery/settery by se daly automatizovat přes magické metody).

A teď kam s nastavením sloupců a názvů tabulky z MSSQL?
Neměly by tohle řešit právě mappery? (Neříkám že musí, ptám se. Prostě nějaká mezivrstva, nad ní by se pracovalo jen s vaším datovým modelem, pod ní by byly konkrétní SQL dotazy pro danou DB, přičemž odlišně navržená DB se může taky vnímat jako by to byla úplně jiná DB. Kdybyste změnil svou DB, tak se změní i entity, pokud by se změnila cizí DB, tak jen upravíte tu mezivrstvu.)
Johny.5
Profil
Tori:
Neměly by tohle řešit právě mappery?
Asi ano, ale... Dává ten mapper praktický smysl? Měl by obecně význam v situaci, kdy používám memcached, ale byla by možnost použít i Redis. Tady to nemá jinou funkci, než že v repository zavolám metodu save() z mapperu. A v mapperu je ta samá metoda save() s těmi samými parametry, která teprve něco provede (předá výsledek zpátky repository a z repository se to dalším krokem dostane ke mně).

Uložím-li to nastavení do mapperu, budu k tomu potom přistupovat z entity, kde bych si třeba vytvořil toArray() / createFromArray()? V entitě bych potřeboval asi tak 90% z těch názvů sloupců, zbytek v mapperu (nebo repository, viz výše). Nedává potom větší smysl vytvořit si opravdu ArticleSettings, v ní konstanty a hodnoty všude načítat přes ArticleSettings::TABLE_NAME apod?
Tori
Profil
Johny.5:
Uložím-li to nastavení do mapperu, budu k tomu potom přistupovat z entity [...]? V entitě bych potřeboval asi tak 90% z těch názvů sloupců, zbytek v mapperu (nebo repository, viz výše). Nedává potom větší smysl vytvořit si opravdu ArticleSettings, v ní konstanty a hodnoty všude načítat přes ArticleSettings::TABLE_NAME apod?
Tím jsme zpátky u zásadní otázky [#2]. Pokud se databáze liší jen názvy sloupců, tak klidně mapa. Pokud jsou rozdíly větší, tak bych použila nějakou mezivrstvu, která v případě entity Article vyrobí dotaz 1:1, v případě Category přeloží některé názvy sloupců, a v případě User třeba čte ze data z úplně jiné tabulky nebo z několika tabulek (a aliasuje sloupce v SQL dotazu, aby nadřízená vrstva dostala pokaždé jen ty očekávané).

Jestli to má překládat repository nebo extra mapper, to nevím. Možná by šlo udělat i nějakou továrničku pro tuhle mezivrstvu:
class BaseMapper
{
    public static function create($name)
    {
        $class = $name.'Mapper';
        return (class_exists($class) ? new $class : new self);
    }
 
    public function find(array $condition)
    {
        return $this->database->select('*')->from($this->table)->where($condition);
    }
}

// ArticleMapper neexistuje, shodné DB schéma i názvy sloupců, použijí se tedy výchozí metody z BaseMapperu

// ale UserMapper už překládá dotazy, takže si implementuje vlastní verzi metody:
class UserMapper extends BaseMapper
{
    public function find(array $condition)
    {
        $where = $join = [];
        if (isset($condition['isAdmin'])) {
            $join[] = ['userRoles', '`user`.`id` = `userRoles`.`userId`'];
            $where[] = ['`userRoles`.`role` '.($condition['isAdmin'] ? '=' : '!=').' %s', 'admin'];
            unset($condition['isAdmin']));
        }
        return $this->database->select('*')->from($this->table)->join($join)->where(array_merge($where, $condition));
    }
}
Předpokládám, že DB vrstva by byla v obou případech stejná (např. dibi). Nejsem si jistá, jestli z hlediska objektového návrhu modelu je to takhle košer, takže to berte spíš jako podnět do diskuse než kvalitní návrh.
Johny.5
Profil
Tori:
Děkuju za komentáře a postřehy, mám teď nad čím přemýšlet. Problém je, že většina lidí se tomu vysměje, že je to úplně jednoduché a jasné, ale přitom by to každý řešil úplně jinak a když se zeptám, jestli by to nešlo jinak, tak znejistí :-)

Pokud mi ještě někdo jiný poradí ze svého pohledu čisté řešení, budu vděčný.
Tori
Profil
Johny.5:
Taky záleží, jestli to celé má sloužit jen k synchronizaci těch DB, nebo to chcete i používat na své straně normálně v aplikaci. Kdyby šlo jen o synchronizaci, tak bych asi zvažovala i možnost napsat si jednoduchý skript, který projde cizí DB, vygeneruje z ní SQL dotazy pro updaty, a ty potom spustí na mé DB.

Vaše odpověď

Mohlo by se hodit


Prosím používejte diakritiku a interpunkci.

Ochrana proti spamu. Napište prosím číslo dvě-sta čtyřicet-sedm: