Einfache Datenbankklasse erstellen

  • (Da es anscheinend immer noch Unklarheiten über den Begriff "Tutorial" gibt, hier mal ein Beispiel wie ein Tutorial aufgebaut sein sollte)


    Ziel: eine einfache Datenbankklasse erstellen, über die leicht einfache Anfragen an die Datenbank gestellt werden können
    Verwendete Techniken: PHP, MySQLi
    Schwierigkeitsgrad: für Anfänger geeignet, Grundkenntnisse über die Funktionsweisen von PHP, OOP und SQL vorausgesetzt
    Anmerkungen: keine PDO, weil in diesem Fall unnötig und für Anfänger nur bedingt geeignet. Kommentare und Fragen sind gerne gesehen, werden in den Beitrag eingebaut und danach gelöscht um den Thread übersichtlich zu halten.
    Kommentare: der Code ist mit Blockkommentaren versehen, die die entsprechenden Attribute bzw. Methoden zusammenfassen. Diese Kommentare sind nach Konvention in englischer Sprache verfasst, für das Verständnis des Codes allerdings nicht notwendig, dafür gibt es das Tutorial. Da man Codes allerdings immer kommentieren sollte (vor allem wenn andere Leute ihn verwenden, aber auch für einen selbst) habe ich diese drin gelassen.
    Fehlerbehandlung: die Klasse schreibt Fehler in ein error-Array der aktuellen Session, die dann an beliebiger Stelle ausgegeben werden können. Alternativ kann man die Fehler natürlich auch direkt ausgeben, dies kann aber je nach Seitenaufbau problembehaftet sein. Natürlich kann man dies auch anders lösen, z.B. mit einer weiteren Funktion error o.Ä.
    Debugging: die meisten Methoden bekommen als letzten Parameter einen boolean-Wert $debug übergeben, der falls gesetzt dafür sorgt, dass Informationen zur momentanen Situation in ein debug-Array der aktuellen Session geschrieben werden, welches dann ähnlich wie das error-Array an beliebiger Stelle ausgegeben werden kann. Natürlich kann man dies auch anders lösen, z.B. mit einer weiteren Funktion debug o.Ä.


    Schritt 1: Was soll die Klasse können?
    Am Anfang steht immer die Frage "was will ich eigentlich erreichen?". Um das ganze einfach und kompakt zu halten (vorerst - das gute an OOP ist ja das Erweiterungen sehr einfach sind), beschränken wir uns auf die wesentlichen Funktionen: Datenbankverbindung herstellen, SELECT-, INSERT-, UPDATE- und DELETE-Anfragen.


    Schritt 2: Struktur
    Da wir die DB-Klasse ständig verfügbar haben wollen, die ohne jedes Mal eine Instanz z.B. als Funktionsargument zu übergeben benutzbar ist, sind die Attribute (Variablen) und Methoden (Funktionen) unserer Klasse statisch (Schlüsselwort static). Da wir die Klasse nicht instanziieren, brauchen wir keinen Konstruktor, stattdessen eine init-Methode, die die Datenbankverbindung aufbaut. Weiterhin möchten wir eine select-Methode, eine insert-Methode, eine update-Methode und eine delete-Methode, die jeweils entsprechende Queries ausführen, ohne dass wir sie jedes Mal selber schreiben müssen. Da diese Funktionen sehr einfach gestrickt sein sollen, möchten wir außerdem noch eine query-Methode für komplexere Anfragen (z.B. JOINs), die einfach eine übergebene Query ausführt und das Ergebnis ohne Aufbereitung zurückliefert. Als Klassenattribut brauchen wir vorerst nur eine noch leere Instanz der Datenbankverbindung, auf die alle Methoden zugreifen.


    Schritt 3: Grundgerüst


    Unsere Klasse heißt ganz einfach DB (da wir statische Methoden haben ist es gut den Namen so kurz wie möglich zu halten, sonst muss man immer so viel tippen beim Aufrufen). Die Klasse hat ein Attribut $db, in welches wir später unsere MySQL-Verbindung speichern. Das Attribut ist private, da wir es lediglich in der Klasse selbst brauchen. Wir initialisieren auf null, da die Verbindung erst später in der init-Methode hergestellt wird.


    Schritt 3: Hauptfunktionen
    Die Hauptfunktionen unserer Klasse sind: init, select, insert, update, delete, query. Während dieses Tutorials werden wir über einige Hilfsfunktionen stolpern, diese werden dann nach allen Hauptfunktionen in Schritt 4 erklärt.


    Schritt 3.1: init


    Die init-Funktion bekommt vier Parameter übergeben: $dbHost (z.B. localhost), $dbUser und $dbPassword zur Authentifikation sowie den Namen der Datenbank auf die verbunden werden soll als $dbName. Mit diesen Parametern wird dann in Zeile 9 eine neue MySQLi-Verbindung geöffnet und in die $db-Variable der Klasse gespeichert. Falls es dabei einen Fehler gibt, wird dieser ins error-Array geschrieben.
    Anschließend wird noch der Zeichensatz der aktuellen Verbindung auf utf-8 gesetzt, um Zeichensatzprobleme zu vermeiden.
    Der Aufruf der init-Methode sollte idealerweise in einer config oder init-Datei erfolgen, auf jeden Fall aber vor der ersten Datenbankverbindung (logischerweise). Der Aufruf erfolgt wie folgt:


    PHP
    1. DB::init(DB_HOST, DB_USER, DB_PASSWORD, DB_NAME); // die Konstanten natürlich vorher setzen



    Schritt 3.1: select


    Die select Funktion bekommt eine ganze Reihe an Parametern übergeben: $table ist der Name der Tabelle, aus der selected werden soll, $columns (optional) ist entweder ein Array mit den Spalten oder ein String, falls nur eine Spalte ausgewählt werden soll, $where und $limit (beide optional) sind die entsprechenden WHERE und LIMIT Bedingungen, wenn gewünscht, zusätzlich der optionale $debug-Parameter.
    Da wir in $columns entweder einen String oder ein Array haben, bedienen wir uns der generateColumnList-Methode (siehe 4.1), die aus dem übergebenen Wert einen für den SQL-Befehl verwendbaren String generiert. Falls die $where und $limit Parameter nicht null sind, wird an das SQL die entsprechende Bedingung angehängt, anschließend wird die Query ausgeführt. Falls ein Fehler auftritt wird dieser ins error-Array geschrieben und ein leeres Array zurückgegeben. Falls die Query fehlerfrei durchläuft, wird anschließend jede Zeile des Ergebnisses in das $ret-Array geschrieben, welches am Ende zurückgegeben werden soll.
    Hat dieses Array, nachdem alle Ergebnisse hineingepackt wurden, nur ein Element, so wird nur dieses eine Element zurückgegeben, ansonsten das komplette Array.


    Diese Funktion kann nun verschiedene Aufrufe handlen:

    PHP
    1. $result = DB::select('user');


    gibt einfach alle Einträge der user-Tabelle zurück


    PHP
    1. $result = DB::select('user', '*', 'active = 1');


    gibt alle Einträge der user-Tabelle zurück, bei denen die Spalte active auf 1 gesetzt ist.


    PHP
    1. $result = DB::select('user', 'username', 'active = 1', 20, true);


    gibt die ersten 20 username-Spalten der user-Tabelle zurück, bei denen die Spalte active auf 1 gesetzt ist, zusätzlich soll debugged werden.



    Schritt 3.2: insert


    Die insert-Methode bekommt zwei Parameter übergeben: $table ist der Tabellenname, $data ist ein Array mit den einzufügenden Daten. Optional kann noch der $debug Parameter übergeben werden.
    Zuerst werden zwei leere Strings initialisiert, $keys und $values. Diese sollen später unsere keys und values für die Insert-Anweisung enthalten. In einer Schleife wird anschließend das $data-Array durchlaufen und die Werte an die entsprechenden Strings angehängt. Hier bedienen wir uns einer weiteren Hilfsfunktion escape (siehe 4.2), welche die übergebenen Strings für den Eintrag in die Datenbank escaped. Da wir beim Anhängen immer ein Komma mit angehängt haben aber natürlich am Ende der Strings kein Komma mehr wollen, entfernen wir anschließend mit der PHP-eigenen Funktion rtrim alle Kommas und Leerzeichen vom rechten Rand der beiden Strings.
    Nun bauen wir die insert-Anweisung korrekt zusammen, speichern sie ggf. im debug-Array, führen sie aus und speichern eventuelle Fehler ab.


    Der Aufruf erfolgt dann wie folgt:

    PHP
    1. DB::insert('user', array(
    2. 'username' => 'test',
    3. 'name' => 'Tester',
    4. 'active' => 1));



    Schritt 3.3: update


    Die update-Funktion arbeitet ähnlich wie die insert-Funktion (mit dem Unterschied, dass aufgrund der unterschiedlichen Syntax von insert- und update-Anweisungen der String direkt ohne Hilfsstrings zusammengebaut wird), bekommt aber als Parameter zusätzlich noch die id des zu updatenden Datensatzes übergeben. Dies erfordert die Existenz einer Primary Key Spalte (!!) in der Tabelle. Dies ist i.d.R. eine Spalte, die jeden Datensatz durch auto_increment mit einer ID versieht. Wir setzen eine weitere Hilfsfunktion getPrimaryKeyColumn (siehe 4.3) ein, welche uns den Namen dieser Spalte in der Tabelle $table zurückliefert, welche dann in der WHERE-Bedingung eingebaut wird, um den entsprechenden Datensatz zu updaten. Da die ID immer unique ist, kann man mit der update Funktion in dieser Form natürlich nur einzelnde Datensätze updaten. Für einfache Zwecke ist das auch erstmal ausreichend. Am Ende erfolgt die übrige Prozedur.


    Der Aufruf erfolgt dann wie folgt:

    PHP
    1. DB::update('user', 1, array('active' => 0));



    Schritt 3.4: delete


    Auch die delete-Funktion hat Ähnlichkeiten mit der update-Funktion, denn auch hier wird wieder eine Tabelle und die ID des zu löschenden Datensatzes übergeben. Daten brauchen wir nicht mehr, schließlich wollen wir ja löschen. Die Query wird wieder mit Hilfe der getPrimaryKeyColumn-Funktion aufgebaut und anschließend in gewohnter Manier ausgeführt.


    Der Aufruf erfolgt dann wie folgt:

    PHP
    1. DB::delete('user', 1);



    Schritt 3.5: query


    Für alle anderen SQL-Anfragen, die komplexer sind als die durch die anderen Funktionen abgedeckten Szenarien, fügen wir noch eine query-Funktion hinzu, die übergebenes SQL einfach ausführt.



    Schritt 4: Hilfsfunktionen
    Schritt 4.1: generateColumnList


    Da diese Funktion nur von innerhalb der Klasse zugänglich sein soll, ist sie private. Als Parameter kommt ein Array oder ein String an. Falls $columns ein Array ist, wird dieses per implode zu einem durch Kommas getrennten String gemacht, falls $columns ein String ist so wird dieser einfach unverändert wieder zurückgegeben. Die Funktionalität der Funktion ist so sehr einfach, allerdings könnte man z.B. sehr einfach noch eine weitere Bestandteile aufnehmen, wenn gewünscht (column as name Unterstützung o.Ä.).



    Schritt 4.2: escape

    PHP
    1. /**
    2. * escape given string
    3. * @param string $string string to escape
    4. * @return string escaped string
    5. */
    6. public static function escape($string) {
    7. return self::$db->real_escape_string($string);
    8. }


    Diese Funktion liefert einfach den escapeden String zurück. Auch hier könnte man noch weitere Dinge einbauen, z.B. Ersetzungen o.Ä. Da eine solche Funktion evtl. auch außerhalb der Klasse von Nutzen sein sollte, machen wir sie public.



    Schritt 4.3: getPrimaryKeyColumn


    Diese Funktion soll den Namen der PK-Spalte der übergebenen Tabelle zurückgeben. Dafür wird eine entsprechende Query zusammengebaut und anschließend die Spalte Column_name der ersten Zeile (mehr als eine sollte es eh nicht sein) des Ergebnisses zurückgegeben. Da auch diese Funktion evtl. noch anderweitig von Nutzen sein könnte, machen wir auch diese public.



    Schritt 5: insertID
    Ein bisher noch nicht genanntes 'Feature' unserer Klasse soll die sogenannte insertID werden. Diese ID wird von der Datenbank nach einem insert geliefert und ist die eindeutige ID des gerade eingefügten Datensatzes. Mithilfe der insertID kann man mit dem gerade eingefügten Datensatz sehr einfach weiter arbeiten.
    Diese insertID wollen wir als öffentlich zugängliches Attribut unserer Klasse speichern, deshalb fügen wir nach der Deklaration des Attributes $db folgendes ein:

    PHP
    1. /**
    2. * id of last inserted row
    3. * @var string
    4. */
    5. public static $insertID;


    Zusätzlich fügen wir am Ende der insert-Funktion noch folgende Zeile ein:

    PHP
    1. self::$insertID = self::$db->insert_id;


    dadurch wird das in der Datenbank-Instanz vorhandene Attribut insert_id in unsere Variable insertID geschrieben (die unterschiedliche Schreibweise ist hier reine persönliche Vorliebe - natürlich könnte man die Klassenvariable auch $insert_id nennen).



    Schritt 6: Komplettcode
    Zum Schluss der nun komplette Code nochmal im Überblick:


    Und fertig ist unsere sehr einfache Datenbankklasse - mehr Tutorials folgen :)

  • Hallo,


    das ganze ist ja eine tolle Sache. Habs auch halb wegs verstanden :-)
    Was mir noch fehlt wären einige Anwendungs Beispiele, im Sinne von wie setze ich das Script im laufenden betrieb ein.
    Da ich in sachen PHP noch nicht ganz den Durchblick habe fehlt mir der letzte schubs...


    Wie setze ich den select, insert, update usw Befehl ein.


    Danke Thomas

  • Hi,


    also ich finde das Konzept grundsätzlich nicht gut. Warum eine Klasse mit lauter statischen Methoden? Dann kannst Du es auch gleich prozedural machen anstatt dieses Pseudo-OOP.


    Hier müsste schon ein Aufruf von z.B. insert(...) Exceptions werfen, falls vergessen wurde init() am Anfang des Scripts auszuführen.


    Wenn schon, dann solltest Du das per singleton pattern lösen, und Objektmethoden, dann ist sichergestellt, dass $db auch immer eine mysqli Instanz ist.


    Gruß Talwin

  • Warum nur statische Methoden verwendet werden, wird doch im Beitrag erklärt.


    Eine Klasse, die sowieso nur ein mal aufgerufen wird und zudem noch keine individuellen Zusammenhänge speichert (wie zum Beispiel die Farbe eines bestimmten Autos), eignet sich wunderbar als rein statische Klasse.


    Würde man eine nicht statische Klasse instanziieren um nur eine der integrierten Funktionen aufzurufen, würde auch Speicher für die restlichen Funktionen der Klasse reserviert werden. Auch das ist bei statischen Klassen nicht der Fall.


    Zudem richtet sich die Anleitung (wie beschrieben) an Anfänger und soll eine Hilfestellung sein, keine perfekte Lösung. Ansonsten wären es auch nicht 221 Zeilen, sondern 2210.

  • Warum nur statische Methoden verwendet werden, wird doch im Beitrag erklärt.

    Naja, aber das ist trotzdem kein gutes Design, auch wenn es "erklärt" ist. Die Klasse hat rein gar nichts mit OOP zu tun, sondern das ist rein prozeduraler Stil.


    Eine Klasse, die sowieso nur ein mal aufgerufen wird und zudem noch keine individuellen Zusammenhänge speichert (wie zum Beispiel die Farbe eines bestimmten Autos), eignet sich wunderbar als rein statische Klasse.

    Das stimmt so nicht. Die mysqli Instanz ist sehr wohl logisch gesehen viel eher eine Instanzvariable als eine statische Klassen-Variable. Genauso z. B. die insertID.


    Würde man eine nicht statische Klasse instanziieren um nur eine der integrierten Funktionen aufzurufen, würde auch Speicher für die restlichen Funktionen der Klasse reserviert werden. Auch das ist bei statischen Klassen nicht der Fall.

    Was soll das denn für ein Argument sein? Speicher für Methoden reserviert? Sorry, was für ein Nachteil soll das sein?


    Zudem richtet sich die Anleitung (wie beschrieben) an Anfänger und soll eine Hilfestellung sein, keine perfekte Lösung. Ansonsten wären es auch nicht 221 Zeilen, sondern 2210.

    Naja, aber man kann doch Kritik äußern? Ich denke, dass gerade Anfänger, die meist auch OOP lernen, es zumindest so nicht gezeigt bekommen sollten. Außerdem hieraus z. B. ein Singleton pattern zu machen würde nicht wirklich 10 Mal so viele Codezeilen bedeuten...

  • Man kann das Ganze bis zum jüngsten Tag diskutieren. In erster Linie kann man 2 Sachen sagen:


    Es ist ein Stück weit Geschmackssache.
    Es kommt immer darauf an.


    Zu "gutem Design", wie du es nennst, zählen weder statische Klassen, noch Singleton Klassen. Hier auch ein weiteres Beispiel, worüber man diskutieren könnte: "Design" hat für mich mit Programmierung nichts zu tun, trotzdem binde ich dir nicht auf die Nase, dass "Form" viel besser klingt.


    Es ist jedoch mit Fakten belegt, dass eine statische Klasse im Gegensatz zu einer Singleton Klasse nicht nur deutlich schneller ist (wir sprechen hier von teilweise gut 62% Unterschied), sondern auch weniger Speicher allokiert. Die obige satische Klasse etwa 40% weniger als das Singleton-Äquivalent. Die Singleton Klassen holen erst dann auf, wenn sie wahnsinnig oft aufgerufen werden. Dabei sprechen wir je nach Ausmaß der Klasse von mindestens 1.000.000 Aufrufen für eine Annäherung. Der Speicherverbrauch ist unabhängig der Aufrufe höher.


    Hinzu kommt die Übersichtlichkeit des Codes und die Redundanz. Die Singleton Klasse braucht eine Funktion mehr und diese muss bei Jedem Aufruf mitgeschrieben werden. Solch redundanter Code sollte vermieden werden.


    In bestehende Objekte lassen sich beide Varianten nur mit zwei zugedrückten Augen einbinden und sowohl Geschwindigkeits- als auch Speichertechnisch fährt eine Standard PHP Klasse beide der hier diskutierten Varianten an die Wand. Außerdem kommt es bei beiden Varianten schnell zu Problemen im Scope, wenn man wirklich Objektorientiert arbeitet.


    Warum mehr Speicherverbrauch schlecht ist erkläre ich nur ungerne. 200 kB mehr Speicher für eine Klasse mit identischem Ergebnis machtn icht viel aus. Wenn man von 200 kB Grundbedarf, 300 kB Static-Bedarf und 500 kB Singleton-Bedarf ausgeht, verbrauchen 8192 parallele Seitenaufrufe auf Seite des Singleton 5.734.400 kB Speicher, auf Seite der statischen Klasse 4.096.000 kB. Bei 4 GB verfügbaren Arbeitsspeicher für die eigenen PHP Skripte, würde die Singleton-Variante also deutlich früher zur Überlastung des Servers führen. Eigentlich selbsterklärend.


    Mehr Infos zur Laufzeit findest du auch hier.


    Das sind Themen mit denen sich niemand beschäftigen muss, der ein Gästebuch für seine Urlaubsseite programmiert und darüber kann man (wie gesagt) ewig diskutieren.

  • Hallo,


    vielen Dank für dieses Tutorial,
    ich habe allerdings ein Problem:



    Ich habe den kompletten Code übernommen,
    Der DB-Zugriff klappt, ich kann auch Daten auslesen,
    wenn ich per insert Daten in die DB schreiben will
    bekomme ich immer eine Fehlermeldung.


    public static function escape($string) {
    return
    self::$db->real_escape_string($string);
    }


    real_escape_string($string); - verursacht den Fehler.


    Da ich momentan leider nicht zu haus bin kann ich die Fehlermeldung
    nur aus der Erinnerung wiedergeben.
    Sinngegmäß wird der Zugriff auf ein nicht vorhandenes Objekt angemeckert.
    Ich hänge seit einigen Tagen an diesem Problem, habe mich schon totgegoogled, komme keinen Schritt weiter.
    Falls du das ließt und Zeit/Lust hast etwas dazu zu schreiben wäre ich sehr dankbar.


    Im Voraus vielen Dank und viele Grüsse


  • Hallo,


    danke für den Beitrag. Jedoch habe ich Schwierigkeiten das ganze einzubinden.
    Ich habe eine DB.php-Classe erstellt, in der der Code aus diesem Post drin steht.
    Nun möchte die Datenbankabfrage in einer anderen nicht statischen Klasse durchführen, was allerdings nicht klappen will.
    Ich habe das DB::init(... auch schon in der index.php recht weit am Anfang reingepackt, aber das bringt nichts.
    Das hier in dieser Klasse in den Constructor reinzupacken funzt auch nicht. PhpStorm meckert bei dem DB:select immer rum, dass
    DB eine nicht definierte const und select eine undefinierte Funktion select sei.
    Anbei der Code aus der Klasse...
    Was kann man da machen, damit die Datenbankabfrage durchgeführt wird? Ich bin ein wenig ratlos...


    Viele Grüße,
    Slini11