Datenbanken & Informationssysteme II Tutorium zum Aufbau einer objektrelationalen Abbildungsschicht in PHP (Version 2.8 vom 18.11.2016) • Einleitung Im folgenden wird die Erstellung einer Klasse zur Realisierung einer OR-Schicht erläutert. Die Klasse enthält die Basisfunktionalität zum Anlegen, Auslesen, Ändern und Löschen von Datensätzen (konkret Schiffe). Das vorliegende Tutorium beschreibt die Implementierung einer möglichen Zugriffsschicht so wie sie dann auch im nächsten Übungsblatt als Programmierrahmen vorgegeben ist. • Datenbank Zu Beginn legen wir erst mal die Datenbank segeltoern mit der Tabelle Schiff an. Dazu rufen wir in einem DOS-Fenster das Kommandozeilentool mysql.exe auf1: mysql.exe -u root Anschließend geben wir folgende Statements an: drop database if exists segeltoern; create database segeltoern; use segeltoern; CREATE TABLE schiff ( s_id bigint(20) NOT NULL, s_name varchar(20) NOT NULL, s_laenge double NOT NULL, s_plaetze int(11) NOT NULL, PRIMARY KEY (s_id) ); Danach tragen wir mal zwei Schiffe ein: insert into schiff (s_id, s_name, s_laenge, s_plaetze) values(1, ’Fury’, 12.6, 7); 1. alternativ kannst du auch eine Oracle Datenbank verwenden. Andreas Schmidt PHP-Fingerübungen 1/11 Datenbanken & Informationssysteme II insert into schiff (s_id, s_name, s_laenge, s_plaetze) values(2, ’Olga II’, 15.1, 8); Auflisten der beiden Datensätze mit: select * from schiff; • CRUD-Schicht Anschließend legen wir eine Datei mit dem Namen CRUD_Schiff.php an und erstellen die folgende Klasse CRUD_Schiff mit Konstruktor, Getter-/Setter-Methoden sowie einer Methode __toString() zun Anzeigen eines Datensatzes: <?php class CRUD_Schiff { protected $id; protected $name; protected $laenge; protected $plaetze; /** * Konstruktor */ function __construct($id, $name, $laenge, $plaetze) { $this->id = $id; $this->name = $name; $this->laenge = $laenge; $this->plaetze = $plaetze; } /* * GETTER */ public function getId() { return $this->id; } Andreas Schmidt PHP-Fingerübungen 2/11 Datenbanken & Informationssysteme II public function getName() { return $this->name; } public function getLaenge() { return $this->laenge; } public function getPlaetze() { return $this->plaetze; } /* * SETTER */ public function setName($name) { $this->name = $name; } public function setLaenge($laenge) { $this->laenge = $laenge; } public function setPlaetze($plaetze) { $this->plaetze = $plaetze; } /** * toString Funktion */ public function __toString() { $str = "$this->id: $this->name - $this->laenge "; $str .= "$this->plaetze Plaetze"; return $str; } } ## Ende Klasse CRUD_Schiff Andreas Schmidt PHP-Fingerübungen 3/11 Datenbanken & Informationssysteme II Schreib jetzt ein kleines Testprogramm, das eine Schiffsinstanz erzeugt und diese mittels print ausgibt. Als nächstes erstellen wir in der Klasse CRUD_Schiff eine Klassenmethode getAll(), welche alle Schiffe zurückliefern soll: public static function getAll() { // SQL-Statement $sql = "select s_id as id, s_name as name, s_laenge as laenge, s_plaetze as plaetze from schiff"; $list = MDB2_Util::query($sql); $result = array(); foreach ($list as $s) $result[] = new CRUD_Schiff($s['id'], $s['name'], $s['laenge'], $s['plaetze']); return $result; } Die Methode nutzt die Hilfsklasse MDB2_Util, welche du dir von der Homepage herunterladen kannst. Konkret wird die Klassenmethode query(...) genutzt, welche ein SQL-Statement entgegen nimmt und ein Array der Ergebnisdatensätze zurückliefert. Jeder Datensatz wird als Dictionary zurückgeliefert, dessen Schlüssel die (Alias) Namen der Spalten des Select-Statements sind. Um dir die Struktur des Resultats anzuschauen kannst du den Befehl print_r($list); hinter den Methodenaufruf schreiben. Anschließend wird über die Ergebnisdatensätze iteriert und Instanzen der Klasse CRUD_Schiff erzeugt. Damit die Hilfsklasse MDB2_Util genutzt werden kann muss diese zuvor noch eingebunden werden. Dies geschieht zu Beginn der Datei (aber innerhalb des PHP-Blocks) durch den Befehl include 'MDB2_Util.php'; Unsere bisherige Implementierung können wir jetzt durch das folgende Programm testen1: Andreas Schmidt PHP-Fingerübungen 4/11 Datenbanken & Informationssysteme II <?php include 'CRUD_Schiff.php'; include 'MDB2_Util.php'; $dsn = 'mysql://root:@localhost/segeltoern'; MDB2_Util::connect($dsn); foreach (CRUD_Schiff::getAll() as $schiff) echo $schiff,"\n"; MDB2_Util::close(); Das Programm führen wir von der Kommandozeile einer DOS-/oder CYGWIN- Box aus: $ php.exe1 test.php und die Ausgabe sollte in etwa wiefolgt aussehen: 1: Fury - 12.6 7 Plaetze 2: Olga II - 15.1 8 Plaetze Im nächsten Schritt soll die CRUD-Schicht um eine Methode zum anlegen von Datensätzen erweitert werden Dazu erstellen wir eine statische Methode create(...), welche sowohl den Datensatz in der Datenbank anlegt, als auch eine Instanz des Datensatzes zurückliefert: public static function create($name, $laenge, $plaetze) { $id = MDB2_Util::create_id('schiff')+1000; // SQL-Statement $sql = "insert into schiff (s_id, s_name, s_laenge, s_plaetze) values (?, ?, ?, ?)"; // execute query $data = array($id, $name, $laenge, $plaetze); $affected_rows = MDB2_Util::query($sql, $data); if ($affected_rows!=1) die("Fehler: Datensatz konnte nicht angelegt werden!"); return new CRUD_Schiff($id, $name, $laenge, $plaetze); } 1. Wenn du eine Oracle Datenbank benutzt, musst du den Connection-String entsprechend anpassen. (etwa so: oci8://<username>:<passwort>@oracledbwi) 1. Im Poolraum bitte die PHP-Version unter h:/schmidt/db2-php.bat verwenden. Andreas Schmidt PHP-Fingerübungen 5/11 Datenbanken & Informationssysteme II Die Methode erzeugt zu Beginn mittels einer weiteren Hilfsmethode (create_id(...)) der Klasse MDB2_Util eine eindeutige ID für das Schiff und bastelt dann das Insert-Statement mit Platzhaltern (?) zusammen. Anschließend werden in einem Array ($data) die Werte für die Platzhalter eingetragen und das ganze dann wieder über die Methode query(...) in die Datenbank eingetragen. Im Unterschied zu einem Select-Statement, das die Ergebnisdatensätze in einem Array zurückliefert wird im Falle eines insert,update/oder delete Statements die Anzahl der vom SQL-Statement betroffenen Datensätze zurückgeliefert, bei diesem insert-Statement logischerweise 1. Wird eine anderer Werzt zurückgeliefert so ist irgendwas falsch gelaufen und wir geben eine Fehlermeldung aus. Zum Abschluss der Methode wird noch eine Instanz mit den Werten erzeugt und diese zurückgeliefert. Als nächstes werden wir eine Methode zum löschen eines Datensatzes implementieren. Dies wird durch eine Instanzenmethode realisiert. public function delete() { // SQL-Statement $sql = "delete from schiff where s_id = ?"; // execute statement $result = MDB2_Util::query($sql, array($this->id)); // error handling if ($result != 1) die("Fehler: Schiff ($id) wurde nicht aus der Tabelle gelöscht"); unset($this); } Die Methode ist relativ selbsterklärend, lediglich das unset($this) am Ende sei noch etwas genauer erläutert: unset(...) zerstört die übergebene Variable (hier: die Instanz selbst), so dass diese später vom Programm aus nicht mehr verwendet werden kann. Als nächstes soll von dir die statische Methode getByID($id) implementiert Andreas Schmidt PHP-Fingerübungen 6/11 Datenbanken & Informationssysteme II werden. Die Methode soll genau eine Instanz zurückliefern, die durch die ID ($id) spezifiziert wird. Nimm als Vorlage die zuvor implementierte Methode getAll() und ändere sie entsprechend ab. Wird kein Schiff mit der entsprechenden ID gefunden, so brich mit einer Fehlermeldung ab. Mittels der setter-Methoden kann eine Instanz modifiziert werden, die Änderungen werden jedoch nicht in die Datenbank übertragen. Dazu benötigen wir eine Methode update(), welche den Zustand der Instanz mit der Datenbank synchronisiert: public function update() { // SQL-Statement $sql = "update schiff set s_name = ?, s_laenge = ?, s_plaetze = ? where s_id = ?"; // Parameter array $data = array($this->name, $this->laenge, $this->plaetze, $this->id); // execute query $result = MDB2_Util::query($sql, $data); } Soweit zur Implementierung einer einfachen OR-Schicht für eine Klasse. Eine weitere sinnvolle Funktionalität liefert beispielsweise die Klassenmethode getByCondition($cond). In der einfachsten Implementierung wird die Bedingung direkt in Form einer SQL-Bedingung formuliert und innerhalb der Methode an das Select-Statement angehängt. Beachte aber, dass dies der SQL-Injection Tür und Tor öffnet, wenn man nicht vorsichtig ist und die Bedingung auf „gemeine Zeichen“1 hin überpürft. • 1:n-Beziehung Als nächstes soll die Datenbank zusätzlich um eine Tabelle Hafen erweitert werden. Gleichzeitig soll jedes Schiff einen Heimathafen besitzen. Hierbei handelt es sich um eine klassische 1:n-Beziehung, die wie wir wissen mittels Fremdschlüssel in der Datenbank realisiert wird. Die Befehle zum Anlegen der Tabelle Hafen, dem hin1. Beispielsweise Anführungszeichen und Kommentare, wenn die Zeichenkette welche die bedingung forrmuliert auch aus Benutzereingaben aufgebaut wird. Andreas Schmidt PHP-Fingerübungen 7/11 Datenbanken & Informationssysteme II zufügen des Fremdschlüssels s_hafen_fk und zweier Beispieldatensätze lauten wiefolgt: CREATE TABLE hafen ( h_id bigint(20) NOT NULL, h_name varchar(50) NOT NULL, h_ort varchar(50) NOT NULL, h_anlegeplaetze int(11) NULL, PRIMARY KEY (h_id) ); ALTER TABLE schiff add s_hafen_fk bigint(20) references hafen(h_id); insert into hafen (h_id, h_name, h_ort) values(1, 'English Harbour','Nelsons Dock (Antiqua)'); insert into hafen (h_id, h_name, h_ort) values(2, 'Marine de Fort','Point a Pitre (Guadeloupe)'); Als nächstes muss nun die zuvor entwickelte OR-Schicht der Klasse CRUD_Schiff um dieses Attribut erweitert werden. Dazu muss dass Attribut als Instanzenvariable in der Klasse definiert werden, im Konstruktor Erwähnung finden und auch überall dort wo SQL-Statements vorkommen (außer natürlich bei delete()). Führe diese Änderungen zur Übung durch. Überprüfe deine Modifikationen1 indem du einem der beiden Schiffe einen Heimathafen zuweist und das Testprogramm durchlaufen lässt. Erweitere gegebenenfalls das Testprogramm. Erstelle weiterhin die Klasse CRUD_Hafen mit den notwendigen Instanzenvariablen, einem Konstruktor, der Getter-Methode getId(), einer statischen Methode getById($id)2 und der Methode __toString(). 1. Wird das zusätzliche Attribut in die Datenbank eingetragen und wieder ausgelesen ? 2. Funktionalität analog zu der der Klasse CRUD_Schiff. Andreas Schmidt PHP-Fingerübungen 8/11 Datenbanken & Informationssysteme II Als nächstes erstellen wir eine Methode getHafen(...), welche eine Hafeninstanz zurückliefern soll. Die Implementierung sieht wiefolgt aus: public function getHafen() { if ($this->hafen_fk) return CRUD_Hafen::getById($this->hafen_fk); else return null; } Die Implementierng überprüft zuerst, ob bei der Instanz die Instanzenvariable hafen_fk gesetzt wurde (andernfalls hat das Schiff keinen Heimathafen). Ist dies der fall so wird die Klassenmethode getById(...) der Klasse CRUD_Hafen aufgerufen, welche die Hafeninstanz zurückliefert. Analog dazu kann enine Methode setHafen($hafen_instanz) definiert werden, welche einem Schiff einen Heimathafen zuordnetm (siehe Übnungsblatt 2). Soll nun zusätzliche Applikationslogik mit hinzu programmiert werden, so geschieht dies nicht in der CRUD-Klasser, sondern in einer von der CRUD-Klasse abgeleiteten Klasse. Die minimale Implementierung sieht wiefolgt aus: <?php require_once('CRUD_Schiff.php'); class Schiff extends CRUD_Schiff { ?> } Existiert eine abgeleitete Applikationslogikklasse so muss überall in der CRUDSchicht wo Instanzen der CRUD-Schicht erzeugt werden, die Erzeugung von Instanzen der abgeleiteten Klasse erfolgen - sonst geht die schöne Appliationslogik gleich wieder flöten ;-). Andreas Schmidt PHP-Fingerübungen 9/11 Datenbanken & Informationssysteme II • Dictionary als Konstruktorparameter: Setzt man beim Konstruktor statt mehrerer Parameter einen assoziativen Array ein, so kann man die Realisierung der CRUD-Schicht etwas vereinfachen. Beispielswiese sieht der Konstruktor der Klasse CRUD-Schiff dann wiefolgt aus: function __construct($dic) { $this->id = $dic['id']; $this->name = $dic['name']; $this->laenge = $dic['laenge']; $this->plaetze = $dic['plaetze']; $this->hafen_fk = $dic['hafen_fk']; } Der Vorteil bei dieser Implementierung ist, dass bei Select-Anfragen die Ergebnisdatensätze in Form eines Dictionaries zurückgeliefert werden, und man deshalb folgendes schreiben kann: $class ='Film'; $res = $stmt->execute($data); $results = array(); while($row = $res->fetchRow(MDB2_FETCHMODE_ASSOC)) $results[] = new $class($row); return $results; Wie man sieht ist innerhalb der Schleife, welche die Datensätze aus der datenbank abholt und die Instanzen erzeugt kein klassenspezifischer Code mehr vorhanden. Alle Konstruktoren besitzen einen Parameter (das Dioctionary) und das Erzeugen der Instanzen der entsprechenden Klasse kannn durch den Parameter $class erfolgen. Dieser sachverhalt erlaubt uns die Definition einer weiteren Methode in der Klasse MDB2_Util, die wie query(...) funktioniert, jedoch bereits Instanzen der angegebenen Klasse erzeugt. Dies führt zu einer Reduktion des notwenigen Codes und zu einer höheren Performance, da die liste der Ergebnisdatensätze kein zweites mal (zum Erzeugen der Instanzen) durchlaufen werden muss. So reduziert Andreas Schmidt PHP-Fingerübungen 10/11 Datenbanken & Informationssysteme II sich zum Beispiel der Code für die Methode CRUD_Schiff::getAll() auf: public static function getAll() { // SQL-Statement $sql = "select s_id as id, s_name as name, s_laenge as laenge, s_plaetze as plaetze from schiff"; return MDB2_Util::object_query($sql, 'Schiff'); } einfach, oder ? Nach diesem Prinzip ist auch der Programmierrahmen für die 2. Übung aufgebaut. Andreas Schmidt PHP-Fingerübungen 11/11