Kirjautuminen

Haku

Tehtävät

Koodit: PHP: Kuvien tallennus MySQL-tietokantaan

Kirjoittaja: ajv

Kirjoitettu: 13.04.2008 – 13.04.2008

Tagit: grafiikka, koodi näytille, vinkki

Kuvien binääridatan tallentaminen tietokantaan herättää aina keskustelua ja tuo esiin erilaisia mielipiteitä menetelmän järkevyydestä. Binääridatan hakeminen tietokannasta kasvattaa liikennettä tietokannan ja asiakasohjelmiston välillä valtavasti verrattuna pelkän informatiivisen datan hakemiseen. Jos tietokanta on jo muutenkin kovalla kuormituksella, lisää tämä kuormaa entisestään ja tekee myös kuvien hakemisesta hidasta. Useimmiten onkin perusteltua tallettaa kuvat levyjärjestelmään. Tällöin tietokantaan kannattaa tallentaa vain nimi ym. tarpeellisiksi nähdyt tiedot. Etenkin web-sovelluksissa, joiden tietokannat luonteeltaan useimmiten ovat hakupainotteisia (tietokannasta haetaan paljon enemmän dataa suhteessa lisäämiseen tai muokkaamiseen) kannattaa käyttää jälkimmäisenä mainittua tekniikkaa.

Jos tehokkuus ja kuormitus painavat vaa'an taakkaa levyjärjestelmän puolelle, löytyy vaa'an toisellekkin puolelle paljon pieniä asioita, jotka puoltavat tietokantaa tallennuspaikkana. Ensimmäinen asia on ylläpidettävyys. Jos tietoja säilytetään sekä kannassa, että levyjärjestelmässä joudutaan väkisinkin ylläpitämään kahta eri järjestelmää. Kun muokataan kuvan nimeä, joudutaan muokkaamaan nimeä sekä kovalevyllä, että tietokannassa. Kun poistetaan kuva, joudutaan poistamaan kuva sekä levyltä että kuvan tiedot tietokannasta. Entä jos halutaan varmistaa, että kummatkin toiminnot onnistuivat? Joudutaan rakentamaan monimutkaisia prosesseja yksinkertaisille asioille, jotka tietokanta yleensä tarjoaa automaattisesti. Muita tietokannan etuja ovat mm.

- kuvien oikeuksien hallitseminen
- viite-eheys-suhteiden ylläpito
- varmuuskopionnin yksinkertaistuminen
- replikointi

Tämä koodivinkki käsittelee nimenomaan kuvan binääridatan tallentamista tietokantaan. Edellinen johdanto kuitenkin pätee myös muihin tiedostotyyppeihin. Joskus voi olla tarvetta dokumentti- tai vaikka musiikkitietokannalle. Varsinkin pienten käyttäjämäärän sovelluksissa kuormitus-ongelma tuskin tulee vastaan ja tiedostojen säilyttäminen tietokannassa helpottaa ohjelmoijan työtä huomattavasti.

Koodivinkissä luodaan pohja kuvatietokannalle, johon web-palvelun käyttäjät voivat lisätä kuvia. Kuvista luodaan automaattisesti pienet peukalonkynsikuvat ja suuremmat katselukuvat. Kuvat tallennetaan tietokantaan kahteen erilliseen tauluun. Yleiset tiedot kuten kuvan nimi, lisäyspäivämäärä tallennetaan ensimmäiseen pictures-tauluun ja luotujen kuvien binääridata tallennetaan picturedata-tauluun. Kun kuva halutaan poistaa, riittää, kun poistamme rivin pictures-taulusta.

Ohjelmistovaatimukset: PHP4, MySQL5, Imagemagick, Apache

Ensin luomme tietokantaan seuraavat taulut:

CREATE TABLE `pictures` (
  `id` int(11) NOT NULL auto_increment,
  `addtime` datetime NOT NULL,
  `name` varchar(255) collate latin1_general_ci NOT NULL
  PRIMARY KEY  (`id`)
) ENGINE=InnoDB;
CREATE TABLE `picturedata` (
  `id` int(11) NOT NULL auto_increment,
  `pid` int(11) NOT NULL,
  `type` varchar(10) NOT NULL,
  `size` int(11) NOT NULL,
  `width` int(11) NOT NULL,
  `height` int(11) NOT NULL,
  `data` longblob NOT NULL,
  PRIMARY KEY  (`id`),
  KEY `pid` (`pid`),
  FOREIGN KEY (`pid`) REFERENCES `pictures` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB;

Tietokantamoottorina toimiin InnoDB, joka mahdollistaa viite-eheyden määrittämisen.

Listaukset selittäköön itse itsensä.

.htaccesilla muotoilemme kuvatietokantamme käytön näyttämään siltä, että kuvat ladattaisiin normaalisti kovalevyltä.
Kuvia voi siis katsella "hakemistoista"

pictures/small/0001.jpg
pictures/tiny/0001.jpg
pictures/large/0001.jpg

lomake.php

<?php
$kuvia_max = 3; // kuinka monta kuvaa kerralla annetaan ladata

echo '<form action="lomake-kasittelija.php" method="post" enctype="multipart/form-data">';

for($i=1; $i<$kuvia_max+1; $i++){
	echo 'Kuva '.$i.': <input type="file" name="pic[]" /><br />';
}

echo '<input type="submit" name="submit" value="Tallenna"/></form>';
?>

lomake-kasittelija.php

<?php
/*  ASETUKSIA  ***********************************************************/

// skripti käyttää kuvien käsittelyyn imagemagickia.
//Aseta tähän polku hakemistoon, jossa se sijaitsee (whereis convert auttaa)
DEFINE( 'BINDIR', '/usr/bin');
// tietokanta-yhteys
$db_config['server']      = 'localhost';
$db_config['user']                 = '';
$db_config['password']         = '';
$db_config['database']         = '';

// kuva taulut
$tbl['pictures']                     = 'pictures';
$tbl['picturedata']                = 'picturedata';

//kuvasta luotavat kopiot, tässä tapauksessa luodaan kolme erikokoista kuvaa
$pic['small']['size']     = 70;    //pidemmän sivun pituus
$pic['small']['quality']  = 80;

$pic['tiny']['size']      = 120;
$pic['tiny']['quality']   = 80;

$pic['large']['size']      = 500;
$pic['large']['quality']   = 80;

/********************************************************************/

mysql_connect($db_config['server'] ,$db_config['user']  ,$db_config['password']);
mysql_select_db($db_config['database']);


if(isset($_POST['submit'])){


  for($i = 0; $i < count($_FILES['pic']['tmp_name']); $i++){

    if($_FILES['pic']['error'][$i] == 0){

            if(file_exists($_FILES['pic']['tmp_name'][$i])
                && is_readable($_FILES['pic']['tmp_name'][$i])){
                $image_details = @getimagesize($_FILES['pic']['tmp_name'][$i]);
                if(is_array($image_details)){
                    list($width,$height,$type) = $image_details;
                }else{
                    exit('Tiedosto ei ollut kuva');
                }
            }else{
                exit('Kuvaa ei voida avata muokkausta varten');
            }

            mysql_query('INSERT INTO '.$tbl['pictures'].' (addtime) VALUES (NOW())');
            $pid = mysql_insert_id(); // picture-id
            $name = sprintf("%05d.jpg",$pid);
            mysql_query("UPDATE ".$tbl['pictures']." SET name = '".$name."' WHERE id = ".$pid);

            foreach($pic as $type => $value){

               // uudelle kuvalle uudet mitat mittasuhteet säilyttäen
               // kuvan koko määritetään kuvan pidemmän sivun perusteella
               if($height > $width){
                  $pic_height = $value['size'];
                  $pic_width = $value['size'] * ($width / $height);
               }else{
                  $pic_width = $value['size'];
                  $pic_height = $value['size'] * ($height / $width);
               }

               //varmistus, ettei tehdä käyttäjän syöttämästä kuvasta isompaa, kuin alkup.
               if(max($width,$height) <= $value['size']){
                  $pic_height = $height;
                  $pic_width = $width;
               }

                // muodostetaan komento imagemagickille
                $cmd =     'convert '.$_FILES['pic']['tmp_name'][$i].' '.
                                '-resize '.$pic_width.'x'.$pic_height.' '.
                                '-quality '.$value['quality'].' '.
                                $type.'.jpg';

                exec(BINDIR.'/'.$cmd);
                // kuva sijaitsee nyt samassa hakemistossa nimellä [type].jpg,
                //eli esim small.jpg => helpottaa mahdollista debuggausta
                $image_data = file_get_contents($type.'.jpg');
                $sql = "INSERT INTO ".$tbl['picturedata']." (
                    pid,
                    type,
                    size,
                    width,
                    height,
                    data
                ) VALUES (
                    ".$pid.",
                    '".mysql_real_escape_string($type)."',
                    ".filesize($type.'.jpg').",
                    ".$pic_width.",
                    ".$pic_height.",
                    '".addslashes($image_data)."'
                )";
                mysql_query($sql);
                unlink($type.'.jpg');
            }


    }
  }
    echo 'Kuva(t) lisätty onnistuneesti!';
}
mysql_close();
?>

picture.php

<?php

$db_config['server']      = 'localhost';
$db_config['user'] 				= '';
$db_config['password'] 		= '';
$db_config['database'] 		= '';

$tbl['picturedata']				= 'picturedata';

mysql_connect($db_config['server'] ,$db_config['user']  ,$db_config['password']);
mysql_select_db($db_config['database']);

if(isset($_GET['type']) && isset($_GET['pid'])){

	$type = mysql_real_escape_string($_GET['type']);
	$pid = intval($_GET['pid']);

	$sql= "SELECT data FROM ".$tbl['picturedata']." WHERE type = '".$type."' AND pid = ".$pid;
	$result = mysql_query($sql);
	$r = mysql_fetch_assoc($result);

	header('Content-type: image/jpeg');
	echo $r['data'];
}
mysql_close();
?>

htaccess

RewriteEngine On
RewriteRule ^pictures/([a-z]+)/([0-9]+)\.jpg$	picture.php?type=$1&pid=$2 [NC]

Kommentit

ajv [13.04.2008 23:20:03]

#

Tämä on tosiaan aika pikaisesti kasaan väännetty. Kommentoin, päivittelen ja siistin koodia vielä lähipäivinä...

Edit 17.4: Koodivinkin johdanto päivitetty

punppis [18.04.2008 11:42:02]

#

Tämä nyt ei tähän oppaaseen liity, mutta voiko tällä tavalla tallentaa muitakin tiedostotyyppejä tietokantaan?

ajv [18.04.2008 13:58:07]

#

En ole kokeillut, mutta pitäisi mennä tuolla samalla INSERT-lauseella muutkin tiedostot kantaan.

punppis [18.04.2008 14:51:51]

#

Meinasin lähinnä tuota file_get_contentsilla datan ottamista ja tallentamista tekstinä kantaan.

tsuriga [18.04.2008 16:18:31]

#

file_get_contents lukee datan oletusarvoisesti binäärimuodossa, mikäli manuaaliin on uskominen.

punppis [18.04.2008 21:13:30]

#

Dataa kyllä saa, mutta esim. .exe tiedostosta ei osaa PHP ottaa mime-tyyppiä. Onko sillä edes? =D En tiedä näistä...

E: Hyvin toimii nyt. Oli PHP:n säädöissä vikaa.

T.M. [21.04.2008 23:22:53]

#

Tommonen kyllä kuormittaa helvetisti serveriä... en suosittelis esim irc galleriaan, servu olis samantien alhaalla.

Mielummin tekisin asiat vaikealla tavalla, kuin että menisin siitä missä aita on matalin ja kaadan serverin ;)

Kuinkas tuo suoriutuu isommista tiedostoista, esim 20 megaa, tai 50 megaa? tai giga? 2 gigaa? ainaki "siellä ämbeenetissä jonne tää kumminki kopsataan" (vaikkei siel ookaa mysli tukee) se raja on just jotain 8megaa PHP:n muistille, pätee varmaan aika mones muussakin mestassa...

ajv [21.04.2008 23:49:09]

#

T.M. kirjoitti:

Tommonen kyllä kuormittaa helvetisti serveriä... en suosittelis esim irc galleriaan, servu olis samantien alhaalla.

No eiköhän tuo serverin kuormitus tule selväksi jo tuolla koodivinkin johdannossa. Siellä myös mainitaan, että tämä soveltuu nimenomaan pienten käyttäjämäärien sovelluksiin.

T.M kirjoitti:

Kuinkas tuo suoriutuu isommista tiedostoista, esim 20 megaa, tai 50 megaa? tai giga? 2 gigaa? ainaki "siellä ämbeenetissä jonne tää kumminki kopsataan" (vaikkei siel ookaa mysli tukee) se raja on just jotain 8megaa PHP:n muistille, pätee varmaan aika mones muussakin mestassa...

Tässä on aistittavissa henkilökohtaista ***tuilua... Koodivinkissä kuvan koko tuskin kasvaa muutamaa kymmentä kilotavua isommaksi, koska ne pienennetään. Tätä koodivinkkiä ei ole tarkoitus copy-pastettaa "ämbeenettiin", vaan tämän koodivinkin tarkoitus on esittää tämän aina keskustelua herättävän menetelmän huonot ja hyvät puolet.

Koodilistauksissa oli tarkoitus esitellä seuraavia tekniikoita: viite-eheyden määrittäminen, tiedoston tallennus tietokantaan, imagemagickin käyttö ja rewrite rule -kikka. Valitettavasti nämä hukkuvat sekavaan toteutukseen. Kunhan vain aikaa löytyy, niin täytyy siistiä nuo koodit.

MikaBug [09.05.2008 14:42:43]

#

Juu aivan turhaa ***tuilua tuommonen. En lukenut koko koodivinkkiä, mutta johdannon luin ja lopun selasin nopeasti läpi (palaan varmasti myöhemmin tähän) ja sain heti vastauksia useampaan mieltäni askarruttaneeseen asiaan. Olen itsekin pohtinut kuinka binääridataa PHP:llä MySQL-kantaan tallennetaan. Tämä koodivinkki on oiva esimerkki asiasta ja jokainen pohtikoon itse omalla kohdallaan järjestelmästä riippuen, kannattaako käyttää tätä tapaa, vai sitä monimutkaisempaa. Itsellä on tapana tallentaa kuvasta vain nimi tietokantaan ja itse kuva levylle, mutta tämäkin vaihtoehto on varsin mielenkiintoinen ja toimii varmasti mainiosti pienissä järjestelmissä.

Eli hyvä koodivinkki ajv, kiitos!

Wizard [15.05.2008 20:07:53]

#

Jos tästä vinkistä halutaan kevyempi, niin kannattaa ampua globit suoraan toiseen tauluun ja tehdä vain relaatio taulujen väliin.

Hyötyjä:

a) taulujen käsittely kevenee tietokantamoottorille, koska varsinainen infotaulu on pelkkää tekstidataa
b) jos ja kun halutaan vain tiedosto, niin se on helppo hakea id:llä suoraan toisesta taulusta, viittaan tässä myös kohtaan a

Tietokantamoottori käsittelee kuitenkin taulua sellaisenaan ja kun joudutaan tekemään tauluun full_scan, on globit raskaita käsitellä.

-W-

Meitzi [09.06.2008 11:56:45]

#

Mikäs taulun "pictures" sarakkeen "default" merkitys on?

ajv [09.06.2008 13:06:59]

#

Sori, jäänyt omasta sovelluksesta kummittelemaan. *Poistettu*

Kirjoita kommentti

Muista lukea kirjoitusohjeet.
Tietoa sivustosta