Früher oder später geht es in jeder Webapplikation darum Daten zu speichern. In einer nach dem MVC-Prinzip orientierten Anwendung wird die ein oder andere Form eines ORM zum Einsatz kommen. Solche ORM (z. B. Doctrine, Eloquent) folgen entweder dem Paradigma des Active Record oder das dem Data Mapper. In diesem Artikel wird es um die Unterschiede dieser beiden gehen.
Was ist überhaupt ein ORM?
ORM ist die Abkürzung für „object-relational mapping“ zu deutsch Objektrelationale Abbildung. Damit ist in der Anwendung eine Abstraktionsebene gemeint, die die relationalen Daten aus einem Speichermedium (z. B. Datenbank oder Datei) mit Objekten im Programmcode verknüpft.
Ein ORM enthält die notwendigen Logiken für die üblichen Operationen, die wir mit einem Datenobjekt anstellen wollen. Diese werden für gewöhnlich mit CRUD abgekürzt. CRUD steht für create, read, update, delete also anlegen, laden, bearbeiten und löschen. Im Falle einer Datenbank übernimmt das ORM also die SQL-Befehle für uns.
Das Active Record Pattern
Beim Ansatz des Active Record gibt es eine Basisklasse, die vom ORM zur Verfügung gestellt wird, und von der alle unsere Models erben. Diese Basisklasse enthält dann zumindest für jede CRUD-Operation eine Methode.
Ein entsprechendes Interface könnte zum Beispiel so aussehen:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
interface ModelInterface { /** * Laden mehrerer Datensätze. * * @param mixed $parameters Suchkriterien. * * @return ModelInterface[] */ public static function find($parameters = null); /** * Laden eines Datensatzes. * * @param mixed $parameters Suchkriterien. * * @return self */ public static function findFirst($parameters = null); /** * Speichern dieses Datensatzes. */ public function save(); /** * Löschen dieses Datensatzes. */ public function delete(); } |
Es hat sich eingebürgert die Methoden zum Laden der Objekte mit find
zu benennen. Gespeichert werden die Datensätze normalerweise mit save
.
Da wir beim Laden noch keine Instanz unseres Models haben sind die find
Methoden static
und liefern eine Instanz von sich selbst.
Die save
Methode wird so implementiert, dass sie selbstständig erkennt, ob das Objekt neu angelegt oder aktualisiert wurde – zumeist anhand eines eindeutigen Indexes.
Zusammengefasst heißt das, das Model enthält selbst (durch Vererbung) die Logiken zur Verwaltung.
Anwendungsbeispiel: Erstellen eines neuen Datensatzes
1 2 3 4 5 6 7 8 9 10 |
// Model erstellen $user = new \Model\User(); // Daten setzen $user->username = 'pö'; $user->setPassword('spaghetti-code'); // Abspeichern // Hier wird ein INSERT ausgeführt $user->save(); |
Anwendungsbeispiel: Aktualisieren eines Datensatzes
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// Datensatz suchen $user = \Model\User::findFirst(['condition' => 'id = :id', 'bind' => ['id' => 15]]); if (!$user instanceof \Model\User) { throw new \InvalidArgumentException(); } // Daten verändern $user->username = 'pö'; $user->setPassword('spaghetti-code'); // Abspeichern // Hier wird ein UPDATE ausgeführt $user->save(); |
Das Data Mapper Pattern
Bei einem Data Mapper handelt sich um einen separaten Service, der unsere Models verwaltet und CRUD-Operationen zur Verfügung stellt.
Ein Interface für einen Data Mapper könnte so aussehen:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
interface DataMapperInterface { /** * Laden mehrerer Datensätze. * * @param string $model Klasse des Models. * @param mixed $parameters Suchkriterien. * * @return object[] */ public function find($model, $parameters = null); /** * Laden eines Datensatzes. * * @param string $model Klasse des Models. * @param mixed $parameters Suchkriterien. * * @return object */ public function findFirst($model, $parameters = null); /** * Speichern eines Datensatzes. */ public function persist(); /** * Löschen eines Datensatzes. */ public function remove(); /** * Ausführen der Datenbankoperationen. */ public function flush(); } |
Da es sich um einen Service (also eigenständige Klasse) handelt, gibt es hier keine statischen Methoden. Allerdings muss dem Data Mapper beim Laden von Datensätzen mitgeteilt werden, um welche Objekte es sich handelt. Die find
Methoden liefern dann ein oder mehrere Instanzen der entsprechenden Modelklasse zurück.
Datensätze anlegen und aktualisieren läuft über die Methode persist
. Löschen über remove
.
Für gewöhnlich werden Speichervorgänge beim Data Mapper nicht sofort ausgeführt, sondern nur gesammelt und erst beim Aufruf von flush
in einer Datenbankoperation abgeschickt.
Anwendungsbeispiel: Erstellen eines neuen Datensatzes
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// Model erstellen $user = new \Model\User(); // Daten setzen $user->username = 'pö'; $user->setPassword('spaghetti-code'); // Manager Service holen $mm = $this->serviceContainer->get('models-manager'); // Datensatz zum Speichern vormerken $mm->persist($user); // Datenbankoperationen ausführen // Hier wird ein INSERT ausgeführt $mm->flush(); |
Anwendungsbeispiel: Aktualisieren eines Datensatzes
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// Manager Service holen $mm = $this->serviceContainer->get('models-manager'); // Datensatz suchen $user = $mm->findFirst(\Model\User::class, ['condition' => 'id = :id', 'bind' => ['id' => 15]]); if (!$user instanceof \Model\User) { throw new \InvalidArgumentException(); } // Daten verändern $user->username = 'pö'; $user->setPassword('spaghetti-code'); // Datensatz für Aktualisierung vormerken $mm->persist($user); // Datenbankoperationen ausführen // Hier wird ein UPDATE ausgeführt $mm->flush(); |
Die Unterschiede von Active Record und Data Mapper
Unterschiedliche Komplexität
Die Vorgehensweise beim Data Mapper erfordert einen zusätzlichen Service, der die Komplexität der Anwendung etwas erhöht. Jede Komponente, die irgendetwas mit Daten macht (also fast alle) benötigt Zugang zu diesem Service. Active Record ist hier einfacher in der Handhabung.
Flexibles Speichermedium
Nicht jede Anwendung bezieht ihre Daten ausschließlich aus einer Datenbank. Hin und wieder spielen auch Dateien (.ini, .yml, .xml, …) eine Rolle. Hier hilft das Prinzip des Data Mapper, da dieser bestimmt wie ein Model konkret abgespeichert wird. Innerhalb der Models gibt es keinen Unterschied.
Anders bei Active Record: Hier muss sich das Model entscheiden, von welcher Basisklasse es erbt. Damit ist dann auch die Wahl des Speichermediums (für immer) entschieden. Mehr Flexibilität bietet hier also Data Mapper.
Abhängigkeiten innerhalb der Daten
In vielen Anwendungen wird es Abhängigkeiten innerhalb der Datenstrukturen (und damit Models) geben. Diese sog. Geschäftslogik muss vom Programm eingehalten werden. Bei Active Record ist für gewöhnlich der „Nutzer“ – also zum Beispiel ein Controller – selbst verantwortlich. Hier können sich leicht Fehler einschleichen, insbesondere, wenn an verschiedenen Stellen mit den gleichen Daten gearbeitet werden muss (z. B. Frontend und Admin).
Der Model-Manager des Data Mapper übernimmt dieses Aufgabe. In der Konfiguration der Models werden die Abhängigkeiten festgelegt und bei jeder Operation vom Model-Manager automatisch eingehalten.
Transaktionen
Sollen mehrere Datenbankaktionen atomar (also alle oder keine) erfolgen, sind Transaktionen notwendig. Wie schon bei der Geschäftslogik muss sich mit Active Record selbst darum gekümmert werden.
Wie wir oben gesehen haben, werden hingegen beim Data Mapper von Haus aus mit persist
alle Operationen gesammelt und als eine Einheit mit flush
abgesetzt.
Was ist denn jetzt besser?
Wie oft in der Programmierung kann auch hier die Antwort nur lauten: „Kommt darauf an.“
Besteht die Anwendung größtenteils aus CRUD-Operationen einzelner Datenobjekte, wird Active Record die bessere Wahl sein.
Gibt es hingegen gewachsene und komplexe Geschäftslogiken, wird ein Data Mapper die Prozesse vereinfachen.