← Journal · Archiv

Ein HowTo über DBObjects oder Der heilige Gral der Datenbank-Anwendungsprogrammierung: Das Object-Relational-Mapping

March 05, 2007

These: Bei Datenbankanwendungen arbeitet man unweigerlich mit Resultsets.
Antithese: Resultsets sind aus dem letzten Jahrhundert. Der heilige Gral der Datenbankanwendungsprogrammierung sind DBObjects (oder auch als Object-Relational-Mapping bekannt)!
DBObjects sind eine Abstraktionsebene zum objektorientierten Abbilden von relationalen Datenbanken. Ziel ist nicht eine Abstraktionsebene zu schaffen um den darunter liegenden Datenbankserver austauschbar zu machen, als vielmehr eine Typisierung der Datenbank vorzunehmen.

In diesem How To werde ich DBObjects anhand von PHP zeigen. Es lässt sich aber ohne weiteres auf alle objektorientierte Programmierpsrachen übertragen.

Das wichtigste Vorweg: Ein DBObject ist eine Klasse und repräsentiert ein Record aus einer Tabelle.
Üblicherweiße sind alle Felder, die in der Tabelle vorhanden sind, auch typisiert in der Klasse vorhanden. Im besten Fall als Property, hier in meinem Beispiel (Benutzer-DBObject) aber als public Variablen.

class User
{
  public $ID;
  public $UserName;
  public $Password;
  public $EMail;
}

Um diese Felder initial zu füllen, bedarf es einer Load-Funktion die aus einem Datenbank-Record, die Felder in die Variablen kopiert.

 /* Load from recordset */
function Load($dbrow)
{
  if ($dbrow != NULL)
  {
    $this->ID = $dbrow["ID"];
    $this->UserName = $dbrow["UserName"];
    $this->Password = $dbrow["Password"];
    $this->EMail = $dbrow["EMail"];
  }
}

Die Load-Funktion wird auch vom Constructor aufgerufen, der das an ihn übergebene Recordset durchreicht.

/* Constructor */
function User($dbrow)
{
  if ($dbrow != null)
  {
    $this->Load($dbrow);
  }
}

Nun können wir bereits aus einem Recordset ein DBObject laden. Speichern sollten wir aber auch können. Hier muss man unterscheiden, zwischen neuen DBObjects und geladenen DBObjects. Ist ein INSERT notwendig oder ein UPDATE.

function Store()
{
  if ($this->ID == 0)
  { # INSERT
    $insert = query(
      sprintf("INSERT INTO user (UserName, EMail) VALUES (
      '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s',
      '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s',
      '%s', '%s')",
        addslashes($this->UserName),
        addslashes($this->Password),
        addslashes($this->EMail),
        addslashes($this->mobile));

      $this->ID = $insert["insert_id"];
  }
  else
  { # UPDATE
    query(
      sprintf("UPDATE user SET UserName='%s', Password='%s',
      EMail='%s' WHERE ID = %s",
        addslashes($this->UserName),
        addslashes($this->Password),
        addslashes($this->EMail),
        $this->ID);
  }
}

Query ist eine eigene Funktion, die eine SQL-Abfrage als Argument aufnimmt und ein Resultset ausspuckt, das u. a. auch den Primärschlüssel, den mysqli bei einem INSERT zurückliefert, beinhaltet. Die addslashes sind einfach zum “Escapen” von Sonderzeichen die im SQL-Syntax benützt werden.

Beispiel zum Ändern eines Records:

$myUser = User($dbrow);
$myUser->Password = "streng geheim";
$myUser->Store();

Wer will, kann natürlich statt hard coded SQL statements auch Stored-Procedures verwenden.

So weit so genial. Bereits jetzt haben wir eine Klasse die wir mittels eines Recordsets kreieren können. Damit das Erstellen solcher DBObjects jetzt zu erleichtern, gehen wir weiter den objektorientierten Weg. Wir erstellen unsere Klasse via Factoring. Damit alles auf einem Haufen ist, schreiben wir dazu als Beispiel drei Static Functions in die User-Klasse.

/* Factoring */
public static function GetAll()
{
  $items = query("SELECT * FROM user;");

  while($dbrow = $items["rows"]->fetch_array(MYSQLI_ASSOC))
  {
    $object = new User($dbrow);
    $objects[] = $object;
  }

  return $objects;
}

public static function GetByUserName($UserName)
{
  $user_name = str_replace('%', 'x', $user_name);
  $item = query(sprintf("SELECT * FROM user WHERE
    UserName LIKE '%s' LIMIT 1", $UserName));
  $dbrow = $item["rows"]->fetch_assoc();

  $object = new User($dbrow);

  return $object;
}

public static function GetByID($ID)
{
  $item = query(sprintf("SELECT * FROM user WHERE ID = %s", $ID));
  $dbrow = $item["rows"]->fetch_assoc();

  $object = new User($dbrow);

  return $object;
}

Static bedeutet, wir können die Funktion aufrufen, ohne eine Instanz der Klasse zu erzeugen. In PHP sieht das nun so aus:

$users = User::GetAll();
foreach($users as $user)
{
  printf("%s hat die ID %s. n",
    $user->UserName,
    $user->ID);
}

In dem Array $users befinden sich nun alle Benutzer aus der Datenbank als DBObject. Mit einer foreach-Schleife können wir sie uns ausgeben lassen.

Und unser Beispiel von vorhin, in dem wir ein Passwort geändert haben, lässt sich viel edler handhaben.

$myUser = User::GetByID(101);
$myUser->Password = "streng geheim";
$myUser->Store();

Das objektorientierte Abbilden von relationalen Datenbanken eignet sich natürlich auch wunderbar um Relationen zwischen Tabellen darzustellen. Eine 1:n-Verbindung kann als Property (In PHP Attribute) in einer der beiden Klassen oder sogar in beiden Klassen vorhanden sein.
Da in PHP Propertys ziemlich schwer und instabil zu handhaben sind, verwende ich einfach eine Function.

// User 1:n Items
function Items()
{
  return Item::GetByUser($this->ID);
}

Fazit: Wer DBObjects verwendet, wird zwar am Anfang jeder Entwicklung über den hohen Schreibaufwand ächzen, wird aber bei der späteren Entwicklung der Anwendung schneller voran kommen und vor allem super sauberen, kompakten Code abliefern. Bei PHP ein nicht zu unterschätzender Vorteil.
Ein Schritt näher an der Perfektion.

6 Kommentare

Simon Stiefel ·

Für PHP gibt es bereits fertige Lösungen, die soetwas automatisch machen. Vorallem die Kombination Propel/Creole (http://www.phpdb.org) ist recht mächtig.
Es genügt hier, das Datenbank-Layout als XML-Datei zu schreiben. Das Anlegen der Datenbank und vorallem das Erstellen der PHP-Klassen läuft danach vollautomatisch.
Auch ist der Umstieg auf andere RDBMS relativ einfach.
Ist auf jeden Fall einen Blick wert!

lemming ·

Hmm, das Propel-Beispiel auf der Homepage ist ja genau mein Beispiel, das ich auch anführe. Ich brauche auch nur zwei Zeilen
Das Rad iwrd zwar auch 10x neu erfunden, ehe es alle kapiert haben, aber warum eine vorgefertigte Klasse verwenden, wenn’s doch wie oben beschrieben so einfach selber geht.

Btw. toll dass sich nicht nur andere Gedanken über DBObjects machen. Viva DBObjects!

lemming ·

Gerade gesehen dass Propel locker Transactions kann, dass wäre ein Grund sich Propel zuzulegen.

Uwe ·

DBObjects rocken! Und DBObjectsManagers erst! Der neue Trend aus den USA! Neu, jetzt mit 50% mehr Ausrufezeichen!

Martin ·

DBObjects, eine wirklich neue Idee ist das aber nicht oder? Fuer viele Programmiersprachen liegen ja schon Frameworks dafuer bei. Auch wenn das dort nicht DBObjects, sondern ORM aka Hibernate, ActiveRecord usw. nennt.

Ziel ist es dabei auf jeden Fal eine Abstraktion zur Abfragesprache und in der Schicht darunter eine Abstraktion zum DBMS zu machen.

lemming ·

Ja, ist das gleiche. Nur warum verwendet es dann keiner? Warum ist Propel nicht in PHP5 integriert?

Kommentar hinterlassen