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.

Ist OpenID tot?

Durch meine Mitarbeit an der node-openid Bibliothek und der Gestaltung meiner neuen Webseite bin ich mal wieder intensiv in den Kontakt mit OpenID gekommen.

Nun bin ich über einen sehr guten Artikel gestolpert der die Frage stellt, ob OpenID nun tot ist. Ganz provokant wird OpenID als der erfolgreichste Fehlschlag des Webs bezeichnet.

Auch aus meiner Sicht ist sicherlich etwas falsch gelaufen. Die auch im Artikel angesprochene Asymmetrie ist überdeutlich: fast jeder Internetnutzer hat wahrscheinlich ohne es selbst zu wissen schon mehrere OpenIDs, dennoch gibt es extrem wenige Webseiten bei denen man sich mittels OpenID anmelden kann. Nur die wenigsten setzen ganz auf OpenID, so wie ich.

Der Grund warum ich es einsetze ist einfach: Ich habe weder Lust noch Fachwissen um hundertprozentige Sicherheit zu gewährleisten. Die billigsten Webseiten speichern einfach die Passwörter im Klartext in der Datenbank, andere benutzen dafür Hashwerte die je nach benutzter Hashfunktion und Salt auch relativ unsicher sind. Ein offizielles SSL Zertifikat kann sich keine Privatseite leisten, über ein selbst signiertes wie ich es benutze schreit der Browser standardmäßig. Dadurch ist das Passwort meist unverschlüsselt im Netzwerk unterwegs. Für die Persistenz kann man auch noch mehr oder weniger unsichere Cookies verwenden.

Alles in allem: Echte Sicherheit zu gewährleisten ist schwer und mit OpenID kann ich all dies an den OpenID Provider delegieren. Das Benutzerpasswort kriege ich nie mit, das einzige was meinen Benutzer identifiziert ist seine OpenID, und die ist sowieso öffentlich. Diese wird auch für Persistenz benutzt und im Cookie gespeichert. Auch hier kann das Cookie gern über XSS oder sonstige Methoden gestohlen werden, es sind ja keine sensitiven Informationen darin enthalten.


Zurück zum Thema: Es gibt zu wenige Seiten bei denen man sich mittels OpenID anmelden kann. Zudem ist das Protokoll auch übermäßig komplex wie ich bei meiner Arbeit an der Bibliothek feststellen musste.

Der Artikel geht aber noch auf ein anderes Problem mit OpenID ein: Der Standard ist für die Authentifizierung gedacht, das heißt sicherzustellen, das dem Benutzer die eingegebene OpenID (z.b. swatinem.de) wirklich gehört.

Über Erweiterungen wie Simple Registration (Sreg) oder Attribute Exchange (AX) kann die Webseite weitere Daten wie beispielsweise E-Mail Adresse und Name anfordern. Diese Erweiterungen sind optional und es ist nicht sichergestellt, das der OpenID Provider diese Informationen auch liefert, also muss man so oder so eine Registrierungsmaske bereitstellen.

Nebenbei Erwähnt: Sreg definierte nur wenige Attribute und wurde daher durch AX ersetzt. AX stattdessen setzt auf absolut flexible an RDF angelehnte URIs als Attribut Schlüssel und somit nicht einheitlich, zu unübersichtlich und zu kompliziert.


Ich als technisch versierter Nutzer kann einerseits nicht nachvollziehen warum so viele Webseiten unnötig viele Nutzerdaten haben wollen, andererseits will ich absolute Kontrolle darüber haben welche Webseite welche Daten von mir hat. OpenID bietet mir genau das.

Als Alternative ist die Anmeldung über Facebook sehr beliebt, nicht allerdings bei mir. Möglicherweise ist das zugrundeliegende OAuth ein einfacheres und besser Protokoll als OpenID, allerdings nerven mich zwei Sachen: erstens das die Webseite von Facebook anscheinend beliebig viele Daten erhalten und ausnutzen kann. Andererseits, weshalb ich die Anmeldung über Facebook nicht implementieren werde: Für die Anmeldung über Facebook ist es erforderlich das die Webseite bei Facebook eingetragen ist, und dazu benötigt man ein Facebook Konto. Dieser Netzwerkeffekt und Anmeldezwang ist es was ich an Facebook am meisten verabscheue und gegen was ich auch weiterhin ankämpfen werde.

Ich will einfach ein dezentrales System wie E-Mail, Jabber, OpenID und möglicherweise Diaspora bei dem jeder miteinander agieren kann egal welchen Dienstanbieter er benutzt. Was wäre bitte E-Mail wenn ich mit meiner Googlemail Adresse nur an Googlemail senden könnte und nicht an GMX oder Hotmail? Dieser Zwang zu einem Dienstanbieter und dadurch dessen Monopolisierung ist meiner Meinung nach eine der größten Gefahren für die Freiheit im Web und muss daher dringend entgegengewirkt werden.

OpenID und node.js die zweite

Nachdem ich meine eigenen Experimente eine OpenID Bibliothek für node.js zu schreiben aufgegeben habe, bin ich vor einigen Tagen auf eine neue Bibliothek gestolpert. Ausprobiert und gleich einen Fehler beseitigt, wegen dem meine delegierte Adresse nicht funktioniert hat. Ich habe dann auch schnell angefangen weitere Verbesserungen zu machen, ich habe eine Simple Registration und eine Attribute Exchange Erweiterung geschrieben und mich für den gegebenen Anlass auch bei github angemeldet um meinen Fork zu veröffentlichen.

Bei der ganzen Arbeit bin ich über einen Fehler in der querystring Implementierung der node Standardbibliothek gestolpert. Die Verwendung von node 0.3 oder eines nicht-standard Moduls beseitigt das Problem dabei.

Ich habe nun heute versucht die inzwischen zufriedenstellend funktionsreiche OpenID Bibliothek in eine auf express.js basierende Seite einzubauen, als Anfang von einer Benutzerverwaltung. Leider ohne großen Erfolg. Da express selbst auch das querystring der Standardbibliothek verwendet bricht es bei einer OpenID Antwort ebenfalls zusammen. Ich hatte dann nochmal versucht node 0.3 anstatt 0.2 zu probieren, dies schlug aber auch fehl. Die Socketverbindung bricht aus unbegreiflichen Gründen ab, ähnlich diesem Fehlerbericht.

Alles in allem bin ich aber dennoch mit dem Fortschritt zufrieden. Unter bestimmten Voraussetzungen funktioniert OpenID einwandfrei, diese Voraussetzungen müssen jetzt nur noch im Zusammenspiel mit express erfüllt werden. Danach kann dann fleißig die tatsächliche Benutzerverwaltung mit automatischer Benutzerkontoerstellung und vielem mehr implementiert werden.

Eigentlich hatte ich auch noch vor etwas den PHP+MySQL mit dem node.js+MongoDB Stack zu vergleichen bei bestimmten Arten von Queries.


older posts