May 5, 2008

Das "Geheimnis" von deklarativen Assoziationsdefinitionen

Da ich nunmehr in zwei verschiedenen Sprachen mich mit dem Thema der deklarativen Art der Definition von Assoziationen zwischen Klassen beschäftige, schien es mir sinnvoll, darüber an dieser Stelle mal etwas zu schreiben.

Erstmal: was meine ich mit diesem toll klingenden Konzept überhaupt? Ich meine damit das, was jeder Rails-Mensch kennt:

class Foo < ActiveRecord::Base
  has_many :bars
  belongs_to :baz
end

oder, mit dem in meinem letzten Post beschriebenen JS-Framewörkchen:

var Foo = ActiveResource.inherit({
  has_many: {
    bars: {}
  }
  has_one: {
    baz: {}
  }
});

Was man erwartet, ist dass ein Aufruf von foo.bars() die Liste aller zu foo gehörenden Bar-Instanzen liefert. Das zu bewerkstelligen ist in jeder Scriptsprache, die so etwas wie eine eval()-Methode oder gar ein ausgefeiltes Mataprogrammier-Konzept wie Ruby kennt, an sich relativ einfach. Das Rezept (am Beispiel von has_many):

  • man definiere eine Methode, welche die find-Methode der assoziierten Klasse mit einem Kontext definierenden Argument wie "für foo_id=self.id" auf. Ich schreibe das bewusst so allgemein, weil die konkrete Implementierung stark von der zu Grunde liegenden Persistenz abhängt -- es könnte (im Falle von ActiveResource) die Generierung einer URL mitsamt eines HTTP-Requests, oder (im Falle von ActiveRecord) die Generierung einer SQL-Query oder einer Suche-in-Textdatei-Methode oder oder... sein.
  • man definiere eine Klassenmethode namens has_many, die als Argument mindestens den Namen der Assoziation entgegennimmt (und daraus ggf. den Klassennamen der assoziierten Klasse herleitet)
  • man generiere eine Methode mit dem Namen des an has_many übergebenen Arguments (z.B. foos), die die erstgenannte Methode kapselt. In Javascript kann man z.B. mit sowas wie eval('Foo.prototype.' + association_name + '=function(){....}') arbeiten. In Ruby würde man die Methode eher gleich mittels define_method dynamisch generieren (define_method association_name do ... end).
  • man freue sich, dass bei einem Aufruf von foo.bars() nun das gleiche zurückgegeben wird, wie es (im Falle von RoR) bei Bar.find(:all, :conditions => "foo_id=#{foo.id}") der Fall wäre.

Aaaaber: das kann doch noch nicht der ganze Zauber gewesen sein? Und in der Tat, das ist erst ein kleiner, naiver Anfang. Wer Rails genauer kennt, weiß dass auch Konstrukte wie foo.bars.find_by_name_and_trallalla() ohne weiteres möglich sind. Dabei ist diese find_by...-Methode nicht etwa eine Methode der zurückgegebenen Collection (ich nenne das mal so, weil es je nach Sprache ein Array, ein Hash oder sonst eine iterierbare Objektsammlung sein kann), sondern wenn dann eine Klassenmethode von (in diesem Falle) Bar. In A Nutshell: Was auch immer beim Aufruf von foo.bars() zurückgegeben wird, es implementiert sowohl die Schnittstelle einer Collection (Array,...) als auch der Klasse, zu der die erwarteten Objektinstanzen gehören. (Genau genommen stimmt das nicht 100%ig, aber an dieser Stelle reicht diese Annahme).

Der geneigte Leser möge sich einstweilen den Rails-Code zu Gemüte führen und dabei feststellen, dass foo.bars() ein "Association Proxy"-Objekt zurückliefert. Dieses Objekt beinhaltet sämtliche Informationen darüber, wie die beiden Klassen zueinander in Beziehung stehen, wie die Instanzen der assoziierten Klasse geholt werden können und stellt Methoden bereit, um auf diese wie in einem Array zuzugreifen. Etwas genauer gesprochen erstellt die has_many-Methode ein Objekt, in dem die Informationen zu der Assoziation zwischen den Klassen gehalten wird und beim Aufruf der entsprechenden Objektmethode (foo.bars()) wird ein Objekt mit Informationen zu den Instanzen der referenzierten Klasse erstellt und zurückgegeben.

Mit dieser Information im Hinterkopf und etwas Geschicklichkeit ist es relativ einfach, miteinander in Beziehung stehende Resourcen jeder Art umzusetzen. Die Details wie man innerhalb des einen Objektes an das jeweils andere Objekt heran kommt, können hinter einer einheitlichen Schnittstelle (die des "Association Proxies") gekapselt werden. Somit ist es dann auch wenig umständlich, z.B. in RoR ActiveRecord- und ActiveResource-Objekte miteinander zu verknüpfen, über die gleiche deklarative Syntax wie man ActiveRecord-Objekte untereinander verknüpft. Ich arbeite gerade noch an einer halbwegs vollständigen, jedenfalls aber brauchbaren Implementierung dafür, denn ich sehe nun schon bald eine Handvoll Projekten, wo das zumindest Hilfreich wäre (Stichwort: Anwendung mit Benutzerverwaltung in einer anderen Anwendung, Kommunikation über HTTP/XML). Sobald das veröffentlichungswürdig ist, wird es selbstverständlich hier erscheinen.

April 14, 2008

Javascript + ActiveRecord + ActiveResource

Dieses Wochenende stand wieder einmal ganz im Zeichen des freien Programmierens und ich muss sagen, dass ich mit den Ergebnissen doch ganz zufrieden bin.

Der Hintergrund: Ich hatte schon länger ein angefangenes Projekt mit unter anderem einem vollständig in JavaScript implementierten Verfügbarkeitsplaner für vermietete Gegenstände (siehe Bild).

snap17.png

Die Implementierung dieses Planers umfasste rund 800 Zeilen Code, im wesentlichen war das eine gigantische Planer-Klasse und zwei kleinere, jeweils für die Repräsentation eines vermieteten Gegenstandes sowie dessen (Nicht-)Verfügbarkeitszeiträumen. Das alles war nicht sonderlich aufgeräumt und insbesondere was die Verwendung von Event-Handlern betraf alles andere als einfach erweiterbar. Deswegen sollte das neu geschrieben werden.

Die Vorarbeit: Am Samstagmorgen hatte ich "eben schnell" das in PHP geschriebene serverseitige Backend neu in Rails implementiert und dadurch auch eine schöne REST-Schnittstelle erhalten. Anschließend ging es darum, die serverseitigen Ressourcen 1:1 auf Clientseite abzubilden. Rails bietet hervorragende Voraussetzungen dazu: im Controllercode erweitert man seine respond_to-Blöcke einfach um ein format.json { render :text => @obj.to_json }.Statement. Dann kriegt der Client beim Aufruf von beispielsweise /clients/2.json die Repräsentation eines Client"-Objektes als JSON (JavaScript Object Notation) geliefert, was sofort evaluiert werden kann und dann als Javascript-Object zur Verfügung steht. Aber: was ist mit Assoziationen? Was mit dynamischem Nachladen von Objekten? Objekt-Caching etc.?

Der Plan: Also ward die Idee geboren, eine Art Mischung aus ActiveRecord und ActiveResource in Javascript zu implementieren. das prototype.js-Framework bietet eine hervorragende Grundlage für solche Unterfangen, und ist mit seiner (nicht ganz zufälligerweise) an die RoR-Idiome angelehnte Namensgebung auch sehr angenehm zu verwenden. Das Ziel sollte nun sein, mit Hilfe von prototype.js ein kleines Framework zu schreiben, eine ähnlich minimalistische Klassendefinition erlauben würde wie ActiveRecord bzw, ActiveResource, inklusive der Generierung von Methoden zur Handhabung der Assoziationen.

Um eine lange Geschichte etwas zu kürzen, werde ich an dieser Stelle nicht konkret auf die Implementierung der ganzen Geschichte eingehen, sondern das in einem späteren Post tun. Hier erstmal ein Beispiel:

// ActiveResource.inherit ist die "magische" Methode, die jede Menge Code generiert...
var Trailer = ActiveResource.inherit({
 // ein paar Assoziationen:
  has_many: {
    'rental_periods': {},
    'occupations': {
      resource: 'occupation_dates',
    }
  },
  has_one: {
    'owner': {
      'class_name': 'TrailerOwner',
      'resource': 'people'
    }
  },
  // der Name der Ressource auf dem Server
  resource: 'trailers'
});

Folgende Methoden stehen nun zur Verfügung:

// holt sich das Trailer-Objekt (synchron) mit der ID 'id' und schreibt es nach {tr}
var tr  = Trailer.findOne(id);
// holt sich alle Trailer-Objekte (synchron) vom Server und schreibt sie nach {trs}
var trs = Trailer.findAll();
// holt Trailer {id} asynchron vom Server und übergibt das Resultat der Callback-Methode
Trailer.findOne(id, callback);
// dito für alle Trailer-Objekte
Trailer.findAlll(callback);
// liefert nach obiger Definition "/trailers"
var url = Trailer.resource_url();
// liefert nach obiger Definition "/trailers/{id}"
var url = Trailer.element_url(id);
// liefert nach obiger Definition "/trailers/{trailer_id}/rental_periods"
var url = tr.rental_periods_url();
// liefert nach obiger Definition "/trailers/{trailer_id}/rental_periods/{id}"
var url = tr.rental_period_url(id);
// liefert nach obiger Definition "/trailers/{trailer_id}/occupation_dates/{id}"
var url = tr.occupation_url(id);
...
// holt das OccupationDate-Objekt mit der ID {id}
var oc = tr.occupation(id);
// holt das Owner-Objekt
var ow = tr.owner();
...

Alle Objekte, die (synchron oder asynchron) vom Server geholt werden, werden in einem internen Cache zwischengespeichert. Momentan greift der Cache nur beim lesen von Einzelobjekten, sprich ein Aufruf von Trailer.rental_periods() würde immer einen HTTP-Request an den Server schicken. Aber daran wird noch gearbeitet :)

Auch ist das Speichern von Objekten noch nicht möglich, aber die Implementierung der Funktionalität dürfte nur unwesentlich mehr als 30 Minuten in Anspruch nehmen...

Die mittels ActiveResource.inherit() generierte "Klasse" (Javascript kennt ja keine wirklichen Klassen, sondern arbeiten Prototyp-basiert) kann ohne weiteres mit der von prototype.js bereitgestellten Methode Object.extend() um weitere Methoden erweitert werden. Auch kann die "Vererbungs"-Methode Class.create(Elternklasse, Kindsklasse) angewendet werden. (Genau genommen tut ActiveRecord.inherit() genau dies: Es erstellt eine neue Kindsklasse mit Class.create(ActiveRecord, MeineKlasse) und erweitert MeineKlasse mit den diversen assoziationsbezogenen Methoden). Es ist auch möglich, Kindsklassen von einer von ActiveResource erbenden Klasse zu erstellen und diese wiederum mit Assoziazionsdefinitionen zu versehen. Allerdings nicht mit Class.create(), sondern wiederum mit ActiveResource.inherit():

var Trailer = ActiveResource.inherit({
  resource: 'trailers'
  has_many: {...},
  has_one: {...},
});

var TruckTrailer = ActiveResource.inherit(Trailer, {
  resource: 'truck_trailers',
  has_many: {...}
});

Es ist theoretisch sogar möglich, beliebige "Klassen" mit der ActiveResource-Funktionalität "nachzurüsten":

var Person = Class.create({...});
var Person = ActiveResource.inherit(Person, {...definitionen der Ressourcen...});

Aber das ist eher eine akademische Überlegung, getestet hab ich das nicht ;-)

Den Quelltext findet ihr, meine werten Leser, hier: active_resource.js. In einem nächsten Post erzähle ich dann mehr zur internen Funktionsweise des Codes. Stay tuned!

March 24, 2008

Eine Sorge weniger! -- und die nächste folgt sogleich.

Die Zeit des Schweigens ist vorbei, die Diplomarbeit ist abgegeben und nun ist wieder viel Zeit für anderes. Natürlich wird die Entwicklung von Consolvix nicht stillstehen und genau genommen hat sich in der Hinsicht seit letzten Mittwoch auch wieder etwas getan. Dieses "etwas" hat hauptsächlich mit der Benutzeroberfläche zu tun, damit am Freitag im Kolloquium auch alle was Schönes zu sehen bekommen werden ;-)

Des Weiteren hab ich endlich mal Zeit gefunden, zusammen mit Philipp den endlich eingetroffenen Server aufzusetzen. Die Kiste ist handlich und fast schon schnuckelig:

Doch wehe, man schaltet das Gerät ein -- die dann aufheulenden Lüfter lassen ein gewisses Luftalarm-Ambiente aufkommen... Dafür ist die Kiste mit ihrem 1.8 GHz C2D und 4GB RAM ganz schön flott und mittlerweile laufen auch schon ein paar virtuelle Maschinen unter Xen darauf. Allerdings hat es die Kombination Xen + Netzwerk in sich, ich hoffe dass wir die auftretenden Probleme bald gelöst bekommen (dazu vielleicht später mehr).

Und ja, richtig geraten: Consolvix soll dereinst genau diesen Server administrierbar machen. Entsprechend wird eine der nächsten Erweiterungen von Consolvix eine Oberfläche für die ganzen xm-Scripte und Xen-Tools-Befehle sein, damit Kunden dann auch ihre eigenen VMs hochfahren, neu starten etc. können.

Stay tuned!

February 3, 2008

Ein Paar Dia-Gramm

OK, diese Diagramme wurden nicht mit Dia erstellt, sondern mit Umbrello. Aber nachdem Philipp schon solch schöne UML-Bildchen online gestellt hat, muss ich natürlich nachziehen ;-)

Das folgende Diagramm fasst noch einmal bildlich zusammen, was ich in einem anderen Post zum Ablauf einer Transaktion beschrieben hatte. Übrigens wäre ich noch immer SEHR dankbar, wenn mir jemand der Erfahrung mit dem Thema hat, ein paar Kommentarzeilen zu dem Eintrag da lassen würde!

Ablauf einer Transaktion

Das zweite Diagramm soll dem unbedarften Leser einen ungefähren Überblick über den Ablauf der Request-Verarbeitung verschaffen. In Wahrheit passieren noch ein paar unwesentliche Schritte mehr, aber ich denke nicht, dass das Weglassen dieser Schritte einen falschen Eindruck vermitteln könnte:

Ablauf eines Requests

Fragen? Ideen? => dafür gibt's das Kommentat-Formular unten ;-)

January 30, 2008

Ideen für die Konfigurationsverwaltung

Nach einem äußerst interessanten Vortrag in Essen über Themen aus der Quantenmechanik von Prof. Dr. Anton Zeilinger persönlich ergab es sich vorhin, dass Philipp und ich und ich zu einer kleinen Diskussionsrunde zusammenfanden, um ein Wenig über das Thema Konfigurationsverwaltung auf Webservern zu diskutieren. Dabei kam es, wie in solchen Diskussionen "leider" üblich, zu jeder Menge neuer Ideen, die ich am liebsten schon von Anfang an in meine Diplomarbeit eingebaut hätte.

Nachdem ich nun in meiner Serververwaltungs-Applikation so etwas wie Konfigurationsversionen eingebaut habe, stellte sich eine weitere, alles andere als triviale Frage: Wie ist das mit Konfigurationsdateien? Bekanntlich habe ich es zur Prämisse gemacht, dass Konfigurationsdateien, sofern sie sich nicht durch Datenbankeinträge ersetzen lassen, direkt von Consolvix bearbeitet werden und nicht jedes Mal aus Datenbankeinträgen generiert werden (uns somit händische Änderungen womöglich rückgängig machen). Um hier auch verschiedene Konfigurationen mit verschiedenen Einstellungen zu erlauben, wäre es denkbar, jeder Konfigurationsdatei z.B. eine entsprechende Endung zu geben die mit dem Schlüssel der jeweils aktiven Konfiguration in der Datenbank übereinstimmt. Diesen Gedanken habe ich nicht weiter verfolgt, da er mir etwas umständlich erschien. Wesentlich besser finde ich die Idee, generell alle Konfigurationsdateien (z.B. das komplette /etc-Verzeichnis) mit Subversion zu verwalten. So könnte man Konfigurationen ändern wie man lustig ist, und wenn mal irgendwann das halbe System dadurch abgeschossen sein sollte, dann checkt man eben eine ältere Konfiguration aus und versucht es von neuem. Um dann noch aus verschiedenen Systemkonfigurationen (Produktion, Wartung, ...) wählen zu können, könnte man immer noch mit Branching arbeiten: Für jede mögliche Konfiguration wird einfach ein neuer Zweig des Repositorys angelegt. Nachdem ich dann mal kurz Gebrauch vom allwissenden Dämon Google gemacht hatte, stieß ich auf ein e Beschreibung, wie man mittels SWIG die Subversion-Bindings für Ruby installiert (für Debian-Benutzer ist es noch einfacher: apt-get install libsvn-ruby ;-)). Besser noch: nach weiterem Googlen fand ich dann das Rails-Plugin acts_as_subversioned für versioniertes ActiveRecord, das Datenbankentitäten versioniert abspeichert! Etwas derartiges wäre eigentlich für mein Gesamtsystem von Anfang an sehr praktisch gewesen -- ich fürchte aber, dass es etwas zu viel Zeit kosten würde, das jetzt noch einzubauen (nach Ende der Diplomarbeit werde ich es aber auf jeden Fall weiter verfolgen!)

Ein weiteres Thema des o.g. Brainstormings war: Wie lasse ich Consolvix Veränderungen am System vornehmen, wenn diese nicht über die Datenbank abgefackelt werden? Momentan habe ich den Benutzer www-data einfach in die Gruppen gepackt, die Zugriff auf die zu den Diensten gehörenden Ordner hat, die es konfigurieren soll (Beispielsweise Gruppe subversion für /var/svn) Auf lange Sicht müsste dazu Consolvix aber root-Reche bekommen -- und spätestens hier sollten sämtliche Alarmglocken losklingeln. Also werde ich vermutlich folgende Lösung verwenden: Für alle durchzuführenden Änderungen im System wird ein Shell-Kommando oder -Script erstellt. Dieses wird an einen kleinen Dämon mit setuid=root weitergereicht, der genau zwei Kommandos ausführt: sudo <Benutzer> und das angegebene Kommando/Script. <Benutzer> ist hierbei immer die UID des gerade in Consolvix eingeloggten Benutzers. So wird sichergestellt, dass Consolvix selbst immer ein unprivilegierter Dienst bleibt und alle durchzuführenden Kommandos immer als der User ausgeführt werden, der gerade in Consolvix eingeloggt ist. Das geht, weil System- und Consolvix-Benutzer identisch sind.

secure_consolvix_exec0.png

Eine Idee war es, die so generierten Kommandos zuerst in ein Subversion-Repository einzuchecken, damit diese vom o.g. Dämon zuerst ausgecheckt und dann durchgeführt werden. Der Hintergedanke dazu war der, dass das erste Zielsystem für Consolvix aus einem Test- und einem Produktionssystem bestehen wird. Im Testsystem werden iterativ Änderungen vorgenommen und erst wenn das System zufriedenstellend lauft, werden diese auch am Produktionssystem vorgenommen. Hier wäre die Idee mit Subversion nicht schlecht, denn so würde sichergestellt werden, dass, ähnlich wie bei Rails-Migrations, immer alle Änderungen in der richtigen Reihenfolge und genau so durchgeführt werden.

secure_consolvix_exec1.png

Allerdings verletzt das die andere Arbeitsprämisse, nämlich dass manuelle Änderungen im System genauso behandelt werden sollen wie von Consolvix durchgeführte -- und jedes Mal manuell ein Shell-Skript zu erstellen, dieses einzuchecken und dann ausführen zu lassen ist gelinde gesagt etwas umständlich. Im Mainframe-Bereich ist das zwar Praxis, aber wir betreiben ja nur einen kleinen Linux-Server... Ich denke, dass in die Richtung noch mehr Gedanken folgen werden, aber bis auf Weiteres werde ich erstmal so weitermachen wie bisher.

January 27, 2008

Ein paar Worte zu Modulen in Ruby

Als Ergänzung zu meinem letzten Eintrag zum Thema "Rubys Objektmodell" folgen ein paar Notizen dazu, wie sich Module in das Gefüge einfügen (müssen sie ja irgendwie, sonst wäre es kein Gefüge ;-))

  • Wird ein Modul mittels include eingebunden, werden alle Modulmethoden zu Instanzmethoden der einbindenden Klasse.
  • Wird ein Modul mittels extend eingebunden, werden alle Modulmethoden zu Klassenmethoden der einbindenden Klasse.
  • es ist (in Rails) durchaus üblich, ein Modul Foo zu definieren, welches dann mit include Foo eingebunden wird, sowie ein Untermodul Foo::ClassMethods, welches mittels extend Foo::ClassMethods eingebunden wird. Diese Praxis habe ich einfach mal so übernommen.

So viel zu den Methoden. Doch was ist mit den Variablen? Wie wir alle wissen, werden Klassenvariablen weitervererbt, Instanzvariablen jedoch nicht (irgendwie logisch...). Aber bei Modulen...?

  • Da ein Modul nicht instanziiert werden kann, kann es auch keine Instanzvariablen (@foo) haben -- ganz einfach. Wird ein Modul mittels include eingebunden, müssen alle @foo's also Instanzvariablen der Objekte werden; wird es mit extend eingebunden, sind es eben Instanzvariablen der Klasse (und NICHT etwa Klassenvariablen oder so...)
  • Klassenvariablen (@@bar) werden bei include einfach übernommen -- sprich, was im Modul eine "Klassen"variable war, wird auch in (den Instanzen) der einbindenden Klasse als Klassenvariable erhalten bleiben. Bei extend jedoch sind alle im Modul definierten Klassenvariablen nur für die im Modul definierten Methoden erreichbar! Alle Versuche, aus dem Kontekt der Klasse (oder einer ihrer Instanzen) an sie heranzukommen, sind bei mir bislang gescheitert.

An einem Beispiel zeigt sich so was immer schön:

module Foo
  @@truth = 42

  def say_the_truth
    p @@truth
  end
end

class Bar
  include Foo
end
Bar.new.say_the_truth # 42 *SMILE*

class FooBar < Bar
  def say_something
    p @@truth
  end
end
FooBar.new.say_something # 42 *SMILE*

class Baz
  extend Foo

  def say_something
    p @@truth
  end
end
Baz.say_the_truth # 42 *SMILE*
Baz.new,say_something # PENG! WUMMS! "NameError: uninitialized class variable @@truth in Baz" ... *Autsch*

Alles klar?

January 25, 2008

Rubys Objektmodell

OK, was ich jetzt hier schreibe ist nichts neues und auch schon auf viel bessere Art und Weise von Why the Lucky Stiff beschrieben worden. Jedenfalls trieb mich ein Bug in Consolvix dazu, mich nochmals intensiver mit dem Thema Metaprogrammierung und dem Ruby-Objektmodell auseinanderzusetzen. Es folgen ein Paar Notizen.

  • Objekte in Ruby können nur Variablen aufnehmen -- Ein Ruby-Objekt hat keine Methoden
  • Objekte sind in erster Linie dies: Objekte. Erst in zweiter Linie sind sie Instanzen einer Klasse. Auch Klassen sind Objekte (ABER auf C-Quellcode-Ebene sind Object und Class zwei verschiedene Structs -- ich halte diese Information für wichtig, wenn man wirklich blicken will, warum diese Class-Objekt-Abhängigkeit keine unendliche Rekursion mit sich bringt)
  • Objekt-/Instanz-Methoden befinden sich in der Klasse, von der sich das Objekt ableitet. Eine Klasse ist nicht ein statisches, abstraktes Dingda, sondern ein Singleton-Objekt. Ein Objekt, das (Klassen-)Variablen UND Methoden aufnehmen kann. Eine Klasse eben. (Und hier würde dann die Rekursion einsetzen ;-))
  • Klassenmethoden befinden sich -- nein, nicht in der Klasse, die ja ein Objekt ist, sondern in der Klasse, die meistens Metaklasse genannt wird.
  • Klassen vererben ihre Methoden ihren Unterklassen. Somit steht, wenn man eine Klasse um eine neue Methode bereichert, sofort sämtlichen Unterklassen und sämtlichen Instanzen der Klasse diese Methode zur Verfügung. Wenn man aber einem einzelnen Objekt an dieser Stelle eine andere Implementierung dieser Methode geben möchte, so kann man dem Objekt diese Methode über die Metaklasse dieses Objektes zur Verfügung stellen. Methoden werden zuerst in der Metaklasse dann erst in der Klassenhierachie gesucht.
  • Das Konzept der Vererbung erweitert eine Klassenhierachie vertikal, während Module die Klassenhierachie horizontal erweitern -- man könne also sagen, dass das Konzept der Metaklassen die Hierachie in der tiefe erweitern -- aber das klingt gleich wieder so esotherisch...
  • Metaklassen können, da diese wiederum Objekte sind, selbst auch wieder Metaklassen enthalten. Eine Veränderung der Metaklasse einer Metaklasse einer Klasse zieht jedoch keinerlei Konsequenzen für die Klasse nach sich. Außerdem ist es sehr unwahrscheinlich, dass von diesem Konzept jemals jemand Gebrauch machen wird (OK, ich werde meinen kranken Geist darauf ansetzen, in einer freien Minute etwas passendes zu finden ;-))

Folgendes ER-Diagramm hab ich nach meinem Verständnis des Sachverhaltes zusammengestellt. Wenn jemand darin einen grundlegenden Fehler entdeckt, bitte sofort melden!

Dieses Klassendiagramm habe ich nach dem ASCII-Diagram aus einem (vermutlich bekannten) Post in der Ruby-Talk-Mailingliste gebastelt. Es stellt die Abhängigkeit der Klassen und Objekte untereinander dar. Wie man sieht, erbt letztendlich alles von Object. Die Klassen mit eingeklammerten Namen sind die jeweiligen Meta-Klassen (bzw. Singleton-Klassen-Instanzen...).

RubyObjectModelInheritances.png

Ich finde, dass das Bild etwas klarer wird, wenn man die Vererbung durch eine gerichtete Beziehung der Objekte untereinander ersetzt. super zeigt somit immer auf das Objekt der Elternklasse während self auf das Objekt der Singleton-Klasse zeigt.

RubyObjectModelReferences.png

So hat man im Wesentlichen die Abbildung des Ruby-Objektmodells auf C-Ebene vor sich: Vererbung und "magische Meta-Dingsda" sind nichts weiter als Structs, von denen einige lediglich Variablen, andere auch Methoden referenzieren können... der ganze Zauber gelüftet, aber eine Menge mehr Klarheit geschaffen :)

Zu dem ganzen ist anzumerken, dass nicht zu jedem Objekt a priori eine Singletonklasse existiert -- das würde unendlich viel Speicher erfordern. Eine Singletonklasse wird nur dann erstellt, wenn sie explizit angefordert wird, und zwar über

class Foo
  # ...
end

obj = Foo.new

class << obj
  # hier sind wir im Kontext der Metaklasse von obj!
end

class << Foo
  # hier sind wir im Kontext der Metaklasse von Foo!
end

class Foo
  class << self
    class << self
      # im Kontext der Metaklasse der Metaklasse von Foo ;-)
    end
  end
end

Alles klar? Nicht? Dann hilft nur lesen des o.g. Artikels von Why the Lucky Stiff oder sich einfach mit anderen Dingen beschäftigen...

January 21, 2008

Ein weiterer Rails-Bug

Vielleicht erinnert sich der eine oder andere aus der Schnittmenge der treuen Leser dieses Blogs und der regelmäßig bei unserer Diplomandenrunde in Dortmund Anwesenden noch daran, dass wir "damals" erwähnt hatten, dass es manchmal, aber nicht immer, in Rails zu einer Exception kommt, wenn man z.B. sowas hier probiert:

@client.user.system_groups.access_rights

obwohl alles ganz sicher korrekt mit has_many usw. definiert wurde. Ich werde hier nichts breittreten, was mich grob geschätzt die letzten 4 Stunden und viel mehr Nerven als diese Stunden Sekunden haben, gekostet hat, sondern es in einem einfachen Satz zusammenfassen:

NIEMALS in Join-Tables einen Primärschlüssel namens 'id' verwenden!!

das Problem ist, dass bei einem Join wie diesem:

User.find(17).system_groups

dieses SQL generiert wird:

SELECT * FROM `system_groups` 
  INNER JOIN groups_users 
    ON system_groups.id = groups_users.group_id 
WHERE (groups_users.user_id = 17 )

Na, wem fällt's auf? genau, da steht SELECT * FROM ..., und nicht, wie es sich gehört, SELECT system_groups.* .... Damit tritt der Spaltenname id nämlich zweimal im Resultat auf (einmal von der Gruppentabelle und einmal von der Join-Table), wobei letzten Endes die ID von der Join-Table von Rails verarbeitet wird und dann als FALSCHE system_group_id eingesetzt wird.

Nach dem Entfernen der id Spalte aus groups_users lief alles wie erwartet.

Man mag mich bei Gelegenheit dafür steinigen, dass ich in Join-Tables eine künstliche ID-Spalte verwende, aber Rails' Verhalten bezeichne ich in diesem Fall als schlichtweg falsch. I hereby declare it a bug.

January 20, 2008

Von etwas komplizierteren Beziehungen

Wenn A viele B hat, und C viele B hat, kann C auch viele A haben, wenn C :through benutzt. Wenn A aber viele B und B viele C hat, außerdem C zu vielen B gehört, dann kann C nicht auch viele A haben, selbst wenn B :through benutzt.

Ich wollt's nur mal erwähnt haben, sollte jemand von euch das auch mal probieren wollen.

Wie Bahnhof? Gut, dann nochmal kurz:

:through geht nicht bei hasandbelongstomany-Assoziationen.

Mein Problem:

class AccessRight
  # ...
end

class AccessRightGrant
  belongs_to :acces_right
  belongs_to :subject,
             :polymorphic => true
end

class SystemGroup
  has_many :access_right_grants,
           :as => :subject
  has_many :access_rights,
           :through => :access_right_grants
  has_and_belongs_to_many :users
end

class User
  has_and_belongs_to_many :system_groups
  has_many :access_right_grants,
           :as => :subject
  has_many :user_access_rights,
           :class_name => 'AccessRight'
           :through => :access_right_grants
  has_many :access_rights,
           :class_name => 'AccessRight',
           :through => system_groups
end

... geht also NICHT.

Was ich erreichen möchte, ist dies: User hat AccessRights, SystemGroup hat AccessRights, User hat alle AccessRights, die SystemGroup auch hat. User.access_rights soll also alle , nicht nur des User's AccessRights zurückliefern (in obigem Code sollten erstmal nur die AccessRights der Gruppen geladen werden, nicht alle). AccessRightGrant ist die Linking Table zwischen AccessRight und User/SystemGroup, wobei letztere über die polymorphische subject-Spalte gelinkt werden.

Dem mit o.g. Code generierten SQL nach zu urteilen, liegt das Problem bei der HABTM-Beziehung. Bestätigt hat das ein Tauchgang in den Rails-Source, der übrigens mit seinen n Metaprogrammier-Ebenen mehr als faszinierend und beeindruckend ist, wenn man sich mal etwas Zeit für ihn nimmt. Bei einer normalen has_many-Beziehung zwischen User und SystemGroupkönnte es funktionieren, ausprobiert habe ich das jedoch nicht.

Aber es lässt sich für alles eine Lösung finden und bis ich hierfür eine elegante Lösung gefunden habe, werde ich einfach ganz skrupellos brute-force-Methoden wie "lade alle Rechte und durchsuche das Array" benutzen. Tja, Rails, das haste nun davon :-)

January 19, 2008

Verdeckte Transaktionen

Wie schon vor einiger Zeit in diesem Eintrag beschrieben, bietet Consolvix eine durchaus ausgeklügelte Möglichkeit, Transaktionen über mehrere Schritte zu implementieren. Gestern Nacht kamen mir kurz vorm Einschlafen dazu noch zwei Ideen, die ich vielleicht noch umsetzen möchte (hier wäre mir die Meinung eines Experten sehr willkommen).

  1. Momentan speichere ich die Daten zu einer Transaktion als YAML serialisiert im data-Feld einer ConsolvixTransaction ab. Wird die Transaktion abgebrochen, werden alle temporären Daten also gelöscht und gut ist. Aber eigentlich, ja EIGENTLICH, ist das ja nicht ganz sauber -- wenn man doch ein User-Objekt hat, und sei es nur ein temporäres, dann gehört das ja eigentlich in die users-Tabelle und in der Transaktion sollte nur die ID und der Typ des Objektes gespeichert werden. Wenn dann die Transaktion gelöscht wird, sollten diese temporären Objekte ebenfalls gelöscht werden. An sich kein Problem, ich bin mir sogar sicher, dass sich das äußerst elegant über HABTM und polymorphische Aggregation lösen ließe. Aber: So lange die Transaktion nicht abgeschlossen ist, befinden sich also unter den "gültigen" Objekten auch "ungültige", also solche, die überhaupt noch nirgends in der restlichen Applikation auftauchen sollten! Also müsste dann doch irgendwie jedes temporäre Objekt als solches markiert werden -- und dann hört die Sache wieder auf, elegant zu sein -- ganz im Gegenteil gar. Dabei sei angemerkt, dass theoretisch jede Entität (aus 20, 30, vielleicht auch irgendwann mal über 50 möglichen) in einer Transaktion vorkommen kann. Also doch lieber bei der jetzigen Methode (also dump/serialisieren) bleiben? Wie werden solche vorläufigen Daten in Businessapplikationen "der Großen" gehandhabt? Hilfe...?
  2. "Points of no Return" einbauen. Bitte was? Gerade bei Webhosting-Angelegenheiten gibt es Aktionen, die die Bestätigung des Benutzers/Kunden erfordern. Beispiel: neue Bestellung, bestehend aus vier Schritten. Schritt 1: Kunde wählt z.B. "5GB mehr Speicher". Schritt 2: Kunde bestätigt Bestellung und erklärt sich mit irgendwelchen Bedingungen einverstanden. Dann wird eine E-Mail an die Buchhaltung geschickt, diese bestätigt Schritt 3, sprich "Kunde bekommt nun 5 GB Speicher mehr abgerechnet". Dann wird eine E-Mail an den Admin geschickt, dieser bestätigt Schritt 4, dass die 5 GB auch tatsächlich freigeschaltet werden. Diese Transaktion hätte zwei "Points of no return": Nachdem der Kunde Schritt 2 bestätigt hat, kann er nicht mehr zu Schritt 1 zurück, was mit der aktuellen Transaktionsverwaltung aber jederzeit möglich wäre. Der zweite PonR wäre, nachdem die Buchhaltung ihren Teil bestätigt hat -- danach kann auch nicht mehr zu Schritt 3 zurückgesprungen werden, sondern entweder der Admin bestätigt auch Schritt 4, oder er bricht die ganze Transaktion ab und alles bleibt beim alten. Ein anderes Beispiel wäre z.B. das Registrieren eines neuen Accounts: der potentielle Kunde füllt drei Seiten Formularkram aus, schickt diese ab und kann danach nichts mehr an Schritt 1..3 ändern, auch wenn die Transaktion an sich erst abgeschlossen ist, wenn die Buchhaltung und der Admin (kann meinetwegen die gleiche Person sein) noch ihre 1...n Schritte abgeschlossen haben. Mir scheint es eigentlich sinnvoll und durchaus elegant, neben der Definition einer Sequenz von Actions als Schritte in einer Transaktion auch "Synchronisationspunkte" zu definieren, nach denen entweder abgebrochen oder weitergemacht, nicht aber zurückgesprungen werden kann. Wird das bei Datenbanktransaktionen nicht auch im Grunde genommen so verwendet? Stating the obvious? Denke ich nun wieder zu kompliziert? Meinungen? Buh-Rufe? Faule Eier?

Jedenfalls kommen noch weitere Änderungen an der bestehenden Transaktions-API hinzu, weil ich gemerkt habe, dass sich noch einige Komplifikationen ergeben, sobald verschiedene Teile einer Transaktion von unterschiedlichen Benutzern (teilweise noch nicht einmal vorhandenen!) ausgeführt werden können müssen.

...

und ich glaube, Deutsch ist tatsächlich die einzige Sprache, in der vier aufeinander folgende Verben ein grammatikalisch korrektes Konstrukt bilden können... ;-)

FTF?!

Dieses Blog, dessen Autor sich unter der eindeutigen ID Willem van Kerkhof ansprechen lässt, beschäftigt sich im Wesentlichen mit Themen rund um des Autors Diplomarbeit und um Ruby, Rails, Apache, Linux-Server, Javascript, anderem Geek-Kram und der Kombination aus diesen Themen.

Powered by
Movable Type 3.31