Dnes bych chtěl napsat zamyšlení nad tím, jak ukládat konzistentně entitu do DB a zároveň obrázek nebo klidně i jiný soubor na disk. Aby to bylo jasnější, popíšu nějaký příklad, s kterým se určitě víceméně každý někdy setkal.
Představme si například situaci, kdy máme aplikaci, kde uživatel může vkládat článek a k němu ilustrační obrázek. Když si představíme ten proces, tak bude existovat pravděpodobně nějaký formulář, kde uživatel zadá titulek a obsah článku a přiloží ilustrační fotografii, která se bude zobrazovat ve výpisu článků. Po odeslání formuláře se provede validace a pokud projde, tak inicializujeme entitu, které nastavíme titulek a obsah. A teď zbývá vyřešit, jak uložíme fotografii. Fotografii nebudeme ukládat do databáze, ale určitě na disk, proto si do entity poznačíme název souboru, pod jakým bude fotografie na disku uložena.
A teď jsme právě na tom rozcestí. Jakým způsobem zajistíme, aby pojmenování fotografie bylo unikátní a zároveň nedošlo k tomu, že při uložení entity, která vyžaduje mít nastavený název souboru obrázku, nedošlo k tomu, že obrázek na disku uložený nebude, ale entita v databázi uložena bude a logicky tak dojde k nekonzistentnímu stavu.
Nyní ukážu některá řešení, jak jsem je dříve někde viděl použité a zmíním jejich nedostatky. Na závěr pak ukážu řešení, které se zdá být nejvíce bezpečné.
Prvně uložit entitu, potom obrázek
Tahle strategie je asi nejméně bezpečná. Spočívá v tom, že prvně uložíme entitu a po jejím uložení získáme ID entity. To poté použijeme k tomu, abychom pod ním uložili v adresáři daný obrázek a po uložení obrázku provedeme update entity tak, že nastavíme cestu k obrázku.
Nevýhoda je jasná, když dojde k selhání uložení obrázku, tak entita bude sice uložena v databázi, ale obrázek k ní bude chybět. Tak samo, pokud dojde k selhání aktualizace entity, budeme mít stejný problém. Na druhou stranu, když se vše povede, budeme mít unikátní název obrázku.
Prvně uložit obrázek, potom entitu
U této strategie naopak budeme ukládat obrázek dříve, než entitu, takže pokud se nepodaří uložit obrázek a dojde k problému, neuložíme ani entitu. Na druhou stranu, je zde nutné vyřešit problém s unikátním pojmenováním obrázku. Můžeme použít různé strategie:
- pomoci hashovací funkce získáme hash, ale ten sám o sobě nezaručuje, že nemůže existovat kolize (tj. že dva různé obrázky budou mít stejný hash)
- k hashovací funkci můžeme přiřetězit časovou značku, která šanci kolize ještě výrazněji sníží
- pro jistotu se můžeme podívat ještě na disk, zda obrázek s daným jménem neexistuje a pokusit se tak ještě více vyhnout kolizi
Suma sumárum, ve všech případech dostaneme prakticky unikátní pojmenování. Obrázek uložíme pod “unikátním” označením a označení si uložíme do entity. Entita se uloží a s vysokou pravděpodobností bude konzistentní.
Nevýhodou v tomto případě může být, že pokud dojde k chybě při uložení entity, tak nám na disku může zůstat obrázek, ke kterému nevede reference. To je ale možné vyřešit nějakým skriptem, který by pravidelně prošel disk a obrázky nemající žádnou referenci, by odstranil.
Druhá nevýhoda může být přehlednost. Ne vždy je úplně pohodlný nemít v pojmenování obrázku systém a systém často tvoří právě ID entity, případně v kombinaci s dalšími identifikátory, jako je třeba EAN apod. Upřímně, mi osobně se nechce procházet složky s obrázky obsahující soubory pojmenované nic neříkajícími značkami.
Uložíme obrázek do pracovní složky, uložíme entitu, přesuneme obrázek, aktualizujeme entitu
Nyní si představíme strategii, která se jeví jako nejbezpečnější. U této strategie postupujeme tak, že daný obrázek uložíme dočasně do nějaké pracovní složky. Poté spustíme transakci nad databází (BEGIN), abychom zajistili atomičnost celé operace. Samotné uložení obrázku do transakce nezahrnujeme, protože chceme, aby transakce byla co nejkratší. Poté uložíme entitu a získáme její ID. Provedeme přesun a přejmenování obrázku do regulérního adresáře a aktualizujeme entitu (pokud bychom nechtěli ukládat název obrázku, který má odvozený formát z ID entity k entitě, pak aktualizaci nemusíme provádět). Na závěr pak provedeme potvrzení databázové transakce (COMMIT). Pokud během transakce dojde k jakémukoliv problému, můžeme vyhodit výjimku a vrátit změny (ROLLBACK).
U této strategie máme jak vždy konzistentní entitu, tak i konzistentní adresář, kam obrázky ukládáme. Pojmenování obrázku je zároveň vždy logické a může odpovídat našemu systému dle libosti.
Shrnutí
Strategie, které jsem zde zmínil spíš slouží k zamyšlení, než že bych chtěl hrdě prohlašovat něco ve smyslu, že pouze ta či ona strategie je jediná správná. Volba strategie často závisí na konkrétní aplikaci a na tom, jak je daná funkcionalita kritická a taky, o jakém množství obrázků (obecně souborů) bychom se bavili. Kupříkladu je rozdíl, pokud eshop pracuje s desítkami tisíc souborů, které často načítá v dávkách a když si někdo na blogu sem tam vloží příspěvek s ilustrační fotografií.
Každopádně ocením jakýkoliv komentář k tomu, jakou vaši strategii používáte při vašem use casu zrovna vy.
Na závěr přidám ještě odkaz na zajímavé rozšíření k Doctrine, které pomáhá s persistencí souborů u entit.