Schlagwort-Archive: Model

Rails: Dynamische Assoziation zwischen verschiedenen Datenbanken

Über Assoziationen lassen sich in Ruby on Rails verschiedene Modelle miteinander verbinden und so einfacher Daten abfragen, aktualisieren und einfügen. In diesem Artikel geht es um zwei Aspekte, die ich im Internet nur schwer recherchieren konnte:

  1. Assoziationen zwischen verschiedenen Datenbanken
  2. Dynamische Assoziationen (der Tabellenname des assoziierten Modells ist dynamisch und hängt von einem Attribut ab)

Anwendungsbeispiel

In meinem Fall geht es um das Modell Contact, welches in der Rails-Anwendung die Kontakte/Aufgaben eines Testmieters verwaltet. Pro Kontakt wird in LimeSurvey ein Teilnehmer eines Fragebogens hinterlegt, der den Fragebogen ausfüllen kann. Die eingegebenen Daten des Testmieters sollen in der Rails-Anwendung mit dem Kontakt (Contact) verbunden werden. Dabei gibt es diese Herausforderungen:

  • Die Datenbank, in dem die Tabelle contacts liegt, ist ralv während die Fragebogen-Antworten in der Datenbank limesurvey liegen.
  • Die Fragebogen-Eingaben liegen je nach Kontakt in verschiedenen Tabellen. Der Tabellenname ist z.B. lime_survey_661185, wobei 661185 die ID des Fragebogens in LimeSurvey ist. Die ID des Fragebogens wird im Kontakt gespeichert.

Die Struktur der Tabellen ist also:

Tabellenstruktur

wobei der Name der Tabelle lime_survey_* von contacts.fragebogen_id abhängt. Deutlicher wird dieser Aspekt in der Datenansicht:

Daten

Assoziationen zwischen verschiedenen Datenbanken

Kümmern wir uns zunächst um diesen einfacheren Part: Modelle aus unterschiedlichen Datenbanken. Als Erstes muss eine zusätzliche Datenbank-Verbindung in /config/database.yml eingerichtet werden:

[code]limesurvey:
adapter: mysql2
encoding: utf8
username: [DBUSER]
password: [DBPASSWORD]
database: limesurvey[/code]
Für die Tabellen lime_survey_* wird zudem ein Modell namens LimesurveyData erstellt:
[code lang=“ruby“]class LimesurveyData < ActiveRecord::Base
establish_connection :limesurvey
self.table_name_prefix = ‚limesurvey.‘
self.table_name = ‚lime_survey_661185‘
has_one :contact, :primary_key => "token", :foreign_key => "token"
end[/code]
Betrachten wir den Quellcode zeilenweise:
1. Zeile: Die Klasse erbt von ActiveRecord, womit sie die Funktionen eines Rails-Modells erhält.
2. Zeile: Für die Klasse soll die vorher in /config/database.yml eingerichtete Datenbank-Verbindung verwendet werden.
3. Zeile: Um generell eine andere Datenbank in einer SQL-Abfrage zu spezifizieren, wird folgende Syntax verwendet Datenbank.Tabelle.Spalte. Rails 3.1 spezifiziert standardmäßig nicht die Datenbank in Abfragen, daher wird die Datenbank über den table_name_prefix in allen Abfragen des Modells eingefügt, sodass klar ist, welche Datenbank bei diesem Modell zu verwenden ist.
4. Zeile: Der Tabellenname wird normalerweise aus dem Klassennamen errechnet. In diesem Fall soll zunächst statisch die Tabelle lime_survey_661185 verwendet werden.
5. Zeile: Die Assoziation zu dem Modell Contact wird als 1:1-Beziehung eingerichtet. Dabei wird festgelegt, dass über das Attribut token in beiden Tabellen die Verbindung hergestellt wird.

In der Contact-Klasse wird zusätzlich ebenfalls die Assoziation zur LimesurveyData-Klasse eingerichtet:
[code lang=“ruby“]class Contact < ActiveRecord::Base

has_one :limesurvey_data, :class_name => "LimesurveyData", :primary_key => "token", :foreign_key => "token"

end[/code]
Der table_name_prefix muss in der Contact-Klasse nicht gesetzt werden, da standardmäßig die richtige Datenbank verwendet wird.

Dynamische Assoziationen

Um die dynamische Assoziation, also den richtigen LimesurveyData-Tabellennamen in Abhängigkeit zur Fragebogen-Id zu setzen, haben wir den wichtigsten Befehl bereits in der LimesurveyData-Klasse kennengelernt: self.table_name

Die Frage ist, an welcher Stelle der Tabellenname gesetzt werden muss. Der Tabellenname kann nicht in der LimesurveyData-Klasse definiert werden, weil dort unklar ist, welche Fragebogen-Id für die Assoziation relevant ist. Die Contact-Klasse ist der bessere Kandidat, denn schließlich wird dort die Fragebogen-Id gespeichert. Aber wie kann der Tabellenname eines anderen Modells gesetzt werden? So:
[code lang=“ruby“]class Contact < ActiveRecord::Base

after_initialize :setTablename

has_one :limesurvey_data, :class_name => "LimesurveyData", :primary_key => "token", :foreign_key => "token"

def setTablename
LimesurveyData.table_name = "lime_survey_#{self.fragebogen_id}"
end

end[/code]
Starten wir diesmal in mit der setTablename-Methode in den Zeilen 7-9: Sie setzt den Tabellenname der Ḱlasse LimesurveyData auf den dynamischen Namen.
In Zeile 3 wird diese Funktion als Callback-Funktion definiert, die immer dann ausgeführt wird, wenn die Contact-Klasse initialisiert wurde.

Und das ist alles: Wir können nun ganz einfach von einem Kontakt auf die korrekten Fragebogen-Antworten zugreifen, obwohl die Daten in einer anderen Datenbank liegen und über verschiedene Tabellen verteilt sind.

Paradigmenwechsel: High Performance Webanwendungen

Ich programmiere schon seit einiger Zeit Webanwendungen. Selten stieß ich dabei auf irgendwelche Performanzgrenzen. Dynamische Webanwendungen sind vom Prinzip her ziemlich simpel gestrickt:

  1. Die Webanwendung erhält eine Anfrage
  2. Die Anwendung holt sich z.B. Daten aus der Datenbank, um die Anfrage beantworten zu können
  3. Die Webanwendung bereitet die Daten auf und gibt sie als HTML aus, die der Browser hübsch dem Benutzer anzeigt

Das gute, alte EVA-Prinzip: Eingabe, Verarbeitung, Ausgabe

Bei Servern, die nicht sonderlich viele Anfragen erhalten, ist das ein bewertes Prinzip. Ein schon etwas in die Jahre gekommener 1-Kern-Webserver konnte beispielsweise locker 5.000 Besucher am Tag bedienen. Nimmt die Anfragefrequenz einer Webseite jedoch weiter zu, muss man sich etwas überlegen. Eine Möglichkeit ist, sich weitere Server anzumieten, um bestimmte Aufgaben auszugliedern – z.B. ein eigener Datenbankserver.

Man kann auch aufs Cachen setzen: Anstatt bei jeder Anfrage die gleichen Berechnungen durchzuführen und auf die DB zuzugreifen, speichert man das Ergebnis oder die auszugebende Seite im Arbeitsspeicher zwischen, so dass sie von dort ohne teure CPU-Zeit und merklich schneller ausgegeben werden kann.

Und wenn diese Maßnahmen nicht mehr ausreichen? Was dann?

Vor genau diesem Problem stand Steve Huffman bei reddit, die monatlich 270 Millionen Anfragen erhielten. In folgendem Vortrag spricht er über die sieben Lehren aus der Zeit bei reddit (eine Transkription gibt es bei ThinkVitamin):

Lessons Learned while at Reddit from Carsonified on Vimeo.

Neben dem „Open Schema“ (oder Entity-Attribute-Value-Model) der Datenbankstruktur finde ich das Zusammenspiel zwischen Anwendung, Cache und Datenbank äußerst interessant. Sämtliche Inhalte von reddit liegen im Cache und sind vorberechnet, sodass die Inhalte ohne Berechnungen angezeigt werden können. Werden Inhalte verändert – etwa Upvote für ein Link, wird der Cache zur Anzeige der höheren Bewertung angelegt, sodass der Benutzer sofort die Rückmeldung bekommt, dass sein Vote erfolgreich war. Des weiteren wird in einer Queue vermerkt, dass der Link einen Vote erhalten hat. Im Hintergrund wird diese Queue abgearbeitet, sodass diese Berechnungen keinen Einfluss auf die Wartezeit des Anwenders haben. Der Vote-Eintrag in der Queue hat z.B. folgende Aktionen zur Folge

  1. Upvote wird in der persistente DB gespeichert, sodass Cache & DB konsistent gehalten werden
  2. Neuberechnung des Caches der betroffenen Übersichten (Listen)
  3. Überarbeitung des Accounts (Cache)
  4. etc.

Fazit

Diese Vorgehensweise der Programmierung einer Webanwendung unterscheidet sich also grundlegend von meiner bisherigen Programmierung: Daten werden redundant gehalten, weil Speicher billig, CPU aber teuer und ein schnell bedienter Kunde (Besucher) wichtig ist. Das Video mit den Einsichten von Steve Huffman in die Abläufe bei reddit stellen für mich einen Paradigmenwechsel dar.

Natürlich: Webanwendungen ohne den Ausblick auf viele Zugriffe, benötigen keine solche Programmierung. Wenn man jedoch auf Skalierbarkeit wert legt, sollte man sich die vorgestellten Lehren besser zu Herzen nehmen…