dWing — die Welt ist nicht gerecht

sign in

Web-Frameworks: ActiveRecord, Lazy-Loading

Ein ActiveRecord, oder besser gesagt Object Relation Mapper (ORM) bildet das M aus MVC. Also der Punkt bei dem es um die Speicherung der Daten geht.

Das ActiveRecord Entwurfsmuster gibt dabei einige Basisoperationen vor. Meist einen Konstruktor mit dem ein neues Objekt erstellt wird, eine save Methode um Änderungen in die Datenbank zu speichern, bei einem noch nicht in der Datenbank vorhandenem Objekt wird dies hineingeschrieben. Eine remove Methode löscht das Objekt. Statische Methoden dienen dazu Objekte aus der Datenbank zu lesen.


Ein ActiveRecord selbst zu implementieren ist eigentlich einfacher als man denkt. Zumindest ein einfaches ActiveRecord was keine hasOne, hasAndBelongsToMany Beziehungen hat. Mein dWing hat ein ganz einfaches ActiveRecord. Benutzten kann man es wie folgt:

class Comment extends ActiveItem
{
  protected $tableName = 'comments';
  protected $definition = array('text' => 'html', 'user_id' => 'user',
    'time' => 'time', 'content_id' => 'required', 'content_type' => 'required');
}

Mein allgemeiner __get()ter sorgt dabei für dynamische hasOne und hasMany Beziehungen, ganz ohne das man diese deklarieren muss.

Dabei wird alles über Lazy-Loading realisiert. Es werden die Objekte nur dann erzeugt und aus der Datenbank geholt wenn diese benötigt werden.

Greife ich z.b. auf $news->comments zu, wird zuerst ein CommentIterator erzeugt. Dieser implementiert auch das Countable Interface. Das bedeutet, das wenn ich ein count($news->comments) mache wird lediglich ein SQL COUNT() ausgeführt. Erst wenn ich den Iterator anfange zu iterieren wird das vollständige SELECT gemacht. Dabei liebe ich auch PHPs PDO.

$statement->setFetchMode(PDO::FETCH_CLASS, 'Comment');
$this->elements = $statement->fetchAll(); 

Hier erzeugt mir PDO direkt meine Klassen ohne das ich dies selbst tun müsste.


Nun zu CakePHP. Was soll ich sagen? Obwohl die Modelle eine save() Methode haben und die besagten hasOne usw. Beziehungen würde ich das Cake ORM keinesfalls als ActiveRecord bezeichnen.

Einerseits sind die Models anscheinend nicht als Globale Klasse verfügbar. Im Controller definiert man über eine $uses Variable welche Modelle man benutzen will. Dann sind diese als $this->Model verfügbar. Und wenn man über eine der find() Methoden Daten haben will werden die nicht als Objekt oder Iterator über Objekte zurückgegeben sondern als ein wild gemischtes Array. Die Cake Hilfe gibt ein Beispiel darüber. Ich muss ehrlich sagen, etwas unintuitiveres habe ich noch nicht gesehen.

Auf der Unteren Ebene kriege ich ein Array, das ist auch das einzig Intuitive. Darunter ein Assoziatives Array, auch bekannt als Hashmap, Dictionary oder einfach Objekt. Dieses hat als Indizes die Modellnamen. Will ich eine News haben ist diese über $news['News'] verfügbar. Redundanz ist ja was schönes oder nicht? Und die hasOne, hasMany usw Beziehungen sind ebenfalls in der Ebene.

Die Kommentare sind also in $news['Comment'][0] usw. An sich ist kein großer Unterschied zwischen $news->comments und $news['Comment']. Aber das die News an sich unter $news['News'] zu finden ist verstehe ich nicht. Selbst wenn man diese Dinge als eine Hashmap ausgibt kann man diese besser gestalten.

Und das was die find Methode liefert keine Objekte sind haben diese keine eigenen save() und remove() Methoden. Diese sind über das Modell erreichbar, was diese Operation immer auf die zuletzt zugewiesene Id anwendet. Absolutes Gegenteil von intuitiv.

Ein weiterer Nachteil ist, das es kein Lazy-Loading gibt. Alle dazugehörigen Beziehungen werden aus der Datenbank ausgelesen, auch wenn man diese gar nicht braucht.

Cake stammt aus der PHP4 Zeit, obwohl es ein Jahr nach dem Erscheinen von PHP5 entwickelt wurde. Leider scheint mir dies der Grund zu sein für dieses schlechte ORM. Ich habe zumindest keinerlei Verständnis dafür.


Für node.js benutze ich Mongoose als ActiveRecord. Ein Schema zu definieren ist darin auch sehr einfach:

var Project = new Schema({
	  title: {type: String, required: true}
	, url: {type: String}
	, description: {type: String, required: true}
	, image: {type: String}
	, _user: {type: ObjectId, required: true}
});

Da MongoDB eine JSON basierte Datenbank ist, kann man direkt in den Objekten Unterobjekte speichern oder Arrays von Objekten. Dennoch ist es in einigen Situationen sinnvoll hasOne Beziehungen zu modellieren. Mongoose hat leider keine eingebaute Fähigkeit dazu. Es existieren einige Projekte dafür aber bisher hat mich keines überzeugt. Ich habe lieber selbst eine einfache hasOne Beziehung modelliert:

var users = [];
var anonymous = new User();

Project.virtual("user").get(function () {
	return this.__user;
}).set(function (user) {
	this.__user = user;
	this._user = user;
});

Project.pre("init", function(next, doc) {
	if (!doc._user)
		return next();
	//if (doc._user && users[doc._user])
	//	return next();
	User.findById(doc._user, function(err, user) {
		if (err || !user)
			return next(err);
		users[user.id] = user;
		return next();
	});
}).post("init", function() {
	this.__user = users[this._user] || anonymous;
	delete users[this._user];
});

Leider war dies um einiges schwieriger als ich gedacht habe. Ich definiere mir also zwei Funktionen die vor und nach dem erstellen des Objektes ausgeführt werden. Dabei ist die erstere Asynchron, also wird diese durch einen Callback beendet. In dieser hole ich mir mein verwandtes User Objekt aus der Datenbank und speichere dies in einem globalen Objekt zwischen, da diese neu hinzugefügten Objekte leider nicht übernommen werden wenn das tatsächliche Objekt erzeugt wird. Dies füge ich im zweiten Schritt an und lösche die globale Kopie, das diese nicht für die Laufzeit des Programms den Speicher zumüllt. Dann habe ich noch an meinem Modell eine Virtuelle Eigenschaft mit der ich das zuweisen eines neuen Users richtig behandeln kann.

Ich habe zwar lange für diese Lösung gebraucht, elegant ist sie nicht aber ich kann sie einfach verallgemeinern und auslagern. Zu benutzen ist es auch einfach.

Ein Problem gibt es allerdings: Da ich mit Callbacks arbeiten muss fällt Lazy-Loading leider weg. Eine getUser(callback) Methode zu definieren ist dabei keine Lösung. Die Views in node/Express sind synchron, können also keine asynchronen Methoden benutzen die auf Callbacks basieren. Damit muss ich nicht nur auf das bereits erklärte Pull-Modell verzichten bei dem sich die View selbst die zu darstellenden Daten holt, sondern auch auf das dynamische Laden verwandter Daten bei deren Zugriff.

Ich will die Kopplung von Controller und View niedrig halten. Mein Controller soll nicht wissen ob die View nun auf project.user zugreift oder nicht. Also muss ich zwangsläufig project.user vorher laden, denn die View kann dies nicht nachträglich machen.


Insgesamt hat node.js mit Express einige Dinge die mir aus meiner Erfahrung mit dWing fehlen und auch performancekritisch sein können. Pull-Views sind nicht möglich, ebenso wenig das dynamische laden von hasOne, hasMany und ähnlichen Beziehungen. Wobei es dank MongoDB auch viel weniger von diesen gibt. Diese werden nicht wie bei SQL durch die Normalisierung erzwungen sondern nur wenn sie sinnvoll sind. Daher gibt es auch viel weniger.

Die wirklich exzessive Verwendung von Callbacks in node ist sehr gewöhnungsbedürftig, aber ich habe mich schon daran gewöhnt. Mich nervt nur das was ich bereits angesprochen habe: Das ich im Controller wirklich alles laden muss was die View braucht. Und das ich dies tatsächlich im Vorfeld machen muss, auch wenn ich es in der View möglicherweise nicht brauche. Dies kann mir unter Umständen die Performance zusammenhauen.

Dennoch bin ich voll auf den Node Zug aufgesprungen. JavaScript ist einfach eine viel bequemere Sprache. So langsam kotzt mich PHP echt an mit den $dollar Variablen, den $array['Klammern'] anstatt der viel einfacheren Punkt.Notation wie in JS. Auch das komplizierte ausschreiben von array('eigenschaft' => 'wert') ist viel nerviger als einfach {eigenschaft: 'wert'}. Aber jetzt wo ich wieder mein dWing angesehen habe finde ich die sehr ausgereiften Lazy-Loading Methoden von PHP sinnvoll, und das es tatsächlich Klassen mit Interfaces hat.

Web-Frameworks: Push/Pull

Im letzten Eintrag habe ich schon erwähnt das die Templates in dWing mehr Fähigkeiten haben. Ein Weg MVC Anwendungen zu machen, und wie es CakePHP tut und express gezwungenermaßen auch ist, dass der Controller bzw. die Ressource die darzustellenden Daten an die View übergibt. Das nennt man das Push-Modell.

Persönlich halte ich dadurch die Trennung von Controller und View einerseits zu stark, denn die View weiß nichts über den Controller, andererseits zu stark, denn der Controller muss alles über die View wissen um die darzustellenden Daten liefern zu können.

Was ist wenn auf jeder Seite, also anders gesagt bei jedem Controller oder jeder Ressource etwas dynamisches in der View dargestellt werden soll. Ich kann entweder in jedem Controller diese Sachen der View übergeben was zu sehr viel redundantem Code führt. Oder aber ich übergebe dies an einer zentralen Stelle. In Cake im AppController, oder in Express in einem Helper.

Dies ist aber auch nicht optimal: Sollte die View die Daten doch nicht benötigen ist es unnützer Overhead der das ganze verlangsamt.

bei dWing gehe ich einen gemischten Weg. Die Ressource kann einerseits die Objekte an die View übergeben, pushen. Oder aber die View holt sich selbst was sie braucht, pull. Somit kann ich die Views ändern ohne den Rest der Anwendung anzugreifen. Es besteht also eine geringere Kopplung und Abhängigkeit der View von dem Controller. Außerdem halte ich meine Controller sauber, und schlussendlich bleibt das ganze auch performant.


Dies kann man leider leider in Express mit node.js nicht machen. Mehr dazu im Teil über Lazy-Loading und Async Callbacks.

Web Frameworks: Model, View und Controller

In letzter Zeit beschäftige ich mich wieder mehr mit Web Frameworks, da ich auf der Arbeit mit CakePHP arbeiten muss und nebenbei node.js und Express lerne. Und natürlich existiert noch mein dWing, das im Prinzip meine Meinung zum Thema Web Frameworks in Code gießt.

Ich habe mir vorgenommen in einigen Beiträgen zu den Themen Object-Relation-Mapping (ORM) bzw ActiveRecord, Push/Pull-Modell, Lazy-Loading und Asnyc Callbacks zu sprechen.


Heute geht es allerdings um Model-View-Controller. Dieses Entwurfsmuster ist dazu gedacht die Speicherung der Daten, die Logik und die Darstellung zu trennen. Persönlich finde ich die Logikschicht absolut überbewertet. Ich finde in modernen Anwendungen sollte es sich mehr um die Grundobjekte drehen und was ich mit diesen machen kann. Und auch dort ist weniger mehr. Denn eigentlich muss ich nur vier Operationen machen können. Diese auslesen (read, GET), erstellen (create, POST), bearbeiten (update, PUT) und löschen (delete, DELETE). In Klammern habe ich jeweils die Begriffe aus CRUD und den HTTP Methoden angegeben.

Im Grunde genommen geht es nicht darum was ich mache, sondern mit welchem Objekt ich dies tue. Bewerte ich einen Eintrag auf dWing dann gibt es keinerlei bewerte Operation auf meinen Einträgen. Stattdessen erstelle ich eine Bewertung zu diesem Eintrag. Somit habe ein weiteres Objekt, auf das ich eine der vier Grundoperationen (erstellen) anwende. So einfach ist dies.

Mein dWing ist genau nach diesem Muster aufgebaut. Meine Ressourcen implementieren einfach das RESTful Interface und alles andere geschieht automatisch. In Express gibt es das express-resource Modul, welches auch automatisch die Resource laden kann. Sehr fein :-) Bei CakePHP sehe ich keine solche Ressourcenorientierung. Dort werden die URLs nach dem /controller/methode Muster interpretiert. Andere Routen können definiert werden aber ich halte dies für recht unbequem.

Statische Seiten werden dort auch über einen Controller geleitet. Bei dWing geschieht dies automatisch im normalen Routing. Kein großer Unterschied. Bei Express musste ich mir selbst auch einen Router basteln der auf ein statisches Template umleitet.

Die Template Systeme von Cake und Express folgen dem Layout Prinzip. Also ist ein Layout Template definiert, dem z.b. als body variable der Inhalt übergeben wird. In dWing inkludiert jedes Template Kopf und Fuß einzeln. Etwas umständlicher, allerdings wird es so möglich das ich den Inhalt des Titel Elementes in meinem statischen Template definiere bevor ich den Kopf einbinde. In Cake und Express ist es nicht möglich solche Variablen vom Template zum Layout zu übergeben. Dafür muss ich mir noch etwas überlegen.

Bei dWing haben die Templates allgemein mehr Fähigkeiten, mehr dazu im Teil Push/Pull und ActiveRecord.

Tagesrückblick 2010-08-11

Ich habe gestern den ganzen Tag mit node.js und MongoDB herum gespielt. Leider gibt es keine Standardbibliothek für MongoDB unter node. Es gibt einige Projekte, die leider sehr unterschiedliche APIs haben, manche basieren nur auf Node, für andere muss man selbst ein C++ Modul übersetzen.

Das aktivste Projekt scheint node-mongodb-native zu sein, auf das man zusätzlich Mongoose aufsetzen kann, was einerseits die asynchrone auf Callbacks basierte Natur der API versteckt und außerdem eine super Schnittstelle zur Modellierung bietet.

Das große Problem dabei ist, dass die Bibliothek über einen Socket mit MongoDB kommuniziert und dabei BSON (Binäres JSON) benutzt wird. Dieses muss leider im Treiber geparst werden, was sehr langsam ist. Die Leistung bricht sauber ein und der node Prozess ist völlig ausgelastet, der Mongo Server stattdessen wird absolut nicht ausgelastet.

Zum Glück arbeitet der Entwickler des Treibers daran das BSON parsing in nativen Code auszulagern. Dann muss ich zwar trotzdem mich mit dem Übersetzen des Codes herumschlagen aber ich hoffe das es damit dann schnell gehen wird.


Auch an einer anderen Stelle ist mir das schlechte Packetmanagement aufgefallen. Es gibt die Möglichkeit die Ausgabe mit gzip zu komprimieren. Dazu kann das compress Modul verwendet werden. Naja, nun gibt es aber mehrere compress Module die den selben Namen tragen aber eine völlig unterschiedliche API zur Verfügung stellen. Sehr verwirrend. Aktuell wird ein Fallback verwendet der in einem Kind Prozess das gzip Programm ausführt. Funktioniert, ist aber sicherlich weniger performant als ein direkt auf die gzip Bibliothek zugreifendes Modul. Aber dann gibt es wieder das Problem dass man den Code in C selbst übersetzen muss.


Alles in allem scheint mir node.js bisher eine Art Spielwiese zu sein die noch stabilisiert werden muss, vor allem benötigt es eine offizielle Modulbörse wo der Umgang mit Modulen in C und die große Duplizierung die es aktuell gibt gelöst werden muss. Im Prinzip ist node bisher eh noch keine stabile (Version 1.0) Software, also kann man diese Probleme noch verkraften. Für Datenbankschnittstellen fehlt es auch an offiziell abgesegneten Modulen. Möglicherweise könnte man die Schuld aber den MongoDB Entwicklern in die Schuhe schieben, die eine Offizielle Schnittstelle für CommonJS pflegen sollen. Ich frage mich aber wie man implementierungsübergreifende native Module anbieten will. Schließlich gibt es mehrere Implementierungen für CommonJS, nodejs ist nur eine davon. Es gibt also noch viele Probleme zu lösen in der CommonJS Welt.

Namen ändern

Nun habe ich endlich eine lange überfällige Funktion in dWing eingebaut. Es ist nun möglich seinen Namen zu ändern. Vor allem für die ganzen Google und Yahoo Nutzer ist dies sehr gut, denn diese Dienste unterstützen nicht den Austausch von Nutzerdaten wie Name bei der Anmeldung.

Diesbezüglich habe ich auch probiert von der veralteten OpenID Simple Registration Erweiterung auf Attribute Exchange umzusteigen, nur um festzustellen, das keiner der Provider, nicht einmal MyOpenID diese Erweiterung unterstützen. Oder aber ich habe etwas falsch gemacht, kann natürlich auch sein.

Für OpenID habe ich auch ein sehr interessantes Widget gefunden, das es sicherlich wert ist ausprobiert zu werden.


Als ich also gestern mal wieder an dWing gearbeitet habe ist mir aufgefallen wie unbequem es doch ist damit zu arbeiten, und das es vielleicht viel besser wäre wieder einmal von null anzufangen. Möglicherweise sogar mit einem ganz anderen Technologiestapel als den altbekannten PHP und MySQL.

Ich habe also intensiv in die Richtung serverseitiges Javascript und NoSQL Datenbanken recherchiert. Node.JS und ExpressJS scheinen sehr interessante Lösungen zu sein. Zusammen mit einer Template Sprache wie Embedded JS, die ich in einer abgespeckten Version schon ein paar mal eingesetzt habe bietet dies sicher eine gute Kombination. Mit serverseitigen JS und Templates die am Server und am Client gleich sind kann man sicherlich schneller zu einem konsistenteren Ergebnis kommen.

Node basiert auf der V8 Engine aus Chrome, ich weiß also nicht genau wie viele der erweiterten Funktionen von Spidermonkey wie beispielsweise Array Comprehension oder Generators es unterstützt. Als Mozilla Entwickler würde es mich auch sehr interessieren, wie sich Jägermonkey in so einem Einsatzgebiet schlagen würde. Bisher ist allerdings ExpressJS sehr schnell. Mit keepalive Anfragen ergibt es über 5000 Anfragen die Sekunde für ein Hallo Welt Skript. PHP kommt auf vielleicht 2000. Hinter einem Lighttpd Proxy ohne keepalive kommt es immerhin noch auf etwas über 2000, also in etwa PHP Niveau. Ich denke aber, dass sich mit der Performance mehr ausgeht, denn die JS Engines haben bessere JIT Compiler und deren Performance wird auch ständig weiter verfeinert.

Wenn man eine andere Syntax mag kann man auch CoffeeScript benutzen. Dies ist eine etwas einfachere Syntax, die zu JS compiliert wird und ebenfalls Funktionen wie Array Comprehension unterstützt.

Als Datenbanklösung könnte sich MongoDB anbieten. Eine Dokumenten- und JSON-basierte Datenbanklösung wäre sicherlich sehr interessant auszuprobieren. Allerdings schweben mir einige Dinge vor von denen ich keine Ahnung habe wie diese damit zu lösen wären.

Für die Arbeit mit CSS finde ich Sass sehr interessant, macht die Arbeit in manchen Situationen sicherlich einfacher. Haml ist auch eine interessante Möglichkeit für Templates, allerdings gefällt mir die Syntax nicht ganz so sehr, und meine sehr alten Erfahrungen mit Smarty haben bewiesen, das es doch sehr viel sinnvoller ist eine echte Programmiersprache in den Templates zu haben.


Alles in allem sehr interessante Projekte die ich gerne mal ausprobieren würde, möglicherweise zuerst in TinderboxPushlog bevor ich mich daran wage aus meinem PHP dWing ein JS dWing zu machen.