Ukládání souborů do databáze

Školení, která pořádám

Ukládání souborů do databáze může někdy dávat dobrý smysl – jednak se k takto uloženým souborům snadno řídí přístup (i když to jde zajistit i s obyčejnými soubory). Druhý argument přijde ke slovu v momentě, kdy je databáze replikovaná a stejným způsobem chceme replikovat i datové soubory.

Pro přístup k takto uloženým souborům se dá vytvořit díky funkci stream_wrapper_register abstrakce:

<?php
/** Třída pro použití funkcí stream_wrapper_register() zapisující do databáze
* @copyright Jakub Vrána, https://php.vrana.cz/
*/
class StreamDB {
    static $table = "tab", $id_col = "id", $data_col = "data";
    protected $id, $data = "", $position = 0;
    function stream_open($path, $mode, $options, $opened_path) {
        $url = parse_url($path);
        $this->id = intval($url["host"]);
        if ($mode == "wb") {
            if ($this->id) {
                mysql_query("INSERT INTO " . self::$table . " (" . self::$id_col . ") VALUES ($this->id) ON DUPLICATE KEY UPDATE " . self::$data_col . " = ''");
            } else {
                mysql_query("INSERT INTO " . self::$table . " () VALUES ()");
                $this->id = mysql_insert_id();
            }
        } elseif ($mode == "ab") {
            mysql_query("INSERT INTO " . self::$table . " (" . self::$id_col . ") VALUES ($this->id)");
        } else {
            $row = mysql_fetch_row(mysql_query("SELECT " . self::$data_col . " FROM " . self::$table . " WHERE " . self::$id_col . " = $this->id"));
            if (!$row) {
                return false;
            }
            $this->data = $row[0];
        }
        return true;
    }
    function stream_close() {
        $this->data = "";
    }
    function stream_read($count) {
        $this->position += $count;
        return substr($this->data, $this->position - $count, $count);
    }
    function stream_write($data) {
        mysql_query("UPDATE " . self::$table . " SET " . self::$data_col . " = CONCAT(" . self::$data_col . ", _binary'" . mysql_real_escape_string($data) . "') WHERE " . self::$id_col . " = $this->id");
        return strlen($data);
    }
    function stream_eof() {
        return ($this->position >= strlen($this->data));
    }
    function stream_flush() {
    }
    function stream_stat() {
        return array("id" => $this->id, "size" => strlen($this->data));
    }
    function unlink($path) {
        $url = parse_url($path);
        mysql_query("DELETE FROM " . self::$table . " WHERE " . self::$id_col . " = " . intval($url["host"]));
        return mysql_affected_rows();
    }
    function rename($path_from, $path_to) {
        $url_from = parse_url($path_from);
        $url_to = parse_url($path_to);
        return mysql_query("UPDATE " . self::$table . " SET " . self::$id_col . " = " . intval($url_to["host"]) . " WHERE " . self::$id_col . " = " . intval($url_from["host"])) && mysql_affected_rows();
    }
    function url_stat($path, $flags) {
        return array();
    }
}

stream_wrapper_register("db", "StreamDB");
?>

S takovouto abstrakcí se dá se soubory uloženými v databázi pracovat v podstatě stejně jako s normálními soubory:

<?php
copy("1.jpg", "db://1");
rename("db://1", "db://2");
copy("db://2", "2.jpg");
unlink("db://2");
?>

Funguje dokonce třeba i funkce imagecreatefromjpeg, naproti tomu ale např. imagejpeg ne (protože používá VCWD_FOPEN místo php_stream_open_wrapper).

Jakub Vrána, Řešení problému, 25.8.2008, diskuse: 27 (nové: 0)

Diskuse

Jan Kodera:

Jak se to bude chovat s hodně velkými soubory? Aspoň dle kódu to vypadá, že se celý soubor načte do objektu a tedy do paměti a odtud se streamuje. Nebylo by lepší streamovat na urovni mysql? Tedy v tom selectu?
Myslím, tím přes klauzuli LIMIT.

Sice by to znamenalo více dotazů do MySQL ale pak by nebyl vůbec žádný problém s velikostí souborů.

ikona Jakub Vrána OpenID:

Pokud má být jeden soubor v jednom řádku, tak by se to dalo zajistit nikoliv přes LIMIT, ale pomocí funkce SUBSTRING().

Jan Kodera:

Pravda :) Ten limit je blbost.

Miroslav Suchy:

Do databaze nema smysl ukladat velke soubory (vetsi jak 1MB):
http://suchy.blog.root.cz/2007/02/08/to-blob-or-not-to-blob/
A jedno mega obvyklke neni problem nacist cele do pameti.

Robert Vlach:

Jakube díky za zajímavý námět. Mohl bys stručně nastínit, jaké jsou hlavní nevýhody ukládání souborů do databáze?

Miroslav Suchy:

Pro malé objekty ( < 256kB) je rozhodně lepší ukládat do DB. Pro velké objekty ( > 1MB) je rozhodně lepší ukládat do FS. Mezi tím je mlhavá hranice a silně záleží na druhu aplikace. Pokud se data často přepisují, tak je lepší spíše FS (lépe se vyrovnává s fragmentací). Pokud se data nepřepisují, tak je lepší DB (větší propustnost malých objektů).

Vojta Kohout:

Jakube, díky za velice zajímavou myšlenku a opravdové rozšíření obzorů. Proč se ale zapojuji do diskuze... Dost často slýchám nejrůznější argumenty při diskuzích na téma ukládat/neukládat binární data do databáze a na zvláštní, pro mě dosud nevyřešený problém jsem narazil při práci s obrázky. Tím je otázka kešování. Když pro přístup k obrázku použiji skript (řekněme s koncovkou .php), ať už posílám klientům (prohlížečům) sebesnaživější hlavičky o tom, že data zaslaná v odpovědi mají kešovat, že naposledy byly změněny tehdy a tehdy... někteří je ne a ne poslechnout a při novém požadavku o daný soubor ani v nejmenším neuvedou v požadavku hlavičku If-Modified-Since, If-None-Match, zkrátka nic. Podotýkám, že některé prohlížeče serverem zaslané hlavičky poslouchají, jiné ne. A ti druzí, bohužel, tupě při každém refreshi tahají ze serveru tučné kilobajty dat... Připadá mi, že některé prohlížeče vůbec nepředpokládají, že by výsledkem běhu skriptu mohl být kešovatelný obsah... Tohle pohřbilo myšlenku použití elegantních binárních struktur v databáze hned u několika našich projektů. Existuje řešení, jak spolehlivě přesvědčit prohlížeče, aby výstup toho či onoho php skriptu kešovali? Jediná asi zaručená metoda, která mě napadla, vedla přes mod_rewrite, kdy by si klient opravdu myslel, že žádá o například .jpg soubor... Ale u ní mám strach z přílišné zamotanosti, kdy bych pak chtěl například obrázky související s CSS mít uložené na disku. Snad jen doplňovat název souboru v URL nějakým prefixem, např. /db_image1.jpg překládat na /image.php?id=image1.jpg, kdy by mod_rewrite přepisoval jenom obrázky začínající na zmíněným prefixem... Ale pořád mi to připadá strašně "nehezký". Díky moc za případné reakce :).

ikona Jakub Vrána OpenID:

Mluv prosím konkrétně - uveď všechny hlavičky, které ze serveru posíláš, a uveď, kdo je nerespektuje.

Já mám zkušenost s posíláním kešovacích hlaviček dobrou a zcela nezávislou na koncovce souboru. Jediný problém je s Operou, která se zdráhá kešovat adresy obsahující otazník, to ale není tvůj případ.

A ještě jedna věc - HTTP kešování se používá kvůli situaci, kdy se daný objekt má znovu získat přirozenou cestou (tedy že návštěvník znovu přijde na danou stránku nebo že se mu má zobrazit stejný obrázek). Vynucený refresh v prohlížeči keš pochopitelně respektovat nemusí.

Vojta Kohout:

Tak jsem na Tvůj popud ještě jednou vše prošel a vypadá to, že problém je rozlousknut. Ten otazník v URL právě můj případ je (plyne to i z toho, co jsem psal), typická problémová URL v minulosti vypadala například "image.php?id=neco.jpg&w=100&h=50". Díky za to info o Opeře, protože ta mě opravdu mátla (používá ji kolega a přesně na jeho popud jsme tenhle problém řešili). Výsledek dnešního šetření: vše je v pořádku v IE a Firefoxu, Opera ve starší verzi problematická, po upgrade na poslední verzi rovněž v pořádku. Vše funguje s pouhým zasláním a anaylzováním hlavičky Last-Modified. Tak mi promiň tu lehkou mystifikaci o příspěvěk výš :).

pojízdná kočka:

Díky za nastínění problému. Nenahrává si snad Opera tyto soubory podle atributu <img src= ? Ve windblows se potom názvy s otazníkem buď neuloží vůbec nebo uloží bez otazníku.
Poučena diskuzí zrovna něco takového řeším, a obrázky
1. bych odkazovala např. <img src="image3098.jpg" alt=""/>,
2. do .htaccess bych někam výše přidala řádku:

RewriteRule ^image(\-?[0-9]+) image.php?id=$1

3. vytvořila bych si skript image.php, který by načetl data ze záznamu s ID = (int)$_GET['id'] a poslal by je na výstup.

LuKo:

Možná hloupý dotaz, ale když do databáze uložím všechny ikonky o velikosti od pár bajtů až po jednotky kB, bude přístup k nim rychlejší, než by byl přes klasický přístup na disk? Nebude na škodu podstatně vyšší počet dotazů do DB? Budou sice krátké, ale bude jich hodně. Nebude to spíš na škodu?

Oldis:

zalezi co s nimi chces delat, pokud jsou soucasti stranky( pokud by byli v db tak bys je musel nacist(coz by slo pri trose duvtipu jednim dotazem, a nacacheovat je na disk, pokud do html budes davat cesty k nim, pak je druha moznost do html vlozit volani scriptu ktery obr vraci podle id, to zase povede k narustu vykonanych scriptu), tak si myslim ze je lepsi je nechat ve FS, pokud je budes nabizet ke stazeni tak bude lepsi je mit v DB. ALE hodne zalezi na konretnim pripadu

LuKo:

Jedná se o klasickou grafiku ve stránce - ikonky layoutu, náhledy obrázků v textu článku. Tzn. veškeré jpg/gif/png obrázky o velikosti do jednotek kB. Velké fotky nemá smysl ukládat do DB, tam se nějaká ta milisekunda při přístupu na disk ztratí. Jde mi právě o malé obrázky ve stránce - jsou mrňavé a jsou jich ve stránce často desítky.

ikona David Grudl:

Bude potřeba dost programátorské zručnosti a bude to mnohem pomalejší. Proč dělat věci jednoduše, když to jde složitě, že? ;)

LuKo:

Právě, že mě výše nalinkované výsledky testů trochu překvapily a proto se tu (možná hloupě) ptám, zda se vyplatí obrázky přesunout do databáze. Po technické stránce by to nemuselo být tolik složité, u obrázku evidovat i čas poslední změny a to pří požadavku na soubor kontrolovat proti hlavičce If-Modified-Since a v závislosti na tom posílat příslušné hlavičky. Je pravda, že toto všechno za mě dělá Apache automaticky, ale pokud se umístění obrázků do DB po výkonnostní stránce vyplatí, tak proč ne? ;-)

ikona David Grudl:

Varianta filesystem: klient požádá o soubor, Apache jej přečte z disku a pořeší hlavičky.

Varianta database: klient požádá o soubor, Apache přečte z disku soubory s PHP skripty, ty se přeloží a vykonají, hlavičky pořeší pomalejší interpreter, připojí se k databázi, načte záznam a odešle ho

Druhé řešení musí být přece vždy pomalejší.

Martin:

Mel jsem za to, ze se porovnavaji varianty:
Varianta filesystem: klient požádá o soubor, Apache přečte z disku soubory s PHP skripty, ty se přeloží a vykonají, hlavičky pořeší pomalejší interpreter, skript najde soubor na disku, odešle ho

Varianta database: klient požádá o soubor, Apache přečte z disku soubory s PHP skripty, ty se přeloží a vykonají, hlavičky pořeší pomalejší interpreter, připojí se k databázi, načte záznam a odešle ho

ikona David Grudl:

Pokud jde o "ikonky o velikosti od pár bajtů, klasickou grafiku ve stránce...", tak není důvod, proč by mělo ve variantě "filesystém" ke slovu přistupovat PHP. Samozřejmě pokud by se porovnávaly tvé varianty, bylo by to docela o něčem jiném.

LuKo:

Já vycházel z informace o pár příspěvků výše:

Miroslav Suchy:
Pro malé objekty ( < 256kB) je rozhodně lepší ukládat do DB. Pro velké objekty ( > 1MB) je rozhodně lepší ukládat do FS. Mezi tím je mlhavá hranice a silně záleží na druhu aplikace. Pokud se data často přepisují, tak je lepší spíše FS (lépe se vyrovnává s fragmentací). Pokud se data nepřepisují, tak je lepší DB (větší propustnost malých objektů).

Toto je zmíněno i v nalinkovaném článku. Ostatně proto se tu ptám, protože se mi tomu nechtělo věřit. A guru zatím ještě nejsem, abych si byl svými domněnkami jistý ;-)
Až při rozkliknutí samotné studie jsem se teď dočetl:

4.2. File based storage
For the filesystem based storage tests, we stored
metadata such as object names and replica locations
in SQL server tables. Each application object was
stored in its own file. The files were placed in a
single directory on an otherwise empty NTFS
volume. SQL was given a dedicated log and data
drive, and the NTFS volume was accessed via an
SMB share.

Se svou chatrnou angličtinou jsem z toho pochopil, že se tam do databáze přistupuje tak i tak, v jednom případě pro cestu k souboru na disku, v druhém případě pro samotná data souboru.

A ještě na závěr ještě jedna úvaha. Kdybych použil APC a memcache pro uložení dat obrázku (abych nemusel sahat do DB), lze předpokládat vyšší rychlost oproti čistému výdeji obrázku z FS?

ikona David Grudl:

Stále je ve hře zbytečné použití skriptovacího jazyka, který neřeší nic, co by nedokázal lépe vyřešit Apache + cache nad filesystémem.

pojízdná kočka:

skriptovací jazyk ovládá databázi, schopnou konzistence souborových dat a metadat, jehož řešení ve filesystému je nulové.

výše zmíněná studie má nějaké konkrétní výsledky, které (za určitých podmínek) hovoří pro databázi. pokud tyto podmínky odpovídají použití v daném projektu, pak buď můžu dál reptat, a nebo zvolit to řešení, které je vhodnější.

Kočičác Bonifák:

Ještě zatím nikdo nedokázal odpovědět, jak zlé to je s počtem přístupu k databázi. Například, pro načtení stránky fotogalerie s náhledy 100 jednotlivých obrázků by stránka v <img src 100x odkázala na 100 skriptů, z nichž každý by otevřel databázové spojení, poslal dotaz a výsledek poslal na výstup. Pokud bych to vynásobil 100 unikátními uživateli, načítající tuto stránku, dostáváme se na 10000 spojení s databází. Není to, že každý ten jednotlivý "obrázkový skript" otevírá vlastní spojení (a skutečnost, že při zpracování stránky je jich najednou zavoláno 100), problém?

ikona Jakub Vrána OpenID:

Pokud se nepoužívají persistentní připojení k databázi, tak každý skript a tedy i každé načtení obrázku z databáze skutečně vytvoří připojení k databázi.

Neználek:

Dobrý den.
řeším ted trochu pro mě obtížný problém.
nejsem zdatným programátorem, což je asi největší problém.
potřebuji vytvořit ddatabázi souborů- (jde o databázi manuálů) takže soubory od velikosti několik kb až po několik desítek MB. potřebuji, aby jsem měl přímý výpis souborů např. jako ftp (adresářová struktura) a zároveň možnost vyhledávání souborů v tomto systému. Stvořil jsem si tento ftp výpis, jenže nemůžu najít žádný kód, který by si poradil s vyhledáváním v tomto systému. Proto bych od Vás chtěl poradit, jakým směrem se v tomto problému ubírat. Nevím, jaké jsou možnosti, jak by se to dalo nejlépe zařídit.
pokud mi můžete jakkoli poradit či doporučit řešení, budu velice rád. Jedná se o aplikaci pro intranet, takže nní problém s místem na disku.
prosím pište mi na email sladz@seznam.cz,icq 308215070.

Děkuji

Bonzakcek:

Dobrý den, mám jenm takový malý dotaz mimo mísu....
co je to to self::

ikona v6ak:

To je označení pro aktuální třídu. Tedy mám-li:
<?php
class Foo{
...
   self::...
...

}

?>
pak místo self může být Foo.

a:

Kdy do databaze ukladat a kdy ne - do FS rozhodne ukladejte na free hostinzích, ktere maji pomale databaze a do DB si ukladejte jen treba cestu. OVSEM POZOR! Nastava problem ze kdyz chcete soubor smazat funkci unlink() tak pokud ho prave nekdo stahuje, tak se nesmaze... Vyresit se to da, ale neni to elegantni... Tento problem v DB neni... Pokud mate ale nejaky normalni placeny hosting naopak je lepsi ukladat do DB protoze tam nejsou rychlostni omezeni NA CO SI ALE DEJTE POZOR!-nahravani INSERTEM do DB co se tyce souboru je pomale, je treba pouzit specialni zpusob na nahrani souboru do DB, dalsi vec kterou je treba zohlednit je CO mate vlastne ZA DATABAZI, nektere sou optimalizovany na text jine na soubory a treti neco mezi... "Klasickou" galerii bych tedy doporucil na placenem hostingu nahravat do databaze.

Vložit komentář

Používejte diakritiku. Vstup se chápe jako čistý text, ale URL budou převedeny na odkazy a PHP kód uzavřený do <?php ?> bude zvýrazněn. Pokud máte dotaz, který nesouvisí s článkem, zkuste raději diskusi o PHP, zde se odpovědi pravděpodobně nedočkáte.

Jméno: URL:

avatar © 2005-2018 Jakub Vrána. Publikované texty můžete přetiskovat pouze se svolením autora. Ukázky kódu smíte používat s uvedením autora a URL tohoto webu bez dalších omezení Creative Commons. Můžeme si tykat. Skripty předpokládají nastavení: magic_quotes_gpc=Off, magic_quotes_runtime=Off, error_reporting=E_ALL & ~E_NOTICE a očekávají předchozí zavolání mysql_set_charset. Skripty by měly být funkční v PHP >= 4.3 a PHP >= 5.0.