Data Mapper extends OR-Mapper

| 2 Kommentare

Beim Aufbau bzw. der Pflege von datenbankgestützten Anwendungen steht man als Entwickler oft vor der Herausforderung, dass die Datenbankstruktur anfangs nicht optimal geplant wurde oder sich mit wachsenden Anforderungen verändert.

Diese Änderungen ziehen sich bei unzureichender Entkopplung dann oft durch die ganze Anwendung und kosten viel Zeit und Geld.

Also lösen wir ein paar Abhängigkeiten auf und machen uns das Leben etwas einfacher.

Das Ziel ist relativ klar: Wenn wir unsere Datenstruktur ändern, dann wollen wir uns in unserer Anwendung möglichst nicht darum kümmern müssen.

Der Weg dahin ist nicht immer ganz so einfach. Zunächst müssen wir herausfinden, welche Daten wir brauchen und wie wir sie verwenden wollen.

Viele Anwendungen, besonders im Internet, sind personalisiert, d.h. ein Benutzer kann sich anmelden und dann in seinem Bereich arbeiten (Email, Chat etc.). Wir brauchen also die Daten des Benutzers und dabei interessiert es uns nicht, ob diese aus einer Datenbank kommen oder woher sonst. Also bauen wir uns doch einfach ein User-Objekt, das uns all seine Informationen gibt, wenn wir es danach fragen.

Warum sollten wir das überhaupt tun, wenn uns unser OR-Mapper schon so handliche Objekte anbietet? Diese Objekte sind im Allgemeinen analog zur Datenbank strukturiert, das bedeutet, dass sich Änderungen noch immer direkt auf unsere Anwendung auswirken.

An dieser Stelle sollte klar werden, dass wir eine weitere Schicht einbauen müssen. Zwischen unseren OR-Mapper und das User-Objekt muss ein weiterer Mapper eingesetzt werden, um diese Ebenen komplett voneinander zu trennen.

Das Prinzip ist recht einfach. Die oberste Schicht ist das User-Objekt mit seinen Eigenschaften. Dieses ist über den User-Mapper mit der Datenbank oder anderen Quellen verbunden.

Fangen wir doch mal mit dem User-Objekt an.

<?php
class User
{
  protected $_id;
  protected $_nickname;
  protected $_password;

  // getter & setter...
}

Wie du sehen kannst, ist unsere User-Klasse erstmal nur ein einfacher Datencontainer. So soll es auch sein, aber eine Verbindung zu unseren Daten haben wir damit noch nicht. Also her mit unserem User-Mapper!

<?php
class UserMapper
{
  protected $_userTable; // z.B. Zend_Db_Table

  // Konstruktor etc.

  public function find($primary, User $user)
  {
    $row = $this->_userTable->find($primary)->current();
    $user->setId($row->id)
         ->setNickname($row->nickname)
         ->setPassword($row->password);
  }

  public function save(User $user)
  {
    $data = array('nickname' => $user->getNickname(),
                  'password' => $user->getPassword());

    if($user->getId()) {
      $this->_userTable->update($data, 'user_id = ' . $user->getId());
    } else {
      $id = $this->_userTable->insert($data);
      $user->setId($id);
    }
  }
}

Der User-Mapper hat Zugriff auf die benötigten Daten und kann sie dem User-Objekt übergeben und auch wieder speichern. Ich habe absichtlich auf die Fehlerbehandlung verzichtet, damit der Code nicht unnötig komplex wird. Daher gleich der Hinweis, dass die Beispiele nicht produktiv einsetzbar sind!

Nun kann ich über den Mapper im besten Fall schon mal ein Objekt mit Daten befüllen und nutzen.

<?php
$user = new User();
$userMapper = new UserMapper();
$userMapper->find(13, $user);

Ok, super! Aber als Programmierer bin ich bequem und geizig, wenn es um die Anzahl der Zeilen in meinem Code geht. Außerdem will ich mich nicht um den Mapper kümmern müssen und nur mit meinem User-Objekt arbeiten. Dafür erstelle ich mir zunächst eine Basisklasse für meine Datenobjekte.

<?php
abstract class Container
{
  protected $_mapper;  // Instanz des Mappers

  // get/setMapper()
}

Die Basisklasse kann und muss später noch weitere Aufgaben übernehmen, wenn wir z.B. ArrayAccess oder einen Observer benötigen.

Da ich es auch als sehr unhandlich empfinde, meine Objekte manuell zu erstellen und mit Mapper-Instanzen auszustatten, schreibe ich mir noch eine UserFactory dazu.

<?php
class UserFactory
{
  private static $_mapper;  // Instanz des Mappers

  public static function getMapper()
  {
    if(null !== self::$_mapper) {
      self::$_mapper = new UserMapper();
    }
    return self::$_mapper;
  }

  public static function getModel()
  {
    $user = new User();
    $user->setMapper(self::getMapper());
    return $user;
  }
}

Unsere Factory hat zwei Methoden. Wir können uns fertige User-Objekte erzeugen lassen oder uns eine Instanz des UserMappers zurückgeben lassen.

Damit die Factory so funktioniert und wir direkt mit dem User-Objekt arbeiten können, müssen wir die User-Klasse noch von der Container-Klasse ableiten und um ein paar Methoden erweitern.

<?php
class User extends Container
{
  // Rest wie bisher

  public function find($primary)
  {
    $this->getMapper()->find($primary, $this);
  }

  public function save()
  {
    $this->getMapper()->save($this);
  }
}

Die Methoden find() und save() sind Delegates, das bedeutet, dass sie den Aufruf nur weitergeben. Die Logik kann verwendet werden, liegt aber nicht in der Klasse. Ich empfehle den Mapper immer über die Methode getMapper() anzusprechen, da dort auch die Fehlerbehandlung sein sollte, falls kein Mapper gesetzt wurde.

Nun können wir schon etwas einfacher mit unserem User-Objekt arbeiten.

<?php
$user = UserFactory::getModel()->find(13);
$user->setNickname('spongebob')
     ->save();

Bislang haben wir schon einiges geschafft: Unsere Anwendung ist nun unabhängig von der Art der Datenhaltung, da der Mapper das Laden und Speichern verwaltet und dank der Factory brauchen wir auch keine Objekte mehr manuell erzeugen. Dadurch könnte die Factory jetzt auch ein von User abgeleitetes Objekt zurückgeben und wir müssten dennoch keine Zeile im Code anpassen.

Die Verkettung von Methodenaufrufen funktioniert durch das Fluent Interface Pattern, indem unsere Setter einfach $this zurückgeben.

Bislang leitet unser Mapper eigentlich nur alles gerade durch, ein Mapping ist ja noch nicht erforderlich und der Mehrwert vielleicht noch nicht ganz erkennbar.

Nun wollen wir unseren Benutzern die Möglichkeit geben, sich den anderen mit einem kurzen Text vorzustellen. Um nicht jedem User-Datensatz ein Textfeld anzuhängen, werden die Texte in einer separaten Tabelle gespeichert. Zudem soll der Text auch nur dann geladen werden, wenn er auch wirklich gebraucht wird.

Dafür müssen wir die User-Klasse um die Eigenschaft “description”, sowie die passenden Getter und Setter erweitern.

<?php
class User extends Container
{
  // ...
  protected $_description;

  public function getDescription()
  {
    $this->getMapper()->loadDescription($this);
    return $this->_description;
  }

  // setDescription()
  // ...
}

Die Methode getDescription() macht noch etwas mehr, als nur den Beschreibungstext zurückzugeben. Hier wird auch wieder deutlich, warum die Arbeit mit Objekten so übersichtlich sein kann. Statt die UserId als Parameter zu übergeben und den Rückgabewert dann wieder zu speichern, übergeben wir einfach das ganze Objekt und der Mapper macht den Rest.

Momentan hängt unser Code leider noch etwas hinterher, da die Methode loadDescription() in der User-Klasse schon erwartet wird, aber im Mapper noch garnicht existiert. Das ändern wir schnell mal.

<?php
class UserMapper
{
  protected $_userDescTable;

  // ...

  public function loadDescription(User $user)
  {
    $rowset = $this->_userDescTable->fetchAll('user_id=' . $user->getId());
    $row = $rowset->current();
    $user->setDescription($row->description);
  }
}

Auch hier wieder der Hinweis, dass der Code nur den Ablauf zeigen soll und keinerlei Fehlerbehandlung enthält!

Zunächst wird der Datensatz zur UserId geladen und der Beschreibungstext dann an das User-Objekt übergeben. Gewöhnlich würde ich die Id prüfen und ggf. quoten lassen. Da ich mich darauf verlassen kann, dass das User-Objekt immer eine Zahl zurückgibt, kann ich darauf hier verzichten.

Aktuell würde bei jedem Aufruf von getDescription nun eine Anfrage abgeschickt werden. Daher sollte dies durch einen Cache oder zumindest durch ein Flag vermieden werden.

Um unsere Beschreibung nun auch speichern zu können, muss einfach nur die Methode save() des UserMappers entsprechend erweitert werden.

<?php
class UserMapper
{
  // ...
  public function save(User $user)
  {
    // ..
    if(empty($user->getDescription)) {
      $this->_userDescTable->delete('user_id=' . $user->getId());
    } else {
      $data = array('description' => $user->getDescription());
      if(1 != $this->_userDescTable->update($data, 'user_id=' . $user->getId())) {
        $this->_userDescTable->insert($data);
      }
    }
  }
}

Ok, zugegeben, die Erweiterung ist etwas komplexer, dafür wird der Datensatz auch gelöscht, wenn die Beschreibung einen Leerstring enthält.

Natürlich können auch mehrere Modelle miteinander verbunden werden, z.B. wenn zu unseren Benutzern auch Adressen gespeichert werden sollen. Die Address-Klasse, der Mapper und die Factory werden erstmal analog zum User umgesetzt.

Nun müssen wir auch schon wieder die User-Klasse erweitern.

<?php
class User extends Container
{
  protected $_addressId;  // Die AddressId
  protected $_address;    // Instanz von Address
  // ...
  // getter & setter AddressId;

  public function getAddress()
  {
    if(null === $this->_address) {
      $this->_address = AddressFactory::getModel();
      $this->_address->find( $this->getAddressId() );
    }
    return $this->_address;
  }

  public function setAddress(Address $address)
  {
    $this->_address = $address;
    $this->setAddressId( $address->getId() );
    return $this;
  }
}

Zunächst mal speichere ich die AddressId mit dem User zusammen, d.h. er kann in diesem Beispiel wirklich nur eine Adresse haben. Die beiden Methoden sollten eigentlich selbsterklärend sein.

Natürlich sollen Änderungen an der Adresse zusammen mit dem User gespeichert werden. Dafür wird die Methode save() der User-Klasse erweitert.

<?php
class User extends Container
{
  // ...

  public function save()
  {
    if(null !== $this->_address) {
      $this->_address->save();
      $this->setAddressId( $this->_address->getId() );
    }
    $this->getMapper()->save($this);
  }

  // ..
}

Das war es eigentlich auch schon zu dem Thema. Die Umsetzung kann sicherlich je nach Datenstruktur recht komplex und zeitintensiv sein. Ich bin mir jedoch recht sicher, dass man diese Zeit bei folgenden Arbeiten wieder einsparen kann und dadurch auch Änderungen an der Tabellenstruktur entspannter werden.

2 Kommentare

  1. Pingback: Es ist ein Model und es sieht gut aus « ebene7

  2. Pingback: Es ist ein Model und es sieht gut aus – Das Grundgerüst « ebene7

Hinterlasse eine Antwort

Pflichtfelder sind mit * markiert.


Schlagwörter: A/B-Test, AbstractType, Adapter, AddOn, Administration, Ajax, Amazon, Animation, Annotations, Anonyme Klasse, Ant, Apache, API, Array, ArrayAccess, Attachment, Auftrag, Ausbildung, Auswertung, Authentifizierung, AutoLoader, AWS, Bedienung, Bedingung, Benchmark, Berechtigung, Berlin, Bildbearbeitung, Bildschirmfoto, Blog, Blogroll, BOM, Bootstrap, Bot, Browser, Bugtracker, Byte Order Mark, Bücher, Cache, CakePHP, Call-Center, Callback, CamelCase, Canvas, Captcha, CDN, Cheatsheet, CLI, Clickout, Closure, Cloud, CodeSniffer, Collection, Community, Comparator, Config, Contest, Controller, Converter, CouchDB, Countable, Cronjob, CRUD, CSS, CSV, CustomLibrary, Custom_Model, Daemon, Data Mapper, Datei, Datenbank, Datenstruktur, Datentypen, Dating, Datum, Debug, Decorator, Dekorierer, Design, Design Patterns, Doctrine, Dokumentation, Dump, Duplikat, each, EC2, Eclipse, Email, Entwicklung, Entwurfsmuster, Enum, Erweiterung, Event, Eventhandling, Exception-Handling, Extension, Facebook, Factory, Fallback, Fehler, Fehlermeldung, Filter, Firefox, Flash, flexigrid, Foreach, Formatierung, Formular, Framework, FTP, Funktion, Futon, ga:pi(), Getter, Google Analytics, Hash, Hash-Bang, Header, htaccess, HTML5, htpasswd, HTTP, HTTPS, IDE, If, Implementierung, InnoDB, Interceptor, Interface, Internet Explorer, isset, Iterator, Java, JavaScript, Job, jQuery, Kommentar, Konfiguration, Konsole, Kontrollstruktur, kopieren, kostenlos, Kundenbetreuung, Late Static Binding, Layout, Links, Linux, Listeners, Lizenz, Logging, Löschen, Magento, Magic Methods, Manual, ManyToMany, Marketing, Methode, Model, Monolog, MVC, MySQL, NetBeans, Network, Nirvanix, Objekt, Observable, Observer, OneToMany, Online Tool, OOP, Open Source, Operator, OR-Mapper, Order, ORM, O’Reilly, Parameter, Partnersuche, Passwort, Performance, PHP, php.ini, PHP hates me, phpMyAdmin, PHPUnit, Plugin, Popup, Proxy, Prüfsumme, Prüfung, QR-Code, Qualitätssicherung, Query, Queue, Redesign, Refactoring, Reflection, Request, Response, Responsive Design, Rest-API, Rockstar, Rollback, Routing, S3, Samba, Scheifen, Schleife, Schutz, Screenshot, Secure Shell, Selbstreferenz, Server, Setter, setTimeout, Shop, Sicherheit, Sicherung, Sichtbarkeit, Singleton Pattern, Skin, SOAP, Social Network, Software, Sortierung, Sourcecode, Spam, Speicherproblem, Spickzettel, SPL, Splittest, SSH, SSL, Stammtisch, Statement, static, Statistik, Status, Stellvertreter, Strategy Pattern, Stream, String, Stuttgart, Stylesheet, Subversion, Sun VirtualBox, Support, SVN, Switch, Symfony, Symfony2, Symfony Live, Tag, Template, Template Method, Ternär Operator, Testing, Theme, Thumbnail, Tool, Tour, Tracking, Twig, Twitter, Type-Cast, Ubuntu, Umwandlung, Underscore, unset, Update, Upload, Url, User Story, Validierung, Vererbung, Versionskontrolle, Versionsnummer, Verzweigung, Video, Videospiel, Virtualisierung, Visitor Pattern, Vorschaubild, walk, Warteschlange, Webserver, Webservice, Weiterleitung, Werkzeug, Windows, WindowsAzure, WordPress, Wrapper, Writer, XML, Youtube, Zeitschleife, Zeitsteuerung, Zend Framework, Zend_Application, Zend_Cloud, Zend_CodeGenerator, Zend_Http_Client, Zend_Reflection, Zend_Service, ZPress, Zugangskontrolle, Zugriffsmethode