
Lazy Connecting und warum eine Datenbank-Klasse sinnvoll ist
Seit längerer Zeit gibt es heute mal wieder einen Artikel rund um die Architektur von PHP-Anwendungen hinsichtlich des Datenbankzugriffs. Genauer gesagt soll es um den Zweck einer PHP-Klasse zum Ausführen von SQL-Querys gehen.
PHP bietet ja bereits von Hause aus für viele Datenbanken Funktionen zum Verbinden, Abfragen usw. an. Für welche Datenbank man sich entscheidet, ist erstmal jedem selbst überlassen. Die Einbindung in PHP sollte aber möglichst einfach, universell, sicher und performant sein.
Die verschiedenen datenbankspezifischen Funktionen machen insbesondere den Punkt "Universell" schwierig. Universell würde in meinen Augen bedeuten, dass man ohne großen Aufwand die darunterliegende Datenbank wechseln kann (z.B. von MySQL zu Oracle). Das ist mit Abstraktionslayern wie PDO möglich. Warum geht das aber in der Praxis oft nur schwer?
In der Praxis sieht es aber oft so aus:
Webanwendungen wachsen mit der Zeit. Am Anfang wird etwas programmiert, später erweitert, noch mehr erweitert usw. Die Altlasten wird man oft nur schwer wieder los. So auch bei den Datenbanken: Man entscheidet sich beispielsweise für MySQL und nutzt die PHP-MySQL-Funktionen. Das funktioniert auch alles – allerdings müsste im Falle des Wunsches auf eine andere Datenbanksoftware umzuziehen die gesamte Anwendung umgeschrieben werden (d.h. jedes Script, in dem mindestens eine mysql_* Funktion vorkommt). Diese Arbeit rentiert sich kaum.
Auch hinsichtlich der Sicherheit gibt es oft Probleme, wenn man einfach die mysql_*-Funktionen verwendet. Man muss bei jeder Abfrage daran denken, mysql_real_escape_string() oder intval() oder andere Filterfunktionen für sämtliche von außen kommende Eingaben einzusetzen, damit SQL-Injections verhindert werden.
Aus diesen Gründen sollte man sich von Anfang an für eine eigene Datenbank-Klasse entscheiden. Es gibt jede Menge solcher fertigen Klassen, z.B. bei PHP Classes – oder man schreibt sie sich schnell selbst…
Nun möchte ich aber noch zum Punkt Performance kommen. Die meisten Web-Anwendungen gehen so vor:
- in jedes Script wird eine globale Funktionsdatei eingebunden
- in dieser Funktiondatei wird die Verbindung zur Datenbank hergestellt
- im eigentlichen Script kann nun stets auf die Datenbank zugegriffen werden
Das funktioniert auch bestens, allerdings möchte ich auf ein Problem aufmerksam machen: MySQL (und andere Datenbanken auch) hat eine Konfigurationsvariable zum Einstellen der maximalen gleichzeitigen Verbindungen (max_connections). Erfolgen mehr gleichzeitige Zugriffe als in der Variable eingestellt sind, so kommt es zum Fehler Too many connections (englische Dokumentation, da ausführlicher). Bedenkenlos erhöhen kann man die Variable leider auch nicht. Es gilt deshalb – wie mit allen Ressourcen – sparsam damit umzugehen.
Das Problem ist, dass immer zu Beginn eines Scripts eine Datenbankverbindung geöffnet und (meist) erst nach Ausführung des Scripts wieder geschlossen wird – und das unabhängig davon, ob die Datenbank überhaupt benötigt wird.
Nun könnte man sagen, dass man die Include-Datei für die DB-Verbindung eben nur in den Scripten einfügt, in denen sie auch benötigt wird. Das ist schon mal ein guter Ansatz, aber es gibt auch Fälle, in denen zu Beginn des Scrips noch gar nicht klar ist, ob die Datenbank benötigt wird – etwa, wenn die Seite aus dem Cache (entweder gecachte HTML-Dokumente auf dem Server oder aus dem Browser-Cache) geladen werden kann und somit die Datenbank ebenfalls nicht gebraucht wird.
Um möglichst sparsam mit DB-Verbindungen umzugehen, empfielt sich das Lazy Connecting.
Lazy Connecting bedeutet, dass DB-Verindungen nur dann geöffnet werden, wenn man sie auch wirklich braucht – und zwar so spät wie möglich. Und man braucht die Verbindung eigentlich erst, wenn man auf die Datenbank zugreifen möchte. Nun wäre es aber unglaublich kompliziert, vor jedem mysql_query() erst zu prüfen, ob die DB-verbindung bereits geöffnet ist. Deshalb empfiehlt sich eine eigene Datenbank-Klasse fürs Lazy Connecting.
Diese könnte beispielsweise so (einfach, für mysql) oder so (etwas komplexer, für mysqli) aussehen.
Dadurch werden Verbindungen zur Datenbank auch nur geöffnet, wenn sie wirklich benötigt werden – bzw. wird vor jeder Abfrage geprüft, ob bereits eine Verbindung hergestellt wurde. Effektiverweise würde dann auch in der Wrapper-Funktion für mysql_query die Absicherung per mysql_real_escape_string() erfolgen (oder man nutzt eben PDO, das das über Bind-Parameter selbst übernimmt).














bmueller sagt
am 2. Februar 2010 @ 14:06
Moin,
in dem Beispiel für MySQL fehlt irgendwie das Schließen der Verbindung, sonst mach das ganze keinen sinn, da ja bei jedem aufrufen der Klasse die Variable $_connected wieder auf false sitzt. Und somit die Verbindungen bei jedem aufruf geöffnet werden werden und erst am ende des Script automatisch geschlossen werden.
Habe ne Klasse geschrieben wobei im __construct die Verbindung geöffnet wird und im __destruct die Verbindung geschlossen wird.
Klappt eigentlich auch ganz gut ausser wenn ich die Query mit mysql_unbuffered_query auführen will. Da wird zwar die Recourcen Kennung zurückgegeben aber die kann nicht verarbeitet werden, ausser ich schalte den __destruct ab. Hat da jemand ein Idee?
Jan sagt
am 2. Februar 2010 @ 15:03
@bmueller: Nein, die Verbindung wird automatisch geschlossen, wenn das Script beendet ist. Es ist ja gerade der Sinn vom Lazy Connect, dass man die Verbinudng erst öffnet, wenn man sie braucht. Natürlich wäre es Quatsch sie direkt nach Gebrauch wieder zu schließen, denn oft folgen ja noch weitere Abfragen innerhalb des Scripts.
Deine Klasse mit dem __construct-Verbinden und __destruct-Schließen hat genau das gleiche Problem wie die im Beitrag beschriebene include-Lösung: Du öffnest damit die Verbindung, obwohl Du noch nicht weißt, ob Du sie überhaupt brauchst.
Wishu sagt
am 2. Februar 2010 @ 21:18
Will morgen anfangen eines meiner Projekte neu zu erstellen und da wäre eine solche Klasse sehr nützlich. Kann jemand so etwas empfehlen?
Besonders darauf bezogen:
Gruß
Wishu
bmueller sagt
am 3. Februar 2010 @ 08:46
@Jan Die Verbidung wird ja in der Klasse geöffnet und an die Klasse wird lediglich die Query übergeben. Somit wird die Verbindung nur geöffnet wenn Sie gebraucht wird und anschließend geschlossen.
Jeder aufruf der Klasse ist ja ein eigenes Objekt und somit würde auch bei der Lazy Connect geschichte bei jeder Query die Datenbank verbindung geöffnet werden, da die Variable $_connected nur auf das Objekt beschränkt ist.
Hier mal meine Klasse (Host, User usw. habe ich entfernt):
class database{
private $_connection;
function __construct(){
$this -> _connection = @mysql_connect();
@mysql_select_db(,$this -> _connection);
}
public static function unbuffered_query($query){
$db = new database;
return $db -> _query($query, 'unbuffered');
}
public static function query($query){
$db = new database;
return $db -> _query($query);
}
protected function _query($query, $mod = false) {
return $mod == 'unbuffered' ? mysql_unbuffered_query($query, $this -> _connection) : mysql_query($query, $this -> _connection);
}
function __destruct(){
mysql_close($this -> _connection);
}
}
Aufruf der Klasse:
database::query('SELECT 1+1');
database::unbuffered_query('SELECT 1+1')
bmueller sagt
am 3. Februar 2010 @ 08:58
Mir ist da gerade aufgefallen, das bei meinem Script die Datenbank verbindung eh kurz nach dem Globalen Verbindung aufruf gebraucht wird. Somit würde das ganze anscheint nix bringen oder doch?
Jan sagt
am 3. Februar 2010 @ 09:07
Nun, ich verstehe eben nicht, weshalb Du die verbindung wieder schließt. Ich würde die Funktionen alle nicht-statisch machen und dann ein Objekt erzeugen per $db = new database();
Und dann beim ersten $db->query() öffnest Du die Verbindung und dann ist sie für alle folgenden Querys ebenfalls offen.
Aber Du hast die Verbindung eben nich tunnötig geöffnet.
Die Verbindung für jede einzelne Query zu öffnen und zu schließen ist eher nicht sinnvoll.
bmueller sagt
am 3. Februar 2010 @ 09:23
Also alle Query mit einem Objekt verarbeiten, scheint mir logischer. Danke für den Tipp.
Nicolas sagt
am 4. Februar 2010 @ 10:05
Eine weitere Idee wäre es, eine Datenbank-Klasse mit dem Singleton-Pattern zu verwenden; dann wird die Klasse erst instanziert wenn man sie braucht (spart nochmals Speicher) und man kann sie in alle Funktionen verwenden ohne irgendwelche global anweisungen zu benutzen.
Habe dazu eine Erweiterung für PDO geschrieben und die für einige Projekte bereits verwendet (Quellcode: http://code.google.com/p/quiveo/source/browse/trunk/sys/classes/db.php)
bmueller sagt
am 8. Februar 2010 @ 08:21
@Nicolas
Not Found
The requested URL /p/quiveo/source/browse/trunk/sys/classes/db.php) was not found on this server.
bmueller sagt
am 8. Februar 2010 @ 08:28
Singleton:
http://de.wikipedia.org/wiki/Singleton_(Entwurfsmuster)#Implementierung_in_PHP_.28ab_Version_5.29
http://blog.newsnavigators.de/2009/05/29/php-design-patterns-2-das-singleton-pattern/
Wishu sagt
am 8. Februar 2010 @ 08:35
Dann versuch doch mal die Klammer zu entfernen ^^
http://code.google.com/p/quiveo/source/browse/trunk/sys/classes/db.php
Nicolas sagt
am 8. Februar 2010 @ 08:39
ou sorry, da hat mir das system einen streich gespielt und die kalmmer ) noch zum link gezählt
du wendes die klasse dann an indem du
DB::getInstance()->query("bla") ausführst. die connect-daten gibts du entweder als argument von getInstance() oder speicherst sie direkt in der klasse in den defaults oder erweiters den constructor, dass er die daten aus einem file lädt
bmueller sagt
am 8. Februar 2010 @ 08:54
@Wishu, oh habe ich übersehen
PHPGangsta sagt
am 8. Februar 2010 @ 22:32
….oder gleich ein PHP-Framework nutzen (z.B. Zend Framework), das "lazy" arbeitet. Ein paar Ausnahmen in einigen Komponenten gibt es aber leider.
stefan sagt
am 9. Februar 2010 @ 14:22
hi, bin schritt für schritt den Tips von bmueller gefolgt und muss sagen, es funktioniert super!!
Y!! sagt
am 12. Februar 2010 @ 20:03
Yii Framework verwenden und glücklich sein – lazy-loading everywhere
nik sagt
am 5. März 2010 @ 23:46
> Man muss bei jeder Abfrage daran denken, mysql_real_escape_string() oder intval() oder andere Filterfunktionen für sämtliche von außen kommende Eingaben einzusetzen, damit SQL-Injections verhindert werden.
Das muss man so oder so. Ohne prepared statements musst Du sonst alle Eingaben (auch int) in die Query mit '-Hochkommata eingeben, sonst ist real_escape wirkungslos (und damit auch nicht automatisierbar).
> Singleton
Besser noch Registry. Niemand weiß, wann man mal zwei verschiedene Connections in seiner Applikation benötigt.
In Verbindung mit OOP – bspw. einem Model, das sowieso eine DB braucht – kann man dann lazy bspw. on construct das DB Objekt holen.
Y!! sagt
am 9. März 2010 @ 04:00
MySQL-Injections können unter Umständen auch mit mysql_real_escape_string() nicht verhindert werden: http://www.scip.ch/?vuldb.2288
Wenn möglich immer Prepared Statements verwenden.
Jürgen sagt
am 2. März 2011 @ 11:46
@ Jan (3. Februar 2010 @ 09:07)
Hallo! Ist schon etwas her das Thema, aber noch eine Frage wenn man die Verbindung (direkt vor der ersten Verwendung) direkt mit der Instanzierung macht (constructor) dann sollte es doch auch gehen, oder?
Also direkt vor der ersten Verwendung:
$db = new database;
und die hat das drinnen
class database {
fuktion database {
… mysql_connect()
}
}
Oder?? Ich meine die Instanzierung braucht ja genauso wenig irgendwo "oben" sein und dann erst der connect weiter unten im skript passieren??
Danke!
Simon XoX sagt
am 14. Oktober 2011 @ 18:36
Hallo ich bin etwas überfordert und nun wollte ich mal wissen ob meine klasse , eine abwandlung von einer die oben gepostet wurde, den ihren zweck erfüllt. Ich bin desshlab überfordert da ich mich noch nie mit klassen befasst habe was ich nun tue.
[CODE]
class mysql_verbindung{
private $_connected = false;
private $_connection;
private $_data;
private $_counter = NULL;
public function connect($data = NULL) {
$this -> _data = $data;
}
private function _connect() {
$con = $this -> _data;
if ($this->_connection = mysql_connect($con[0],$con[2],$con[3])) {
mysql_select_db($con[1]);
$this->_connected = true;
}
}
public function query($query) {
if (!$this->_connected) {
$this->_connect();
}
$this->_counter=mysql_query($query,$this->_connection);
if($this -> _counter == FALSE){die('ERROR');}else{
return $this -> _counter;
}
}
public function count() {
return mysql_num_rows($this -> _counter);
}
}
[/CODE]