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:

  1. in jedes Script wird eine globale Funktionsdatei eingebunden
  2. in dieser Funktiondatei wird die Verbindung zur Datenbank hergestellt
  3. 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).

Jan hat 152 Beiträge geschrieben

20 Kommentare zu “Lazy Connecting und warum eine Datenbank-Klasse sinnvoll ist

  1. bmueller sagt:

    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?

  2. Jan sagt:

    @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.

  3. Wishu sagt:

    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:

    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.

    Gruß
    Wishu

  4. bmueller sagt:

    @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’)

  5. bmueller sagt:

    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?

  6. Jan sagt:

    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.

  7. bmueller sagt:

    @Nicolas

    Not Found
    The requested URL /p/quiveo/source/browse/trunk/sys/classes/db.php) was not found on this server.

  8. Nicolas sagt:

    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

  9. nik sagt:

    > 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.

  10. Jürgen sagt:

    @ 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!

  11. Simon XoX sagt:

    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]

Eine Antwort schreiben

Ihre E-Mail-Adresse wird nicht veröffentlicht. Benötigte Felder sind markiert mit *

You may use these HTML tags and attributes: <a href=""> <blockquote cite=""> <pre lang=""> <b> <strong> <i> <em>