Archiv der Kategorie: Computer

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.

Zu wenig Speicherplatz beim Samsung Galaxy Ace GT-S5830i

Wer ein Galaxy Ace besitzt, guckt schon nach der Installation nur weniger Apps ins Leere: Mit der Fehlermeldung „Zu wenig Speicherplatz“ wird die Installation neuer Apps unterbrochen! Da zudem Google ohne Rückfrage zusätzliche Apps wie „Play Books“ installiert, schrumpft der Speicherplatz (interner Telefonspeicher)  so weit, dass bereits Updates von vorhandenen Apps unmöglich werden. Was also tun, wenn gerade kein Geld für das neuere und teurere Modell vorhanden ist?

Vorübergehende Lösung: App 2 SD

Mit der App 2 SD (Play Store) können Anwendungen vom internen Telefonspeicher (bei mir 190 MB) auf die SD-Karte (1.884 MB) verschoben werden, die deutlich mehr Daten speichern kann. Der große Wermutstropfen bei dieser App ist jedoch, dass sich nur wenige Anwendungen verschieben lassen. Bei mir konnten lediglich zwei Apps verschoben werden, was zur zwischenzeitlichen Lösung des Speicherplatz geführt hat… Im Endeffekt konnte ich mein Smartphone aber erst durch einen tiefer gehenden Eingriff wieder in den Zustand versetzen, neue Apps zu installieren.

DIE Lösung: Link2SD

Mit Link2SD (Play Store) sind tiefer gehende Eingriffe möglich, so können auch die Anwendungen verschoben werden, die App 2 SD nicht auf die SD-Karte verschieben kann. Link2SD kann sogar vorinstallierte Anwendungen deinstallieren, um noch mehr Speicherplatz freizugeben. Link2SD benötigt allerdings Root-Rechte auf dem Gerät, das sind Zugriffsrechte, die dem Benutzer auf dem Smartphone standardmäßig nicht gegeben werden. Wird das Gerät durch den Benutzer trotzdem gerootet, erlischt die Herstellergarantie.

In meinem Fall konnte ich auf meinem Smartphone gerade mal Whatsapp, Foursquare, den Google Reader und irgendeine App zur Synchronisation mit dem Firmen-MS-Exchange-Server nutzen, was mir eindeutig zu wenig war. Die Alternative wäre mir ein teureres (und neueres) Smartphone zu kaufen – die Obsoleszenz-Bemühungen des Herstellers und Google wollte ich nicht unterstützen, sodass ich mich entschloss mein Gerät trotz Garantie zu rooten.

Rooten

Das Rooten des Samsung Galaxy Ace GT-S5830i ist überraschend einfach:

  1. Download der Datei root.zip (die Datei wird automatisch auf der SD-Karte im Ordner /download/ gespeichert) [via youtube]
  2. Schalte dein Smartphone aus
  3. Starte das Smartphone im Recovery Modus. Dazu sind folgende drei Tasten beim Anschalten des Smartphone gleichzeitig zu drücken bis das Samsung Galaxy Ace GT-S5830i-Logo erscheint: die Home- + Volume Up- + Power-Taste
    Recovery-Modus beim Samsung Galaxy Ace GT-S5830i
  4. Es öffnet sich der Recovery-Modus (Android System Recovery).
  5. Gehe mit dem Kippschalter für lauter/leiser zum Eintrag apply update from sdcard und bestätige mit der Home-Taste (die Taste auf der Vorderseite unten mittig).
  6. Wähle den Ordner download/ aus
  7. Wähle die heruntergeladene Datei root.zip aus
  8. Wähle anschließend reboot system now aus
  9. Das Smartphone startet wie gewohnt
  10. Ist die zusätzliche App Superuser installiert, hat das Rooten geklappt!

Anschließend kann die App Link2SD (Play Store) installiert und verwendet werden. Die Anwendung listet alle Apps auf dem Smartphone auf und die meisten Apps können problemlos auf die SD-Karte verschoben werden, sodass der verfügbare interne Telefonspeicher wieder neu befüllt werden kann und zusätzliche Apps installiert werden können.

Ein wichtiger Hinweis noch: Das Verschieben/Deinstallieren von System-Anwendungen sollte nur durchgeführt werden, wenn ihr wisst, welche Auswirkungen das haben kann… Nämlich GANZ BÖSE!

Highcharts und PrinceXML

Hintergrund

Mein Arbeitgeber lebt zu 90% von unterschiedlichsten Berichten. Diese werden bis dato in Word geschrieben, ggf. mit Seriendruckfeldern aufgefüllt und separat Diagramme in Excel erstellt. Fast alle Kollegen sind tagelang damit beschäftigt, Berichte zu erstellen, zu formatieren und zu kontrollieren. Gerade bei größeren Berichten ist die Arbeit stupide, denn Werte werden kopiert, Diagramme erstellt, Diagramme im Bericht eingefügt, ggf. wird festgestellt, dass beim Kopieren ein Fehler unterlaufen ist und der Spaß beginnt von vorne.

Beim aktuellen Bericht sollten Standortprofile für die einzelnen Bezirke einer Stadt erstellt werden: pro Bezirk sollen zwei Seiten mit Tabellen, Kommentaren und 16 Diagrammen erstellt werden. Bei 81 Bezirken müssen also knapp 1.300 Diagramme erstellt werden. Das händische Erstellen der Diagramme per MS Office hätte bestimmt Wochen beansprucht. Die automatische Berichtsgenerierung mit PHP, MySQL, Highcharts, Javascript, Ajax und PrinceXML dauert jetzt 7 Minuten.

Technische Herausforderungen

PrinceXML nimmt ein HTML-Dokument und erstellt daraus ein druckbares PDF. Mit PrinceXML kann man über HTML und CSS Seitenumbrüche, Seitenzahlen, Seitenabstände, Fußnoten, Inhaltsverzeichnisse und andere druckrelevante Features steuern. So wird bspw. aus dieser HTML-Seite diese PDF-Datei.

Eine weitere Herausforderung ist die Verwendung von Highcharts mit PrinceXML. Warum? Highcharts wird per Javascript im Browser des Besuchers gerendert. PrinceXML kann zwar auch Javascript interpretieren, aber für Highcharts reicht die Unterstützung bislang nicht aus (erst in Version 8.2 soll Highcharts unterstützt werden). Es gibt verschiedene Ansätze dieses Problem zu lösen, denn HighCharts erzeugt eine SVG, welche durch PrinceXML problemlos dargestellt werden kann: Serverseitiges Ausführen von Highcharts mit Node.JS oder PhantomJS oder indem man CutyCapt als serverseitigen Browser verwendet, der das erzeugte SVG dann an das Skript zur weiteren Verarbeitung übergibt. Mich überzeugte keines der Ansätze, denn

  1. es ist zusätzliche Software notwendig, in dessen Abhängigkeit man sich begibt
  2. in meinem Anwendungsfall ist es problemlos möglich den Bericht im Browser anzeigen zu lassen, dort die Diagramme generieren zu lassen und die SVG-Daten in einer Datenbank zu speichern, um sie später im Bericht für PrinceXML zu verwenden.

Aufbau

Aufbau: Highcharts und PrinceXML

Die einzelnen Schritte sind:

  1. Rufe alle Daten zur Erstellung der Diagramme des Berichts ab
  2. Erstelle eine Berichtsversion, in der Highcharts über Javascript generiert wird
  3. Im Bericht werden nach Erstellung der Diagramme die SVG-Daten per Ajax abgesetzt
  4. und durch die svgsaver.php-Datei in der Datenbank gespeichert
  5. Zur Erstellung des Berichts mit den SVG-Daten, werden diese von der DB abgefragt
  6. Die SVG-Berichtsversion enthält kein Javascript und kein Highcharts-Code mehr, sondern nur noch die SVG-Daten
  7. Die SVG-Berichtsversion wird von PrinceXML zur…
  8. … Erstellung des PDFs verwendet

svgsaver.php

Der SVG-Saver sichert lediglich die per Ajax erhaltenen Daten mit folgenden Feldern: berichts_id, container (Name des Highchartscontainers), svg (die eigentlichen SVG-Daten) und eingefuegt.

[code lang=’php‘]
$stmt = $mysqli->prepare(„INSERT INTO highcharts_svg SET berichts_id=?, container=?, svg=?, eingefuegt=NOW()“);
if (isset($_POST[„berichts_id“]) AND isset($_POST[„container“]) AND isset($_POST[„svg“])){
$berichts_id = intval($_POST[„berichts_id“]);
$svg = $_POST[„svg“];

$stmt->bind_param(‚iss‘, $berichts_id, $_POST[„container“], $svg);
$stmt->execute();
}

$stmt->close();[/code]

bericht.php

Die bericht.php muss je nachdem welche Berichtsversion angezeigt werden soll, einen Platzhalter (div-Container) für das Highcharts-Diagramm anzeigen oder die SVG-Daten aus der Datenbank. Das übernimmt die Funktion container, die dies abhängig von der Variable $show_svg realisiert:
[code lang=’php‘]function container($name, $height=270, $width=470){
global $show_svg, $berichts_id, $mysqli;
if ($show_svg){
$stmt = $mysqli->prepare(„SELECT svg FROM highcharts_svg WHERE berichts_id=? AND container=? ORDER BY eingefuegt DESC LIMIT 1“);
$stmt->bind_param(‚is‘, $berichts_id, $name);
$stmt->execute();
$stmt->bind_result($svg);
$stmt->fetch();
echo $svg;

} else {
echo ‚

‚;
}
}[/code]

Im Headbereich der HTML-Seite wird über folgenden JS-Code das SVG der generierten Diagramme per Ajax an die vorher beschriebene svgsaver.php gesendet:
[code lang=’js‘]$(window).load(function() {
for (var i = 0; i < charts.length; ++i){ $.ajax({ type: 'POST', url: 'http://yourdomain.com/svgsaver.php', data: { berichts_id: 1, container: charts[i].options.chart.renderTo, svg: charts[i].getSVG() }, statusCode: { 404: function() { alert("Highcharts-Diagramme konnten in der Datenbank nicht gespeichert werden."); } } }); } });[/code] Alle Highcharts-Diagramme müssen folglich in dem Array charts enthalten sein, weswegen das Highcharts.Chart-Objekt in dem charts-Array gespeichert wird:
[code lang=’js‘]var charts = Array();
$(function () {
$(document).ready(function() {
var i = 0;
charts[i++] = new Highcharts.Chart({
chart: {
renderTo: ‚container_1‘,
type: ‚line‘,
…[/code]

Das sollte bei der Lösung der größten Hürden helfen, denn noch detaillierter möchte ich hier nicht werden.

Nachteile

Dieser Lösungsansatz zur Verwendung von Highcharts mit PrinceXML ist sicherlich nicht der Weisheit letzter Schluss:

  1. Es sind immer zwei Schritte nötig, um das PDF zu erzeugen: 1. Aufrufen der Highcharts-Berichtsversion, um die SVG zu erzeugen und 2. PrinceXML mit der SVG-Version aufrufen.
  2. Die Diagramme werden im Browser und nicht auf dem Server generiert. Bei sehr vielen Diagrammen wird der Client daher stark belastet. (Für die Generierung der 1.300 Diagramme braucht der Browser auf unseren Workstations ~ 7 Minuten).

aber es funktioniert! Und wie sagte George S. Patton?

Ein guter Plan heute ist besser als ein perfekter Plan morgen.