Schlagwort-Archive: Klasse

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.

Java: Erstellen großer CSV-Dateien zum Datenbank-Import

Anstatt Millionen von INSERT oder UPDATE-Befehlen an die Datenbank zu schicken, um eine Tabelle zu aktualisieren, bietet es sich bei großen Datenmengen an, eine CSV-Datei zu erstellen, welche dann über LOAD DATA INFILE mit extrem hoher Geschwindigkeit eingelesen wird.

Erstellen von CSV-Dateien mit Java

In Java bietet sich zum Erstellen von CSV-Dateien die Klasse Super CSV an. Um eine Datei zu erzeugen, benötigt man als erstes einen CSVMapWriter:
[code lang=’java‘]CsvMapWriter writer = new CsvMapWriter(new OutputStreamWriter(new FileOutputStream(„C:\meineDatei.csv“, true),“UTF-8″), CsvPreference.EXCEL_PREFERENCE);[/code]
Anstatt mit einem OutputStreamWriter zu arbeiten, kann man auch einen FileWriter verwenden. Da man bei letzterem aber kein Encoding („UTF-8“) angeben kann und in der Datenbank die Zeichen UTF-8-kodiert sind, müssen wir den Stream verwenden.
Interessant ist der zweite Parameter des FileOutputStream, der angibt, dass die Daten, die eingefügt werden, angehängt werden und nicht vorher gelöscht werden.
Die Einstellung CsvPreference.EXCEL_PREFERENCE legt fest, welche Trennzeichen in der CSV-Datei verwendet werden – wenn man andere Trennzeichen verwenden möchte, muss der später behandelte LOAD DATA INFILE-Befehl angepasst werden.

Als nächstes werden die Textfelder für die CSV-Datei definiert:
[code lang=’java‘]final String[] header = new String[] { „id“, „name“, „adressfeld“ };[/code]
Unsere Datensätze bestehen also aus 3 Feldern.

Um die CSV-Datei mit Daten zu füllen sind folgende Zeilen notwendig:
[code lang=’java‘]final HashMap data = new HashMap();
data.put(header[0], this.id);
data.put(header[1], this.name);
this.adressfeld = this.adressfeld.replace(„\\“, „\\\\“);
data.put(header[2], this.adressfeld);[/code]
Die Daten werden in einer HashMap gespeichert und per data.put eingefügt. Was mich etwas überraschte war, dass man das Backslash maskieren muss, ansonsten geht es in der Verarbeitung verloren. Eigenartig, weil man diese Funktionalität sicherlich problemlos in Super CSV hätte integrieren können.

Abschließend werden die Daten dem CSVMapWriter übergeben und dieser wieder geschlossen:
[code lang=’java‘]writer.write(data, header);
writer.close();[/code]

Und mehrere Datensätze?

Obiges Beispiel ist für einen einzelnen Datensatz, der in einer CSV-Datei gespeichert wird. Wenn man mehrere Datensätze einfügen möchte, müssen sämtliche Schritte (Anlegen eines CSVMapWriter, Daten vorbereiten, Daten einfügen) für jeden Datensatz wiederholt werden.

Den CSVMapWriter 1x anzulegen und am Ende per .close zu schließen, funktioniert nicht – meine Versuche mit der Super CSV-Klasse haben gezeigt, dass dann Datensätze verloren gehen bzw. abgeschnitten werden.

Einlesen der CSV mit LOAD DATA INFILE

Um die so erstellte Datei mit LOAD DATA INFILE in die Datenbank zu importieren, muss folgender Befehl eingetragen werden:
[code lang=“sql“]LOAD DATA [LOCAL] INFILE ‚C:\\meineDatei.csv‘
[REPLACE]
INTO TABLE meine_tabelle
FIELDS TERMINATED BY ‚,‘
OPTIONALLY ENCLOSED BY ‚\“‚
LINES TERMINATED BY ‚\n‘
(id, name, adressfeld);[/code]

Vorab: Der Befehl wird so nicht funktionieren, da die Bestandteile innerhalb der eckigen Klammern optional sind:

  • LOCAL wird dann angegeben, wenn die CSV-Datei vom Clientprogramm auf dem Clienthost gelesen und an den MySQL-Server geschickt wird. Fehlt LOCAL, liegt die Datei auf dem MySQL-Server
  • REPLACE nutzt man, wenn die eingelesene Datei nicht nur neue Datensätze importiert, sondern auch bereits vorhandene überschreibt – also UPDATES durchführt

Alle/Weitere Optionen zu dem LOAD DATA INFILE-Befehl gibt es im MySQL-Handbuch.

Performancevergleich

Ein Performancevergleich ist hier zu finden.

Java: Falscher ResultSet.getter verursacht Memory Leak

Ich bin ein Gänseblümchen im Sonnenschein
und durch meine Blüte fließt die Sonne in mich rein
Ich bin ein Gänseblümchen und mir wird ganz warm
ich könnt die ganze Welt und dann mich selbst umarmen.
Ich bin ein Gänseblümchen ohne Aggression
Wut, Ärger – was bringt das schon?

JavaDurchatmen, Freakcommander! Momentan gehst du durch die harte Schule einer dir immer noch relativ unbekannten Sprache. Da fällt man mal hin und muss wieder aufstehen, ansonsten wird das nichts.

Die Vorgeschichte

Aber man muss zugegeben, dass der heutige Fehler nicht ganz so leicht zu finden war:

  • Ich habe die Klasse Host umgeschrieben. Vorher hat sie einfach per Methode .toDB() ihre Attribute in die Datenbank geschrieben. Nach dem Umschreiben soll sich die Klasse nur dann in die Datenbank schreiben, wenn sie entweder noch nicht in der DB ist oder ein Attribut sich verändert hat und die DB somit aktualisiert werden muss.
    Grund für das Umschreiben: Innerhalb eines Programmaufrufs werden hintereinander Millionen Instanzen erzeugt und geguckt, ob sie sich verändert haben. Wenn das der Fall ist, muss der Eintrag auch in der DB aktualisiert werden. Da aber UPDATE oder INSERT-Befehle teurer sind und nur ein sehr kleiner Teil der Millionen Einträge sich auch wirklich verändern, macht eine Überprüfung, ob ein UPDATE oder INSERT erfolgen muss, definitiv Sinn.
  • Wie bin ich vorgegangen?
    1. Eine Methode .getFromDB, die die „alten“ Daten aus der DB liest
    2. Eine Methode .equals, die eine Instanz des Objekts mit einer anderen Instanz vergleicht und TRUE zurück gibt, wenn die Attribute gleich sind
    3. Die alte Methode .toDB() dementsprechend angepasst. Einziges Manko war, dass ich in dieser Methode eine Instanz des eigenen Objektes anlegen musste – diese angelegte Instanz des eigenen Objekts bekommt mittels .getFromDB() die alten Daten verabreicht und wird dann mit .equals verglichen.
  • Das Anpassen des Quellcodes war schnell erledigt. Das große Problem war nun allerdings, dass die Anwendung jetzt ein Speicherleck hatte. Der benutzte Speicher wuchs und wuchs und endete schließlich in einer Exception.
  • Anstatt mich direkt mit dem Code zu beschäftigen und durch auskommentieren und System.out.println-Meldungen dem Problem auf die Schliche zu kommen, beschäftigte ich mich mit der Literaturrecherche zu Memory Leaks, Speicherlecks, Garbage Collector und Eclipse Memory Analyzern oder VisualVM, einem Tool, das eine Realtime Heap-Usage Analyse verspricht.
  • Beim Eclipse Memory Analyzer funktionierte das Generieren der .hprof-Datei nicht, die man braucht, um den Analyzer einzusetzen.
  • Und die VisualVM ist sicherlich ein tolles Tool, aber mein uralt Notebook, welches zu wenig RAM und CPU-Leistung hat, arbeitet so langsam mit der VisualVM, dass beim Start schon der Java-Prozess mit meiner Anwendung, die den Memory Leak hat, verschwunden war. So konnte ich leider keinen Memory-Monitorer einschalten oder den Heap dumpen.. No chance.
  • Und nach stundenlanger Recherche und antesten etlicher Tools, um einem Speicherleck in Java zu begegnen, bekam ich schon wieder dieses Gefühl: Wie kann es sein, dass eine Programmiersprache, die millionenfach eingesetzt wird, kein einfach zu installierendes Tool hat, welches automatisch die Daten sammelt, um einem dann vorzuschlagen, welche Klassen oder Methoden man sich nochmal genauer anschauen sollte, um das Speicherleck zu schließen. Wenn in Foren ein Programmierer Probleme mit einem Tool hat, wird ihm einfach ein anderes Tool empfohlen, welches ähnlich ist und bei ihm laufen sollte. Na ja, ich hatte keinen Bock mehr mich durch irgendwelche Seiten und Tools zu klicken.
  • Das Problem war an irgendeiner Stelle in meinem Code, also finde ich einfach die Stelle und behebe das Problem.
  • Nachdem ich sichergestellt hatte, dass sämtliche Statements, PreparedStatements, ResultSets und Connections nach getaner Arbeit auch wieder geschlossen wurden und das Problem nach wie vor auftrat, kümmerte ich mich um die Instanz, die innerhalb der .toDB()-Methode erzeugt wird. Vielleicht werden ganz viele Instanzen erzeugt, die der Garbage Collector aus irgendwelchen Gründen nicht löschen kann. Aber auch daran lag es nicht.
  • Schließlich ging ich dazu über ganze Funktionen und Programmteile auszukommentieren. Folgender Programmcode verursachte das Speicherleck

Der Fehler

In der Methode .getFromDB(), die einfach nur per DB-Query Daten einliest, gab es folgende 3 Zeilen Code, die Grund für das Memory Leak waren:

[code lang=’java‘]this.setM_swap(rs.getInt(16));
this.setD_total(rs.getInt(17));
this.setD_free(rs.getInt(18));[/code]

Aus dem ResultSet der DB-Abfrage werden per Setter-Methoden die Attribute gesetzt. Das Problem? Da die Java-Datentypen und die DB-Felder für m_swap, d_total und d_free mit Integer bzw. int nicht ausreichend lang waren, habe ich sie nachträglich auf long bzw. Bigint (DB) geändert. Dabei habe ich auch die setter wie setM_swap() so geändert, dass sie als Parameter long erwarten.  Normalerweise schreit ja gleich das IDE los, wenn ein Typ nicht passt, aber Java findet (und in diesem Fall völlig zu Recht), dass Methoden, die long als Parameter erwarten, auch mit int gefüttert werden können und hat deswegen nicht gemeckert.

Das Memory Leak kann ich mir nur so erklären, dass mit rs.getInt auf eine Spalte zugegriffen wurde, welche in der Datenbank als BigInteger definiert ist.

Richtigerweise hätte man mit rs.getLong() arbeiten müssen, dann gibt es auch keine Probleme mit dem Speicher. ;)