@williamshatner Yes, Standard Orbit, Captain. And we're detecting signs of life on the surface.
— Chris Hadfield (@Cmdr_Hadfield) Januar 3, 2013
[via Nerdcore]
@williamshatner Yes, Standard Orbit, Captain. And we're detecting signs of life on the surface.
— Chris Hadfield (@Cmdr_Hadfield) Januar 3, 2013
[via Nerdcore]
Viele Wege führen nach Rom. So auch bei Abfragen in Rails. Es gibt 3 verschiedene Wege, um Konditionen zu formulieren:
class User < ActiveRecord::Base
def self.authenticate_unsafely(user_name, password)
where("user_name = '#{user_name}' AND password = '#{password}'").first
end
def self.authenticate_safely(user_name, password)
where("user_name = ? AND password = ?", user_name, password).first
end
def self.authenticate_safely_simply(user_name, password)
where(:user_name => user_name, :password => password).first
end
end
Die String-Methode ist unsicher, da die Daten nicht bereinigt werden und so SQL-Injections möglich sind.
Die Array-Methode ist sicher und erinnert syntaktisch stark an Prepared Statements.
Die Hash-Methode ist ebenfalls sicher, aber typisch Rails, weil einfach: lediglich ein Hash wird übergeben. Der Schlüssel (key) als Symbol entspricht der Spaltenbezeichnungen der Tabelle und der Wert (value) definiert die Bedingung.
Das Schöne an der Hash-Methode ist neben der Sicherheit, dass das Hash wesentlich einfacher dynamisch berechnet werden kann (als die Array-Methode):
def all_by_employee_or_manager(employee_id=nil, manager_id=nil) condition = Hash.new condition[:employee_id] = employee_id unless employee_id.blank? condition[:manager_id] = manager_id unless manager_id.blank? where(condition) end
Sobald jedoch in der Abfrage gejoint wird, bekommt man bei einer Hash-Bedingung wie
joins(:employees).where(:employees.name => name)
folgenden Fehler:
NoMethodError: undefined method `name' for :employees:Symbol
Das Problem ist, dass das Symbol :employees.name nicht komplett als Symbol interpretiert wird, sondern nur bis zum Punkt.
Bei der Internetrecherche habe ich für gejointe Abfragen nur Konditionen im String- oder Array-Format finden können, was in meinem Anwendungsfall aber nicht praktikabel war, denn die Konditionen mussten dynamisch berechnet werden.
Um Konditionen in gejointen Abfragen mit der Hash-Methode anzugeben, muss Ruby klar gemacht werden, dass employees.name komplett als Symbol verstanden werden soll. Das geht so:
joins(:employees).where(:"employees.name" => name)
Einfach, wenn gewusst wie!
Ü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:
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 Struktur der Tabellen ist also:
wobei der Name der Tabelle lime_survey_* von contacts.fragebogen_id abhängt. Deutlicher wird dieser Aspekt in der Datenansicht:
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:
limesurvey: adapter: mysql2 encoding: utf8 username: [DBUSER] password: [DBPASSWORD] database: limesurvey
Für die Tabellen lime_survey_* wird zudem ein Modell namens LimesurveyData erstellt:
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
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:
class Contact < ActiveRecord::Base ... has_one :limesurvey_data, :class_name => "LimesurveyData", :primary_key => "token", :foreign_key => "token" ... end
Der table_name_prefix muss in der Contact-Klasse nicht gesetzt werden, da standardmäßig die richtige Datenbank verwendet wird.
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:
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
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.
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?
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.
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.
Das Rooten des Samsung Galaxy Ace GT-S5830i ist überraschend einfach:

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!
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.
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
Die einzelnen Schritte sind:
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.
$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();
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:
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 '<div id="'.$name.'" style="border: 1px solid black; height: '.$height.'px; width: '.$width.'px; margin: 0 auto"></div>';
}
}
Im Headbereich der HTML-Seite wird über folgenden JS-Code das SVG der generierten Diagramme per Ajax an die vorher beschriebene svgsaver.php gesendet:
$(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.");
}
}
});
}
});
Alle Highcharts-Diagramme müssen folglich in dem Array charts enthalten sein, weswegen das Highcharts.Chart-Objekt in dem charts-Array gespeichert wird:
var charts = Array();
$(function () {
$(document).ready(function() {
var i = 0;
charts[i++] = new Highcharts.Chart({
chart: {
renderTo: 'container_1',
type: 'line',
...
Das sollte bei der Lösung der größten Hürden helfen, denn noch detaillierter möchte ich hier nicht werden.
Dieser Lösungsansatz zur Verwendung von Highcharts mit PrinceXML ist sicherlich nicht der Weisheit letzter Schluss:
aber es funktioniert! Und wie sagte George S. Patton?
Ein guter Plan heute ist besser als ein perfekter Plan morgen.