PDO – Istället för mysql-funktionerna PHP5
Jag har under ett par år använt mig av PDO när jag ska koppla en webbplats skriven i PHP5 med en databas. Innan dess använde jag mysql-funktionerna för att koppla upp mig mot en mysql-databas. Anledningen till jag bytte till PDO var att under min tid på gymnasiet träffade på en lärare vid namn Lars Gunter som då tipsade mig om PDO. Efter lite vana insåg jag hur flexibelt, användbart och kraftfullt PDO faktiskt är. Jag gjorde till och med ett större projektarbete där PDO spelade en avgörande roll då eftersom att webbapplikationen som jag gjorde var start beroende av en databas.
Ett av de hinder som jag stötte på under projektets gång var att hitta bra guider om PDO. De guider som fanns var antingen dåligt skrivna, bristande information eller andra brister. Därför fick jag lära mig om PDO genom att dels läsa de få guider och artiklar om PDO som var hyfsad bra, och dels att förlita sig på PHP-manualen. Med mycket om och men så lyckades jag lära mig det till slut. Även om det idag ser aningen bättre ut så har jag hittills inte hittat någon bra guide som samlar det viktigaste om PDO på ett och samma ställe, speciellt inte någon som är skriven på svenska. Därför har jag med detta inlägg dragit mitt strå till stacken.
Som vanligt tar jag gärna emot feedback på hur den kan förbättras. Skriv gärna det som en kommentar nedan eller alternativt skicka det som ett mail till mig via mitt kontaktformulär.
Är det några frågor eller funderingar som du undrar över är du välkommen att ställa frågan som en kommentar längst ned i detta inlägg. Dock är det bra om du innan du ställer frågan är att du gör följande:
- Kontrollera din fråga eller likande fråga inte redan finns under ”Vanliga frågor och svar”. Om du har tur finns svaret redan och du behöver inte lägga energi på att skriva en kommentar med samma fråga.
- Se efter om det inte reda finns en kommentar av någon som redan ha ställt sin fråga före dig och fått ett svar på den.
Har du gått igenom listan och uppfyllt samtliga punkter är du välkommen att ställa din fråga. När du skriver din kommentar, tänk på att beskriva problemet så utförligt som du kan och skicka gärna eventuell kod om det behövs.
Inledning
När jag började med PHP i början av 2000-talet fick jag lära mig att när man ska använda sig av en MySQL-databas skulle man använda följande kod:
<?php mysql_connect('localhost','användarnamn','hemligt lösenord'); mysql_select_db('my_bookstore_database'); $result = mysql_query("SELECT title, author, genre, price FROM books WHERE genre = '".mysql_real_escape_string($_POST['genre'])."' AND price < ".mysql_real_escape_string($_POST['price'])." LIMIT 0,5") or exit('Ett fel har uppstått med databasen. Kontakta administratören.'); while ($r = mysql_fetch_assoc($result)) { // Lista upp alla titilar echo $r['title'].' '; } ?>
Fördelen med mysql-funktionerna är att det är mycket enkelt att koppla upp sig mot en MySQL-databas, och därför har denna metod blivit populär bland nyblivna PHP-programmerare. Dock finns det många problem med denna metod; bland annat för bristande säkerhet, felsöknings-möjligheter och flexibilitet.
Säkerhet
Säkerhetstänkandet kring mysql-funktionerna är inte mycket att hänga i granen, speciellt inte när det kommer till SQL-injection som utgör en av de mest vanligaste och farligaste attacken som en webbplats eller webbapplikation kan utsättas sig för. Detta eftersom mysql-funktionerna inte har något inbyggt skydd mot detta. Enda sättet för att helt slippa undan dessa attacker är att använda sig av funktionen mysql_real_escape_sring() på allt som ska matas in i en SQL-fråga (precis som jag gjorde på rad 5 i kodstycket ovan). Att göra detta varje gång är både besvärligt och tidskrävande samtidigt som det är stor risk att en programmerare glömmer att lägga in denna funktion i någon sträng och därmed kvarstår fortfarande säkerhetsproblemet.
Felsökning
Något annat som också kan betraktares som säkerhetsrisk men som också kan kategoriseras som en fråga om felsökning är det faktum att man skriver ”or exit();” vid varje MySQL-funktion. Att skriva som på rad 5 i kodstycket ovan är att föredra när koden ska köras i en publik miljö, men vid felsökning krävs att man istället använder mysql_error() för att kunna få en vettig grund att gå på när man som programmerare ska åtgärda felet. Problemet uppstår när man man har många rader kod i många filer som behöver genomsökas. Att ta reda på vilken mysql-funktion som ger felet är inte lätt, speciellt som alla or exit(); innehåller ej unika felmeddelanden eller numrering som inte stämmer. Det bästa är egentligen att köra med mysql_error() istället, men då blir det en fråga om säkerhet eftersom en potentiell hacker kan få ut mycket matnyttig information från mysql_error().
Flexibilitet
De problem som jag har tagit upp nu går att lösas, med mycket besvär, genom att skapa olika klasser och/eller funktioner som förenklar eller täpper till säkerhetshålen. Däremot går det inte bygga bort alla problem, speciellt inte det faktum att mysql-funktionerna fungerar endast för en typ av databas. Vill man byta från MySQL till någon annan databas är detta inte möjligt, om man inte är beredd att skriva om princip all kod för att det ska fungera med den nya databasen.
Vad är lösningen?
Lösningen till alla problemen ovan är PDO (PHP Data Objects) som är en klass som finns inbyggd i PHP från och med version 5.0 och uppåt. Förutom att det faktum att den kan kommunicera med de flesta kända databaserna (så som MySQL, SQLite, Oracle, PostgreSQL, IBM DB2, MS SQL Server, ODBC-databaser och Firebird) har den även inbyggd skydd mot SQL-injection. Något annat som gör PDO ett bättre val än de flesta databas-klasser är att den är byggd på moduler. Detta innebär att stöd för fler databaser kan snabbt läggas till. Dessa moduler är dessutom oftast gjorda av respektive företag/organisation som står bakom databasen, vilket innebär att de databaser som stöds av PDO inte bara fungerar utan även är optimerad för PDO. Man kan med andra ord säga att PDO har konkurrerat ut (nästan) alla databasklasser som finns idag.
PDO i aktion
För att demonstrera hur PDO fungerar har jag valt att ge några exempel på användning. Som första exempel tänkte jag översätta den första koden som jag skrev till motsvarande kod om man använder sig av PDO. Längre ner i artikeln ges en beskrivning på vad respektive rad gör.
<?php $dbh = new PDO('mysql:host=localhost;dbname=my_bookstore_database', 'användarnamn', 'lösenord'); $dbh->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, 1); //$dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING); $stmt = $dbh->prepare('SELECT title, author, genre, price FROM books WHERE genre = :typ_av_bok AND price < :valt_pris LIMIT 0,5'); $stmt->execute( array( 'typ_av_bok' => $_POST['genre'], 'valt_pris' => $_POST['price'] ) ); $result = $stmt->fetchAll(); foreach($result as $r) { // Lista upp alla titilar echo $r['title'].' '; } ?>
Uppkopplingen
$dbh = new PDO('mysql:host=localhost;dbname=my_bookstore_database', 'användarnamn', 'lösenord');
Redan här märker man hur mycket annorlunda PDO är jämfört med mysql-funktionerna. Första parametern (’mysql:host=localhost;dbname=my_bookstore_database’) kallas för DSN (Database Source Name) och bestämmer vad för databas som ska användas och i så fall vad för övriga inställningar som behövs göras innan man kan börja använda databasen. I detta fall använder vi av oss Mysql och kräver därmed adressen till databas-serven samt vilken databas man vill arbeta mot. Här har man med andra ord slagit ihop mysql_connect() och mysql_select_db() till en enhet. De övriga två parametrarna (’användarnamn’ och ’lösenord’) är till för att kunna ange användarnamn och lösenord för att komma in i databasen.
MySQL-buffer
$dbh->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, 1);
Denna rad kod ser till att Mysql använder buffrade förfrågningar. Skillnaden mellan en buffrad och obuffrad frågan är att en buffrad förfrågan väntar tills databasen är helt klar med sin förfrågan innan resten av scriptet körs. En obuffrad förfrågan väntar man inte på databasen är klar utan kör igång resten av scriptet och tar emot data från databasen allteftersom.
Fördelen med obuffrad förfrågan är att den drar mindre arbetsminne och man får fram sitt resultat snabbare än buffrad förfrågan. Med andra ord är den en fördel att använda när man hanterar mycket data. Nackdelen är att det går inte söka fram och tillbaka i det resultat som man får ut. Ett exempel på detta är att man kan inte få ut antalet rader som frågan gav, då man inte kan veta detta först när frågan är helt slutförd. Det går i och för sig att komma runt detta genom att använda sig av ::fetchAll(), en metod som jag går in på djupare under rubriken Hämta ut resultatet.
Vill man göra det enkelt för sig så se till att alltid ha med denna rad kod, då kommer det att fungera utan större problem. Har man en låg memory limit eller vill göra koden så portabel som möjligt, kommentera då denna kod.
Felhantering
//$dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING);
Som standard är PDO alltid tyst om något fel skulle uppstå, vilket oftast till en fördel då en hemsida visas publikt. Men när man felsöker vill man förstås få information om vad det är för fel och annan information som krävs för att identifiera och åtgärda problemet. Vill man slå på felsökning är det bara att ta bort snedstrecken (dvs. //).
För den mer avancerade användaren är kanske ERRMOE_WARNING inte så lämpligt i vissa sammanhang, då den bara skriver ut felmeddelandet rakt ut på hemsidan. Vill man hellre utnyttja exception för att ta hand om felmeddelanden på ett snyggare sätt ersätter man koden ovan med följande istället:
$dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
Sedan kör man try{} catch(){} precis som vanligt på de ställen där man vill fånga upp felmeddelandet.
Mer läsning om PDO:s felhantering i dokumentationen på php.net.
Skicka SQL-frågan
$stmt = $dbh->prepare('SELECT title, author, genre, price FROM books WHERE genre = :typ_av_bok AND price < :valt_pris LIMIT 0,5');
Denna rad kan man säga är motsvarigheten till mysql_query(). Som namnet på metoden ($dbh->prepare()) är denna till för att göra förberedelser innan en förfrågan skickas eller utförs mot databasen. I ::prepare() matar du bara in den SQL-förfrågan som man vill utföra. Enda skillnaden mot den ursprungliga koden är att ”:typ_av_bok” ersatte ”’”.mysql_real_escape_string($_POST[’genre’]).”’ ” samt ”:valt_pris” ersatte ”’”.mysql_real_escape_string($_POST[’price’]).”’ ”. Dessa kan man kalla för pekare som då ::execute() använder sig av för att veta vad för data som ska stoppas in i SQL-förfrågan. Mer ingående hur ser man i nästa rad kod.
Execute, mer än bara att köra SQL-frågan
$stmt->execute( array( 'typ_av_bok' => $_POST['genre'], 'valt_pris' => $_POST['price'] ) );
Det ::execute() gör är, som namnet antyder, att den verkställer förfrågan. Man kan säga att det är först nu den kontaktar databasen, och skickar SQL-frågan för att databasen ska kunna leta upp den information om begärs. Om man ersätter koden mellan raderna 8 och 11 med ”$stmt->execute();”, skulle detta innebära att SQL-frågan som man skrev tidigare i ::prepare() skickas till databasen utan att PDO bearbetar förfrågan eller kontrollerar indata mot SQL-injection attacker.
Men i detta fall är det lite annorlunda, då eftersom jag har lagt in en array som innehåller ”’typ_av_bok’ => $_POST[’genre’]” och ’valt_pris’ => $_POST[’price’]. Denna array finns här för att det är meningen att all data som ska matas in i en SQL-sats ska samlas här för att sedan stoppas in i förfrågan. Med andra ord, där det står :typ_av_bok kommer att ersättas av innehållet av arrayen med index typ_av_bok, det vill säga :typ_av_bok ersätts av värdet som finns i $_POST[’genre’]. I den SQL-fråga som skickas till databasen kommer den se ut följande om $_POST[’genre’] = Action och $_POST[’price’] = 299:
SELECT title, author, genre, price FROM books WHERE genre = 'Action' AND price < 299 LIMIT 0,5
Varför man gör på detta sätt istället för att lägga dessa direkt i SQL-frågan som man gjorde i mysql-funktioerna är av främst på grund av två anledningar. Den första är att PDO:s skydd mot SQL-injection fungerar enbart när man matar in input-data på detta vis. All input-data som ska matas in måste gå igenom ::execute() istället för att lägga in direkt i SQL-frågan, annars missar man en stor poäng med att använda PDO.
Den andra anledningen till varför man gör på detta sätt är att man då kan lösa en hel del andra problem som tidigare uppstod med mysql-funktionerna. Ett exempel är att om man vill skicka flera SQL-frågor i en loop mot databasen utan att förlora nämnvärd prestandarad. Mer om detta tar jag upp under rubriken Styrkan i prepared statements.
Observera att anledningen till att jag lägger in input-data direkt från $_POST utan att kontrollera eller filtrera data är enbart för att visa att det är möjligt att göra detta utan att riskeras att utsättas för SQL-injection. Att man skyddar sig mot SQL-injection är inte samma sak som att man är skyddad mot exempelvis XSS. Självfallet ska man alltid kontrollera och filtrera all input-data innan den används både som i databasanrop och andra delar av systemet. Läs följande artikel om PHP-säkerhet och anpassa koden efter råden. Med dessa råd kommer man långt och kommer då att vara skyddad till crika 99% procent.
Hämta ut resultatet
$result = $stmt->fetchAll(); foreach($result as $r) { // Lista upp alla titilar echo $r['title'].' '; }
Nu till det sista steget, då när man äntligen får ut sin information så att den kan användas. Här fungerar utskriften precis som om man använde mysql-funktionerna. Skillnaden är att man nu istället får använda sig av foreach() istället för while(), eftersom att metoden ::fetchAll() returnerar sitt resultat som en array och då lämpar sig foreach bättre. För att se all information som PDO-spottar ur sig, använd i så fall istället denna kod:
$result = $stmt->fetchAll(); foreach($result as $r) { // Nu visas alla information om arrayen som finns i $r. echo '<pre>'; print_r($r); echo '</pre>'; }
Vill man istället bara skriva ut enda rad av data bör man istället använda sig av ::fetch(). Ett exempel på hur ::fetch() används:
$r = $stmt->fetch(); echo 'Min favorit bok är '.$r['title'].' som kostar '.$r['price'].' kr';
Även om man kan använda ::fetch() för att i en while-loop hämta ut resultatet rekommenderar jag att man använder sig av foreach() om man ska hämta fler än 1-2 rader. Detta för att det generellt sätt går snabbare för PHP att loopa igenom en array. Vid undantagsfall om man har snålt med ram-minne kan ::fetch() vara att föredra om man har en databas som kan leverera information tillräckligt snabbt.
Kör man en fråga som inte kräver retur av data (exempelvis DELETE, UPDATE och INSERT) kan man givetvis strunta i ::fetch() och ::fetchAll() och enbart köra kod ned till execute (vilket jag kommer att visa i exemplen nedan).
Styrkan i prepared statements
Man märker att det är stor skillnad mellan PDO och mysql-funktionerna i hur man arbetar med dessa. Detta beror främst på att de båda lösningarna har utgått från två olika tankesätt över hur man ska hantera och styra resurser mot en databas. Mysql-funktionerna bygger tänkandet på att man ska anropa funktioner för att skicka, ändra och ta emot information samt även att utföra kommandon mot databasen. Detta fungerar utmärkt och det är rätt enkelt att förstå sig på principen. Nackdelen är dock det är svårt att återanvända anrop som man har tidigare gjort mot databasen. Vill man exempelvis sätta in flera rader i en databas på en och samma gång måste man göra princip följande:
<?php $books_to_insert_to_database = array('Bok 1','Bok 2','Bok 3'); foreach($books_to_insert_to_database as $book) { mysql_query("INSERT INTO new_books (title) VALUES ('$book')") or exit('Ett fel har uppstått med databasen. Kontakta administratören.'); }
Detta är ineffektivt sätt att arbeta på, då man gör en ny uppkoppling mot databasen vid varje loop och därmed slösar massa trafik i onödan. Dessutom tar det förhållandevis lång tid att köra, speciellt om man har en hemsida som har mycket trafik.
Det som gör att PDO inte detta problem (åtminstone inte lika hög grad) är att man har byggt PDO på en annan princip än mysql-fuktionerna. Tankesättet som är bakom PDO:s uppbyggnad heter prepared statements (vilket betyder ungefär ”förberedande uttalanden”), vilket innebär att man ska förbereda SQL-frågor innan man de körs mot databasen. Detta har vi tidigare gjort under rubrikerna Skicka SQL-frågan och Execute, mer än bara att köra SQL-frågan. Det är nu som ::prepared() kommer till sin rätt, då man kan utnyttja denna mer effektivt än att bara köra en SQL-fråga en gång. Följande exempel är en översättning av den kod som jag skrev ovan till motsvarande kod med PDO:
<?php $books_to_insert_to_database = array('Bok 1','Bok 2','Bok 3'); $stmt = $dbh->prepare("INSERT INTO new_books (title) VALUES (:bok)"); foreach($books_to_insert_to_database as $book) { $stmt->execute( array('bok' => $book) ); }
Som man kanske märker redan när man ser koden är att PDO återanvänder det som ::prepare() förberedde innan loopen körs igång. Istället för att uppkopplingen varje gång stängs ned när förfrågan genomförts, återanvänds den till andra frågor som ska också skickas med som har exakt samma SQL-fråga (förutom att den innehåller dynamisk data som förändrar sig vid varje förfrågan).
För den som vill läsa mer om prepared statements eller om man är skeptisk till om detta ger en presstandardvinst kan läsa mer om det i följande blogg.
Arbeta med andra databaser
En stor fördel med PDO är som jag tidigare har skrivit om är att det är förhållandevis mycket enkelt att byta databas. Även om det är ganska ovanligt att man byter en befintlig databas på en hemsida som är i drift till en annan, är det bekvämt för en programmerare att hålla sig till ett system som man kan oavsett vilken databas som används i ett projekt. Det sparar både tid, pengar och mycket uppslag i PHP-manualen.
Praktiskt exempel
Som exempel på hur man byter från en databas till en annan har jag valt att konvertera om en MySQL-databas till Sqlite. Överflytten av all data är gjort, det som återstår är att modifiera koden. Så här ser den ursprungliga koden ut:
<?php $dbh = new PDO('mysql:host=localhost;dbname=my_bookstore_database', 'användarnamn', 'lösenord'); $dbh->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, 1); //$dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING); $stmt = $dbh->prepare('UPDATE visitors_on_my_bookstore SET visitors = visitors + 1 WHERE id = 1 LIMIT 1'); $stmt->execute();
Och så här ser den ut när jag har gjort om koden till Sqlite:
<?php $dbh = new PDO('sqlite:/sökväg/till/min/bokaffär/database.sqlitedb'); //$dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING); $stmt = $dbh->prepare('UPDATE visitors_on_my_bookstore SET visitors = visitors + 1 WHERE id = 1 LIMIT 1'); $stmt->execute();
Lägg nu märke till rad 3 och 4 som jag har markerat i respektive kodstycke. Det är dessa rader som jag enbart har ändrat på för att sqlite ska fungera i den nya koden. Koden i övrigt är identisk. Lika smidigt kan det även gå för längre kodstycken eller rent av hela webbplattformar. Det enda som kan göra att en övergång inte går så smidigt som man har tänkt sig är om man skriver databasspecifika SQL-frågor eller PDO-metoder. Ett exempel på SQL-kod som är databasspecifik är Microsoft Access, som använder sig av TOP istället för LIMIT för att begränsa antalet reder som ska visas. Men i övrigt bör detta fungera rätt smärtfritt.
Förutsättningar innan man byter databas
För att exemplet ovan ska fungera är det några saker som man bör tänka på. Jag nämnde tidigare om att PDO är baserad på moduler som respektive företag som utvecklar databasen oftast gör. Dessa moduler finns oftast installerade tillsammans med PDO, dock är de flesta avaktiverade från start. För att ta reda på vilka moduler som finns installerade och aktiverade kan man antingen titta i phpinfo() och under rubriken PDO för att se vilka ”drivers” som finns. Ett annat alternativ är att man kör följande kod:
<?php echo '<pre>'; print_r(PDO::getAvailableDrivers()); echo '</pre>';
Oftast finns Sqlite version 2 och 3 samt MySQL på plats från start. Saknas din databas går det oftast att aktivera eller hitta de drivers som behövs för att PDO ska kunna arbeta med databasen. Eftersom att det är olika tillvägagångssätt beroende på vilken databas som man vill använda sig av hänvisar jag till PHP-manualens drivers sektion för PDO eller att man googlar exempelvis på ”pdo driver for ’din databas'”.
Lista med DSN
Även om de flesta databaser enbart kräver en ändrad DSN finns det undantagsfall då de kräver lite mer jobb för att fungera fullt ut (exempelvis MySQL-buffer som jag har skrivit om tidigare). Eftersom alla databaser har olika behov av vilka ändringar som behövs göras innan den ska fungera med PDO hänvisar jag än en gång till PHP-manualens sektion för PDO drivrutiner för databaser eller att helt enkelt att googla på det. Dock har jag valt att ta med de mest förekommande databaserna.
MySQL
$dbh = new PDO('mysql:host=hostname;dbname=database', $username, $password); $dbh->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, 1);
Sqlite version 3
$dbh = new PDO('sqlite:/opt/databases/mydb.sq3');
Notera: Om databasen inte finns på angiven sökväg skapas en ny som standard. Detta förutsatt om tillräckliga rättigheter finns för den mapp där den ska skapas i.
Sqlite version 3 (Memory)
Vill man skapa en Sqlite databas i minnet för att sedan förstöras vid avslutad körning eller sparas ned på annat sätt kan man använda sig av denna kod:
$dbh = new PDO('sqlite::memory:');
PostgreSQL
$dbh = new PDO("pgsql:dbname=database;host=hostname", $username, $password);
Microsoft SQL Server
$dbh = new PDO('mssql:host=hostname,port;dbname=database', $username, $password));
Slutord
Nu har jag gått igenom grunden för hur man använder sig av PDO och detta bör vara mer än tillräckligt för att komma igång och börja antingen anpassa redan existerande applikationer / hemsidor / projekt till PDO eller använda det i framtida projekt.
Den nackdel som jag kan komma på med PDO jämfört med mysql-funktionerna är att det är lite mer omständligt att skriva kod för att koppla upp mot databasen samt att köra SQL-frågor. Men precis som när det gällde att lära sig hur mysql-funktionerna fungerade, handlar det om att använda systemet i sin vardag. Med tid och vana sitter det ganska snart i ryggmärgen. Om man vill kan man också skriva egna klasser och/eller funktioner för att underlätta när man kodar med PDO. Själv har jag skapat en databasklass (som jag inte vill dela med mig av på grund av den är ”work in progress”) för att främst underlätta arbetet med PDO.
Ett exempel på hur jag använder min klass:
<?php require_once 'db.class.php'; $dbh = new Db('mysql:host=localhost;dbname=my_bookstore_database', 'användarnamn', 'lösenord'); $result = $dbh->preExeFetchAll('SELECT title, author, genre, price FROM books WHERE genre = :typ_av_bok AND price < :valt_pris LIMIT 0,5', array('typ_av_bok' => $_POST['genre'], 'valt_pris' => $_POST['price']) ); foreach($result as $r) { // Lista upp alla titilar echo $r['title'].' '; } ?>
Kanske inte ser mycket ut för världen, men det gör kodningen betydligt smidigare och kunna göra samma saker på färre rader än med mysql-funktionerna.
Vill man lära sig mer om PDO kan du hitta all information du behöver i php.net:s dokumentation om PDO.
Vanliga frågor och svar
1. Hur räknar jag antalet rader?
Här finns det två sätt att gå till väga. Ett sätt är att göra följande:
<?php $stmt = $dbh->prepare('SELECT id FROM books WHERE price < :valt_pris LIMIT 1'); $stmt->execute( array( 'valt_pris' =>200 ) ); $count = count($stmt->fetchAll()); echo "Det är $count st böcker som kostar lägre än 200 kr.";
Eller använder man sig av SQL för att lösa samma problem (rekommenderas):
<?php $stmt = $dbh->prepare('SELECT count(id) AS qty_books FROM books WHERE price < :valt_pris LIMIT 1'); $stmt->execute( array( 'valt_pris' =>200 ) ); $r = $stmt->fetch(); echo "Det är ".$r['qty_books']." st böcker som kostar lägre än 200 kr.";
Observera att ::rowCount() är inte samma sak som mysql_num_rows(). Se fråga 4 för mer ingående förklaring.
2. Finns det motsvarighet till mysql_num_rows()?
Se fråga 1 och kodstycke 1 för motsvarande funktion, även om kodstycke 2 är att rekommendera.
3. Kan jag få reda på last insert id?
Absolut! Så här gör man detta i PDO med ::lastInsertId():
<?php $stmt = $dbh->prepare('INSERT INTO books (title, price) VALUES (:book_title, :book_price)'); $stmt->execute( array( 'book_title' =>$title, 'book_price' => $price ) ); $book_id = $dbh->lastInsertId(); echo "Boken $title fick id-nummer: <strong>$book_id</strong>";
4. Är det möjligt att få ut antalet rader som berödes vid en SQL-fråga?
Ja, under förutsättning att det rör sig om INSERT, DELETE eller UPDATE (vid SELECT hänvisar jag till fråga nummer 1). Om detta så är fallet använder man sig av ::rowCount():
<?php $stmt = $dbh->prepare('DELETE FROM books WHERE price > :valt_pris'); $stmt->execute( array( 'valt_pris' => 2000 ) ); $count = $stmt->rowCount(); echo "Det var $count st böcker som raderades eftersom att de kostade mer än 2000 kr.";
5. Vad är ::bindValue?
I denna introduktion till PDO har jag använt mig av ::prepare() och ::execute() med en array för att förbereda frågor och verkställa dem. Detta för att bland annat motverka SQL-injections attacker. Dock finns det fler sätt att göra det på med PDO och ett sätt är med ::bindValue. Som kodexempel utgår jag från koden under rubriken Execute, mer än bara att köra SQL-frågan och gör om till motsvarande kod för ::bindValue:
$stmt->bindValue( 'typ_av_bok' , $_POST['genre']); $stmt->bindValue('valt_pris', $_POST['price']); $stmt->execute( );
Att använda sig av ::bindValue eller av en array i execute som det är utformad i kodexemplet har varken några tydliga fördelar eller nackdelar, utan det är mest tycke och smak vilken metod man föredrar. Däremot om man vill kunna definiera datatyp och kanske till och med längd så har bindValue en fördel. Ett exempel på användning:
$stmt->bindValue( 'typ_av_bok' , $_POST['genre'], PDO::PARAM_STR, 12); $stmt->bindValue('valt_pris', $_POST['price'], PDO::PARAM_INT); $stmt->execute( );
På rad 8 kommer värdet att skickas som en stäng med en längd på max 12 tecken, medan rad 9 kommer att skicka sitt värde som ett heltal. Vill man kunna sätta ett pris med kronor och ören måste man mata in det som en sträng, precis som jag har gjort på rad 8. Detta eftersom att decimala datatyper så som Float och Double finns inte för PDO::PARAM. För att lösa mer om detta och vilka typer som finns, läs om ::bindValue i PHP-manualen.
6. Varför används frågetecken i SQL-frågan?
När jag förbereder frågor med ::prepare använder jag oftast pekare att visa vilket värde som ska in var. Men det finns ett annat sätt att göra detta på, och det är med frågetecken som pekare. I detta kodexempel utgår jag från koden under rubriken Kan jag få reda på last insert id? från vanliga frågor och svar:
$stmt = $dbh->prepare('INSERT INTO books (title, price) VALUES (?, ?)'); $stmt->execute( array( $title, $price) ); $dbh->lastInsertId(); echo "Boken $title fick id-nummer: <strong>$book_id</strong>";
En fördel med att använda sig av frågetecken som pekare är att koden blir lite mindre, eftersom man slipper döpa sina pekare. Vilket frågetecken som kommer att ha ett visst värde bestäms av den ordning som finns i arrayen. Med andra ord kommer första frågetecknet att få värde från $title, andra frågetecknet kommer ha samma värde som $price. Vill man ändra vad för värde som ett frågetecknen får måste man byta plats på värderarna i arrayen.
Personligen avråder jag att man använder sig av frågetecken som pekare, eftersom att det är väldigt lätt att får en ostrukturerad kod som är svårt att underhålla. Visst är det möjligt att göra en SQL-fråga som skickar tio argument eller fler utan större problem när man skriver koden första gången. Men får man för sig att ta bort eller lägga till en parameter kan man få ett mindre helvete. Att räkna antalet frågetecken och verkligen se till att de hamnar i rätt ordning är både mödosamt och själ dyrbar projekttid. Vet man däremot att en SQL-sats aldrig kommer att ha fler än två argument (exempelvis inloggning) eller ännu hellre enbart ett argument kan det vara okej att använda frågetecken som pekare.
7. Jag får konstiga tecken istället för ÅÄÖ, varför?
Detta har egentligen inget som beror på PDO, utan det är en frågan om vad för teckenuppsättning som man har använt när man har gjort databasen respektive den HTML-kod som man skickar till webbläsaren. Har man HTML-kod med UTF-8, vilket är att rekommendera, kan man antigen ändra teckenuppsättningen för de tabeller som används eller lägg följande efter raden efter när PDO-obejektet skapas ( $dhb = new PDO() ):
$dbh->exec('SET NAMES UTF8');
Eller alternativt ändra DSNen enligt följande exempel:
$dbh = new PDO('mysql:host=localhost;dbname=my_bookstore_database;charset=UTF-8', 'användarnamn', 'lösenord');
Eller:
$dbh = new PDO('mysql:host=localhost;dbname=my_bookstore_database;charset=UTF-8', 'användarnamn', 'lösenord', array(PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8"));
Finner det underligt att ingen kommenterat denna artikel ännu. Den är faktiskt en av de bättre resurser jag hittat. Åtminstone av de som finns på svenska. Det enda jag saknar egentligen är lite fler exempel (kan dock inte komma på något lämpligt just nu) och användandet av $dbh->bindParam(); då jag finner det lite tydligare än att som du använda arrays.
@Tommy Brunn
Tack för Feedback och de vänliga orden! 🙂
Jag har försökt lägga en del exempel löpande i texten, men jag ska fundera på om jag kan hitta på fler exempel.
$dbh->bindParam() är något som jag normalt inte använder mig av då jag tycker att array är tillräckligt tydligt för mig. Men alla är inte som jag, då är det rimligt att skriva något om det också.
Hej! tänkte bara tacka för en bra guide för jag hittade precis de problemen jag hade…. ny på PHP och fått ett projekt och inte kopplat databas mer än till .net innan så bra förklaring! =)
Tack så mycket @Pernilla ! 🙂
Nice artikel, allt jag behövde veta!
Tackar 🙂
Jag såg PDO idag i en klass jag studerade och sökte lite på google och hittade din sida, väldigt bra skrivet och förklarat tycker jag. Ska börja implentera PDO istället för mysql funktionerna nu.
Har varit mycket bristfälligt med korrekt information om säkra sätt att ansluta till databaser med hjälp av PHP, mycket hjälpsamt!
Fortsätt så!
Hej,
Tack för en bra artikel. Ska gå över till PDO så mycket som möjligt från mysql.
En fråga angående lastInsertId().
I artikeln står det $stmt->lastInsertId();
Borde det inte vara $dbh->lastInsertId()?
@Jonna Tärneberg
Helt rätt, tack!
En fullmatad artikel – och på svenska. Bra gjort!