Problem
Man entwickelt fast immer eine Webapplikation mit Datenbank-Zugriff und muss dann immer wieder die gleichen Funktionen einbinden, damit der Zugriff klappt. Dabei benötigt nicht jede Web-Applikation zwingend eine Datenbank (DB). Dann die Tabellen installieren und eventuell mit Daten vorbelegen. Die Anwendung bearbeitet die DB. Oft wird eine Applikation auch ohne DB-Zugriff aufgerufen, zum Beispiel bei kleineren Aufgaben (Loginmaske zeigen). Hier wirkt der am Anfang eingebundene DB-Verbindungsaufruf störend, bzw mindert die Performance. Gerade gut laufende Applikationen werden immer wieder erweitert. Diese Erweiterungen sind oft mit DB-Manipulationen behaftet. Nun muss der Entwickler Update-Pakete schnüren, welche auch noch die DB anpassen. Die Komplexität beim Update, bzw der Installation wächst in Form von Installationsschritten, die der Betreiber aktiv durchführen muss. Dazu kann es passieren, dass man eventuell die DB wechseln muss und dann alle Funktionsaufrufe, welche auf eine bestimmte DB zugeschnitten sind, ändern. Bei grossen Applikationen kann dies schon sehr aufwendig sein. Gerade DB-Zugriffe sind oft vom gleichen Schema: Daten holen, Daten in Array packen, Daten durchgehen. Diese relative formale Angelegenheit, verlängert nur den Code. Wenn man dann noch Messungen oder andere vor- bzw. nachgelagerte Funktionen ausführen möchte, werden auch diese zu wiederholenden Elementen, die den Code verlängern und damit ebenfalls zu Lasten der Performance gehen. Hier sei angemerkt, dass nicht jeder kurze Code Performance bringt und sich auf wiederholenden Ausführungen von Codezeilen bezieht.
Idee
Um die möglichen Probleme in den Griff zu bekommen bietet es sich von je her an, die DB-Zugriffe zu kapseln. Alter Hut und dennoch immer wieder neu. Durch die Kapselung der eigentlichen DB-Zugriffe vermindern wir die Performance, da mindestens ein weiterer Funktionsaufruf erzeugt wird. Dennoch gewinnen wir viel an Flexibilität und Entwicklungsgeschwindigkeit. Das Kapseln der Funktionen bedeutet, dass wir eine Wrapper-Funktion, um den eigentlichen Aufruf legen. In unserem Fall eine Funktion, die dann die richtige Funktion aufruft, nach dem einige Vorbereitungen getroffen wurden. Und das Ergebnis zurück liefert, nachdem einige Manipulationen vorgenommen wurden. Diese Wrapper-Funktionen sind auch ideal, um die eigentlichen Aufrufe zu zählen und damit Aufschlüsse über die Performance der Applikation an sich zu geben. Desweiteren wollen wir noch, dass der Verbindungsaufbau nur dann geschieht, wenn wir die DB wirklich brauchen. Dies können wir ideal in der Wrapper-Funktion einbinden. Da bei einem Fehler, die Wrapper-Funktion dies erfährt, können wir auch hier die Fehlerbehandlung integrieren und entsprechend agieren. Dafür binden wir hier eine automatisierte Reparatur-Funktion ein, welche Änderungen an der DB entsprechend vornimmt. Der Anwender bekommt davon nichts mit und die Installation ist spielend einfach und kurz gehalten. Sollte sich nun ein DB-Wechsel anbahnen, muss man lediglich die entsprechende Änderung in der Wrapper-Funktion vornehmen. An dieser Stelle ist ein Ausbau möglich, so dass man gleich die verschiedenen DB-Systeme integriert. Für diesen Post reicht erstmal der Zugriff auf MySQL, da gerade bei der Auswertung der Fehler, noch einiges an Code auf uns zu kommen würde. Dieser aber nur einmal und dann landen diese Funktionen in unser Framework. Somit lohnt sich der Ausbau für den Leser schon. Jeder Entwickler baut im Laufe der Zeit ein eigenes Framework, welches er immer wieder für seine neuen Projekte nutzt.
Umsetzung
Um Klassen zu vermeiden, werden globale Funktionen eingesetzt. Es ist jedem selbst überlassen, wie er seine Wrapper erstellt. Wobei man zugeben muss, dass gerade diese Arbeit wunderbar in einer Klasse realisiert werden könnte. Derzeit traue ich dem Klassen-System von PHP noch nicht soweit, dass es zukunftssicher gelten kann (also auch in 4 Jahren noch so ist, wie derzeit) und verwende Funktionen mit angepassten Namen und globale Variablenzugriffe, damit diese Beispiel-Funktionen immer funktionieren. Der Zugriff erfolgt auf MySQL mittels PHP.
Wir wrappen folgende Funktionen:
- mysql_connect
- mysql_insert_id
- mysql_query
- mysql_affected_rows
Als globale Variable für die DB-Zugriffe definieren wir einen assoziativen Array, der in der Konfigurationsdatei durch den Betreiber gefüllt wird. Diese Variable sollte zu Beginn oder in einer Konfigurationsvariable gesetzt werden.
$system = array();
$system['db']['dbuser'] = "DBuser";
$system['db']['dbuserpw'] = "DBpasswort";
$system['db']['dbname'] = "DBname";
// prefix wird an die Tabellennamen vorangestellt,
// so kann man eine DB mit verschiedenen Tabellen und
// Applikationen nutzen. Man muss nur auf die Eindeutigkeit
// des Prefix pro Applikation achten
$system['db']['prefix'] = "meinPrefix_";
$system['db']['dbserver'] = "localhost";
$system['db']['dbserverport'] = "3306";
$system['db']['connected'] = false; // hilft beim Erkennen, ob DB schon verbunden
Im Gegensatz zum klassischen Entwurf, binden wir die DB-Initialisierung nicht in den PHP Code ein. Beim ersten Aufruf einer DB-Funktion wird die Verbindung aufgebaut. Somit kann man frei entwickeln und die DB erst einbauen, wenn Sie wirklich gebraucht wird. Sehr gut auch für Prototypen-Bau. So kann man die DB unkompliziert einbinden und wieder rückstandlos entfernen. Die folgenden Funktionen sollte man direkt in sein Framework einbinden. So kann man diese immer nutzen, wenn nötig.
// stellt die Verbindung mit DB her
// hier wird absichtlich nicht geprüft "$system[db][connected] == true",
// so kann man eine Verbindung nochmal provozieren
function _db_connect() {
global $system;
$dbv = $system[db][dbserver].":".$system[db][dbserverport];
// Datenbank verbinden
$dbcon = mysql_connect($dbv, $system[db][dbuser], $system[db][dbuserpw]);
if (!$dbcon)
die("Problem in der Datenbankverbindung");
// Datenbank vorauswaehlen
mysql_select_db($system[db][dbname]);
$system[db][connected] = true; // verbindung steht aufschreiben
return $dbcon;
}
// die anzahl der betroffenen Zeilen
function _db_num_rows() {
return mysql_affected_rows();
}
// liefert die ID der letzten INSERT Operation auf der DB
// die $dbcon, kann genutzt werden,
// wenn das Skript mehrere verschiedene DB-Verbindungen aufbauen muss
function _db_sql_insert_id($dbcon) {
if ($dbcon == 0):
return mysql_insert_id();
else:
return mysql_insert_id($dbcon);
endif;
}
// diese Funktion kann man in eine separate Datei auslagern und
// erst einladen , wenn benötigt.
// sie enthält die DB Struktur und wird aufgerufen, bei einem DB Fehler
// so repariert sich das System alleine
// man könnte hier statt die SQL direkt auch generische Informationen ablegen und
// die SQL Befehle zusammen bauen, damit wäre man noch unabhängiger von DB
// die globale Variable $system und der Anteil "tabellen" enthält die Tabellennamen,
// damit das System automatisch den Prefix setzen kann und man einfachere Namen verwenden kann
// als im System genutzt.
function _system_installiereTabellen($force=false)
{
global $system;
$sql = "CREATE TABLE ".$system['db']['tabellen']['besitzer']." (
id int(32) unsigned NOT NULL auto_increment,
PRIMARY KEY (id))";
// mit gesetztem $nichtnochmal flag aufrufen, um endlosschleifen zu vermeiden
_db_query($sql, 0, -1);
$sql = "alter table ".$system['db']['tabellen']['besitzer']." add start int(32) unsigned NOT NULL";
_db_query($sql, 0, -1);
$sql = "alter table ".$system['db']['tabellen']['besitzer']." add email varchar(250) NOT NULL";
_db_query($sql, 0, -1);
$sql = "alter table ".$system['db']['tabellen']['besitzer']." add password varchar(250) NOT NULL";
_db_query($sql, 0, -1);
$sql = "alter table ".$system['db']['tabellen']['besitzer']." add sessionid varchar(32) NOT NULL";
_db_query($sql, 0, -1);
$sql = "alter table ".$system['db']['tabellen']['besitzer']." add aktiv int(1) unsigned NOT NULL";
_db_query($sql, 0, -1);
$sql = "alter table ".$system['db']['tabellen']['besitzer']." add unique index idx1 (email)";
_db_query($sql, 0, -1);
$sql = "alter table ".$system['db']['tabellen']['besitzer']." add index idx2 (sessionid)";
_db_query($sql, 0, -1);
}
// hiermit wird die eigentliche SQL an DB gesendet
// nutzt man, um die SQL direkt zu senden, ohne einen Rückgabewert zu erwarten,
// ausser der last insert ID. Also bei NICHT "select"
// die $dbcon, wird genutzt, wenn mehrere DB-Verbindungen stehen
// $nichtnochmal hilft gegen rekursive endlos-Schleifen
function _db_query($sql, $dbcon=0, $nichtnochmal=0)
{
global $system;
// steht eine Verbindung schon ?
// müsste noch um die Abfrage der dbcon erweitert werden.
if ($system[db][connected] == false)
_db_connect();
// wurde ein Datenbankkonnektor-Resource mitgegeben (Bei mehreren Verbindungen)
if ($dbcon == 0):
$erg = mysql_query($sql);
else:
$erg = mysql_query($sql, $dbcon);
endif;
// prüfen, ob ein INSERT gemacht wurde und liefere dann die last-insert-id
if ($erg):
if (strtolower(substr($sql, 0, 6)) == "insert")
return _db_sql_insert_id($dbcon);
return $erg;
endif;
// fehler prüfen
$errno = mysql_errno();
if ($nichtnochmal > 0):
if (function_exists("_db_fehler_melden"))
_db_fehler_melden("Fehler bei der SQL-Anweisung:<br>".($sql)."<br>".$errno." : ".mysql_error()."<p>");
return 0;
elseif ($nichtnochmal == 0):
// hier muesste noch das protokollieren der SQL_Fehler erfolgen
switch($errno) {
case 1054: // feld existiert nicht
case 1064: // fehler in der SQL-Abfrage
case 1146: // tabelle existiert nicht
// ladePHPDatei("_system_db.php");
_system_installiereTabellen(true);
return _db_query($sql, $dbcon, 1); // nicht noch mal, wenn es wieder schief geht
break;
case 1046: // keine datenbank ausgewählt
die(mysql_error());
break;
default: // alles andere ignorieren
return $erg;
} // ende switch
endif;
return $erg;
}
// SQL Abfrage senden und die Daten abholen und gleich aufbereitet zurück geben
function _db_datenholen($sql) {
$werte = array();
$r = _db_query($sql);
if ($r):
while($daten = mysql_fetch_array($r, MYSQL_ASSOC)):
// prüfen, ob der User nicht schon die Verbindung vom Browser zum Server
// geschlossen hat. Dann könnte man aufhören
// manchmal ist das aber auf keinen Fall gewünscht
// (updates mit vorherigen Daten zum Beispiel)
if (!$system['dontbreakevenconectionaborted'] && connection_aborted())
exit;
$werte[] = $daten;
endwhile;
else:
echo $sql;
endif;
return $werte; // die daten als assoziativen Array (hash) zurück liefern
}
Beispiel
Bindet die notwendigen Framework Dateien ein und setzt eine Abfrage ab.
In unserem Beispiel werden, wenn die DB-Zugriffs-Daten hinterlegt wurden, auch gleich die Tabellen angelegt und zuvor die DB verbunden. Die echo Anweisung wird leer sein, da ja keine Daten in der Tabelle hinterlegt sind. Man könnte dort einen "INSERT"-SQL hinterlegen und den ersten User (default-Admin) oder dergleichen anlegen lassen. Dieser wird beim Bestehen auch nicht gelöscht, da INSERT nur fehlschlagen würde.
<?php
include ("dbvar.php"); // enthält die $system Variable definition
include ("fkt.php"); // enthält das Framework, also die _db_funktionen etc
$sql = "select * from ".$system[db][tabellen][besitzer]." where id = 1";
$daten = _db_datenholen($sql);
echo $daten[0][email];
?>
Fazit
Mit diesen wenigen Code-Stücken kann man in kürzester Zeit eine Applikation schaffen und muss sich über die DB-Zugriffe keine Sorgen mehr machen. Sobald die richtigen Daten in der $system[db] eingetragen sind, kann man die Codezeilen stark reduzieren und hat schnell DB-angebundene Applikationen geschaffen. Grundsätzlich sollte man immer nach einer Vereinfachung suchen und vorallem nach etablierten wiederverwertbaren Code-Elementen Ausschau halten. Mach dein Leben einfacher. Dadurch sinkt die Fehlerquote, da dieser Code immer wieder verwendet wird und so schon erprobt ist und erhöht die Entwicklungsgeschwindigkeit von Applikationen. Jedoch sollte man darauf achten, dass nicht das Framework selbst irgendwann zum Performance Fresser wird, weil es so gross geworden ist. Ich überlege sehr lange, ob es sich lohnt eine Funktion in mein Basis-PHP-Framework einzubinden. Da diese Basis-Datei bei allen Applikationen eingebunden wird. Die Funktionen müssen oft genutzt werden und mir auch eine Erleichterung und einen Geschwindigkeitsvorteil beim Entwickeln bringen. Sollte eine Verbesserung an dieser Datei vorgenommen werden, so profitieren gleich alle Projekte davon.
Saso Nikolov
Keine Kommentare:
Kommentar veröffentlichen