Bei in Java bei XML ist die bekannteste Lösung manchmal nicht die Lösung, die man gerade braucht. Groß, komplex und kann alles. Dependencies machen dann aber Probleme und wenn man nur eine Datei schnell und einfach lesen möchte, braucht man nicht irgend eine HTML-Lib, die nur in ganz bestimmten Fällen nötig wäre.
Beim Lesen von Excel-Dateien in PHP ist es genau so. HTML-Lib machte Probleme beim Installieren über Composer, aber ich will ga rkeine HTML-Sachen damit machen. Cool das es gehen würde, aber ich will nur schnell und einfach die Daten der Tabelle auslesen. CSV hätte ja gereicht, aber es kommt eben eine Excel-Datei.
Daten per Criteria abfragen und in einer Table oder einer Form darzustellen oder verarbeiten ist in der Shopware 6 Administration extrem einfach und fast zu 100% genau so wie in PHP. Aber manchmal will man auch auf eine sehr spezielle Art oder Weise Daten erhalten und diese dem Benutzer direkt als Download anbieten können.
Hier ein kleines Beispiel eines CSV-Downloads der von einem API-Controller mit der Route "/api/example-module/download/csv/{id}" kommt. Man kann aber genau so gut alles per Criteria abfragen und das CSV in JavaScript bauen.
Die Methode für den Download sieht mit Controller-Lösung so aus:
Das Prinzip ist an sich sehr einfach. Man baut sich ein a-Element mit gesetzten download-Attribute und simuliert einen Click darauf. Nur hinterlegt man hier die Daten als Data-URL und nicht die URL zum Controller direkt. Das würde nicht funktionieren als dann dort die Authorization-Infos im GET-Header fehlen würden.
Die Data-URL kann eben aus Daten von einem Controller gebaut werden oder direkt aus Daten, die JavaScript aggregiert wurden.
Sehr viel einfacher als die alten Lösungen mit FileSaver-Lib und viel hin und her wandeln von Blobs.
Ich hatte das Problem, dass egal was ich versucht habe den Daten einer CSV sich nicht nach der Spalte mit dem Datum sortieren lassen wollten. Ich hab versucht die Spalte als Datum zu formatieren, aber das hat nicht geholfen. Am Ende war die Lösung:
Die Datum-Daten markieren. Suchen und Ersetzen aufrufen.
Suchen nach:
^.*$
Ersetzen durch:
$
und noch auswählen, dass nur im markierten Bereich gesucht werden soll und natürlich dass das ein RegEx-Ausdruck ist.
Falls man mal CSV-Daten per CURL irgendwo hin schicken will und es funktioniert einfach nicht, wie gedacht.. dann sollte man überprüfen, ob man nicht --data-binary vergessen oder durch -d ausgetauscht hat und somit alle Zeilenumbrüche verloren gegangen sind.
Da zuerst zu gucken, dann einen viel Zeit bei der Fehlersuche ersparen.
Am 30.6. war wieder Oldtimer Really in Bremen. Diesmal hatte ich frühzeitig zwei Thinkpad T500 organisiert, falls es Probleme bei der Bereitstellung anderer Notebooks geben sollte. Die Software hatte ich seit dem letzten Jahr nicht mehr groß angefasst. Nur die aktuelle Version der Client-JAR zu finden brauchte etwas. Aber nachdem ich die ganz unverhofft im Projekt-Verzeichnis fand, lief dann alles ohne wieder ohne Probleme.
Wieder 5:00 los und.. KEIN REGEN!!!!!! Auch eine weitere Besonderheit kam hinzu. Das 2. Zeitfahren wurde mit einem anderen System gemessen und später sollten die Ergebnisse beider Systeme für das Gesamtergebnis kombiniert werden. Ein kleines PHP-Script lass alle Renn-Ergebnisse als CSV ein und wertete diese aus. Alle Fahrer die nicht alle 3. Zeitfahrten absolviert hatten.
Nach dem Tag hab ich mir vorgenommen, die Integration mit anderen Systemen zu verbessern. Auch die Clients könnten dann ihre Auswertungen nicht nur per REST-API übermitteln, sondern auch eine CSV generieren, die dann ausgelesen werden kann. Der Import wäre dann so flexibel, dass er auch CSV-Dateien anderer Systeme importieren könnte.
Ein anstrengender Tag, aber die Messungen liefen wirklich gut und es gab kaum Fehlmessungen, durch Fahrtzeuge die zu dicht am Messbereich parkten.
Wer also bei Rennen oder ähnlichen jemanden braucht, der die Zeitmessungen und Auswertungen übernimmt, kann sich vertrauensvoll an die Switch GmbH wenden... auch was Temperaturüberwachung angeht.
Aufbauen
Surface als Messstation, der Client läuft noch in Eclipse
Ich hatte es schon fertig und dachte ich könnte es später auch mal im Community Store anbieten.. aber dann kam mir schon jemand zu vor und dann auch noch kosten los.
Also hier einfach mein Plugin als Code für jeden:
<?php
namespace HPrExportAttrExtend;
use Doctrine\DBAL\Connection;
use Shopware\Components\Plugin;
Wer sich mit CSV-Importen in Neo4j 3.0 versucht wird schnell merken, dass die angegebene File-URL sich irgendwie immer auf das Neo4j Verzeichnis bezieht auch wenn man sich auf das Root Verzeichnis bezieht. Der Fehler liegt nicht an der File-URL, sondern an einer Einstellung in Neo4j, wo als default Verzeichnis das eigene import-Verzeichnis gewählt ist und die File-URL sich nur auf Dateien innerhalb dieses Verzeichnisses bezieht.
Man kann einfach in der conf/neo4j.conf das import Verzeichnis von import auf / (Linux) ändern und es verhält sich wieder wie in der 2.x Version.
Oft werden NoSQL für sehr spezielle Fälle eingesetzt. Die normale Datenhaltung bleibt weiter hin den SQL-Datenbanken überlassen. Also müssen regelmäßig die Daten aus dem SQL-Bestand in die NoSQL Datenbank kopiert werden. Das dauert oft und viele aufbereitungen der Daten wird schon hier erledigt. die NoSQL Varianten sind deswegen auch oft schneller, weil man eine Teil der Arbeit in den Import-Jobs erledigt, die sonst bei jedem Query als Overhead entstehen. Natürlich haben die NoSQL auch ohne das ihre Vorteile, aber man sollte immer im Auge behalten, ob die Performance von der Engine kommt oder auch von der Optimierung der Daten, weil die Optimierungen der Daten könnte man auch in die SQL-Struktur zurück fließen lassen und diese in die Richtung hin verbessern.
So ein Import dauert... wenn man in der Nacht ein Zeitfenster von einer Stunde hat, ist alles kein Problem. Will man aber auch in kurzen Abständen importieren, muss der Import schnell laufen. Auch wenn man als Entwickler öfters mal den Import braucht, ist es wichtig möglichst viel Performance zu haben.
Hier geht es darum wie man möglichst schnell und einfach Daten aus einer MySQL Datenbank in eine Neo4j Graphen-Datenbank importieren kann, ohne viel Overhead zu erzeugen. Ich verwende hier PHP, aber da an sich keine Logik in PHP implementiert werden wird, kann man ganz leicht auf jeden andere Sprache, wie Java, JavaScript mit node.js und so übertragen. Es werden keine ORMs verwendet (die extrem viel Overhead erzeugen und viel Performance kosten) sondern nur SQL und Cypher.
Wie man einfach sich eine oder mehrere Neo4J-Instanzen anlegt (unter Linux) kann man hier sehr gut sehen:
Wir verwenden bei Neo4j den Import über eine CSV-Datei. Wir werden also nicht jeden Datensatz einzeln Lesen und Schreiben, sondern immer sehr viele auf einmal. Ob man alles in einer Transaktion laufen lässt und erst am Ende commited hängt etwas von der Datenmenge ab. Bis 200.000 Nodes und Relations ist alles kein Problem.. bei Millionen von Datensätzen sollte man aber nochmal drüber nachdenken.
PERIODIC COMMIT ist da eine super Lösung, um alles automatisch laufen zu lassen und sich nicht selbst darum kümmern zu müssen, wann commited wird. Alle 1000 bis 10_000 Datensätze ein Commit sollte gut sein, wobei ich eher zu 10_000 raten würde, weil 1000 doch noch sehr viele Commits sind und so mit der Overhead noch relativ groß ist.
Unsere Beispiel Datenbank sieht so aus:
CREATE TABLE USERS(
USER_ID INT(11) UNSGINED NOT NULL,
USER_NAME VARCHAR(255) NOT NULL,
PRIMARY KEY (USER_ID)
);
CREATE TABLE MESSAGES(
MESSAGE_ID INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
MESSAGE_TITLE VARCHAR(255) NOT NULL,
FROM_ID INT(11) UNSIGNED NOT NULL,
TO_ID INT(11) UNSIGNED NOT NULL,
CC_ID INT(11) UNSIGNED NOT NULL,
PRIMARY KEY (MESSAGE_ID)
);
Wir legen uns 50.000 User an dann noch 100.000 Messages mit jeweils einen FROM, einem TO und einem CC (hier hätte man über eine Link-Table sollen, aber das hier ist nur ein kleines Beispiel, wo das so reicht). Das sollten erst einmal genug Daten sein. (Offtopic: da ich das gerade neben bei auch in PHP schreibe.. warum kann ich für eine 100000 nicht wie in Java 100_000 schreiben?)
Die erste Schwierigkeit ist es die Daten schnell zu exportieren. Ziel ist eine CSV. Wir könnten entweder über PHP die Daten lesen und in eine Datei schreiben oder aber einfach die OUTFILE-Funktion von MySQL nutzen, um die Datenbank diese Arbeit erledigen zu lassen. Wir werden es so machen und erstellen für jede Art von Nodes und Relations eine eigene CSV. Weil wir Header haben wollen fügen wir diese mit UNION einmal oben hinzu
$sql="
SELECT 'user_id', 'user_name'
UNION
SELECT USER_ID,USERNAME
FROM USERS
INTO OUTFILE ".$exchangeFolder."/users.csv
FIELDS TERMINATED BY ','
ENCLOSED BY ''
LINES TERMINATED BY '\n'
";
Damit schreibt MySQL das Ergebnis des Queries in die angegebene Datei. Falls ein Fehler auftritt, muss man gucken, ob der Benutzer unter dem die MySQL-DB läuft in das Verzeichnis schreiben darf und ob nicht eine Anwendung wie apparmor unter Linux nicht den Zugriff blockiert. Es darf keine Datei mit diesen Namen schon vorhanden sein, sonst liefert MySQL auch nur einen Fehler zurück. Wir müssen
die Dateien also vorher löschen und dass machen wir einfach über PHP. Also muss auch der Benutzer unter dem die PHP-Anwendung läuft entsprechende Rechte haben.
Man kann das gut einmal direkt mit phpmyadmin oder einem entsprechenden Programm wie der MySQL Workbench testen. Wenn die Datei erzeugt und befüllt wird ist alles richtig eingestellt.
Mit dem Erstellen der CSV-Datei ist schon mal die Hälfte geschafft. Damit der Import auch schnell geht brauchen wir einen Index für unsere Nodes. Man kann einen Index schon anlegen, wenn noch gar kein Node des Types erstellt wurde. Zum Importieren der User benutzen wir folgendes Cypher-Statement:
$cyp="USING PERIODIC COMMIT 10000
LOAD CSV WITH HEADERS FROM "file:///".$exchangeFolder."/messages.csv" AS row
MERGE (m:message{mid:row.msg_id,title:row.msg_title});";
Der Pfad zur Datei wird als File-URL angegeben. Hier merkt man auch Neo4J seine Java-Basis an. Wenn man mal in eine Temp-Verzeichnis schaut sieht man dort auch Spuren von Jetty.
Am Ende wird der Importer nur eine Reihe von SQL und Cypher Statements ausführen. Wir benötigen um komfortabel zu arbeiten 3 Hilfsmethoden. Dass alles in richtige Klassen zu verpacken wäre natürlich besser, aber es reicht zum erklären erst einmal ein Funktionsbasierter Ansatz.
Da MySQL keine Dateien überschreiben will, brauchen wir eine Funktion zum Aufräumen des Verzeichnisses über das die CSV-Dateien ausgetauscht werden. Wir räumen einmal davor und einmal danach auf. Dann ist es kein Problem den Importer beim Testen mal mittendrin zu stoppen oder wenn er mal doch mit einem Fehler abbricht.
function cleanFolder($folder){
$files=scandir($folder);
foreach($files as $file){
if(preg_match("/\.csv$/i", $file)){
unlink($folder."/".$file);
}
}
}
Für Neo4J bauen wir uns eine eigen kleine Funktion.
use Everyman\Neo4j\Client;
use Everyman\Neo4j\Cypher\Query;
$client = new Everyman\Neo4j\Client();
$client->getTransport()->setAuth("neo4j","blubb");
function executeCypher($query){
global $client;
$query=new Query($client, $query);
$query->getResultSet();
}
Der Rest ist nun sehr einfach und linear. Ich glaube ich muss da nicht viel erklären und jeder Erkennt sehr schnell wie alles abläuft. Interessant ist wohl das Cypher-Statement für die Receive-Relations, da neben der Relation diese auch mit einem Attribute versehen wird im SET Bereich.
//clear for export (if a previous import failed)
cleanFolder($exchangeFolder);
//export nodes
echo "create users.csv\n";
$sql=" SELECT 'user_id', 'user_name' UNION
SELECT USER_ID,USER_NAME
FROM USERS
INTO OUTFILE '".$exchangeFolder."/users.csv'
FIELDS TERMINATED BY ','
ENCLOSED BY ''
LINES TERMINATED BY '\n'";
$db->execute($sql);
echo "create messages.csv\n";
$sql=" SELECT 'msg_id', 'msg_title' UNION
SELECT MESSAGE_ID, MESSAGE_TITLE
FROM MESSAGES
INTO OUTFILE '".$exchangeFolder."/messages.csv'
FIELDS TERMINATED BY ','
ENCLOSED BY ''
LINES TERMINATED BY '\n'";
$db->execute($sql);
//export relations
echo "create relations_etc.csv\n";
$sql=" SELECT 'user_id', 'msg_id', 'type' UNION
SELECT TO_ID, MESSAGE_ID, 'TO'
FROM MESSAGES
UNION
SELECT CC_ID, MESSAGE_ID, 'CC'
FROM MESSAGES
INTO OUTFILE '".$exchangeFolder."/relations_etc.csv'
FIELDS TERMINATED BY ','
ENCLOSED BY ''
LINES TERMINATED BY '\n'";
$db->execute($sql);
echo "create relations_from.csv\n";
$sql=" SELECT 'user_id', 'msg_id', 'type' UNION
SELECT FROM_ID, MESSAGE_ID, 'FROM'
FROM MESSAGES
INTO OUTFILE '".$exchangeFolder."/relations_from.csv'
FIELDS TERMINATED BY ','
ENCLOSED BY ''
LINES TERMINATED BY '\n'";
$db->execute($sql);
//create indexes for fast import
echo "create index's in neo4j\n";
$cyp="CREATE INDEX ON :user(uid);";
executeCypher($cyp);
$cyp="CREATE INDEX ON :message(mid);";
executeCypher($cyp);
//import nodes
echo "import users.csv\n";
$cyp="USING PERIODIC COMMIT 10000\n
LOAD CSV WITH HEADERS FROM "file:///".$exchangeFolder."/users.csv" AS row\n
MERGE (u:user{uid:row.user_id,name:row.user_name});";
executeCypher($cyp);
echo "import messages.csv\n";
$cyp="USING PERIODIC COMMIT 10000
LOAD CSV WITH HEADERS FROM "file:///".$exchangeFolder."/messages.csv" AS row
MERGE (m:message{mid:row.msg_id,title:row.msg_title});";
executeCypher($cyp);
//import relations
echo "import relations_from.csv\n";
$cyp="USING PERIODIC COMMIT 10000
LOAD CSV WITH HEADERS FROM "file:///".$exchangeFolder."/relations_from.csv" AS row
MATCH(u:user{uid:row.user_id})
MATCH(m:message{mid:row.msg_id})
MERGE (u)-[r:send]->(m);";
executeCypher($cyp);
echo "import relations_etc.csv\n";
$cyp="USING PERIODIC COMMIT 10000
LOAD CSV WITH HEADERS FROM "file:///".$exchangeFolder."/relations_etc.csv" AS row
MATCH(u:user{uid:row.user_id})
MATCH(m:message{mid:row.msg_id})
MERGE (m)-[r:receive]->(u)
SET r.type=row.type;";
executeCypher($cyp);
//clear after import
cleanFolder($exchangeFolder);
Hier sieht man wie der Importer die 50.000 User, 100.000 Messages und insgesamt 300.000 Relations von einer MySQL in die Neo4J Instanz importiert.
Die Festplatte ist nur über SATA-2 Angeschlossen und nicht besonders schnell. Eine SSD, wie für Neo4J empfohlen, würde alles sehr beschleunigen.
Zum Löschen aller Daten aus der Neo4J kann man diese Statement verwenden:
Auch heute in Zeiten von JSON und XML ist einer der Haupt Import- und Export-Formate immer noch CSV. Der Vorteil ist eben, dass es sich einfach erstellen lässt, einfach einlesen und zur Kontrolle in einem Texteditor laden lässt. Excel kann es auch irgendwie und OpenOffice bzw LibreOffice kann super damit umgehen.
Meistens erstellt man die Dateien ja in dem man Daten aus der Datenbank lädt und dann das Resultset durchläuft und direkt ausgibt oder in einen String schreibt, den man in eine Datei schreibt. Der Vorteil ist, dass man die Daten noch mal bearbeiten kann. Aber wenn man nicht zu komplexe Bearbeitungen vornehmen will und man diese auch mit SQL erledigen kann, gibt es auch die Möglichkeit CSV-Dateien direkt in der Datenbank (hier MySQL) zu erstellen.
Gerade wenn man die Datei nicht ausgeben will über eine PHP Datei sondern sie direkt in einer Verzeischnis kopiert (auch über FTP oder SSH) und die Datei dann von dort von einem anderen System eingelesen wird (z.B. von Neo4J.. so bin ich darauf gekommen) ist dieses Vorgehen sehr viel performanter (gerade wenn OR-Mapper im Spiel sind) als das normale Vorgehen.
SELECT ID,NAME
FROM TEST
WHERE ID>5
INTO OUTFILE '/var/www/app/data/export/out_test.csv'
FIELDS TERMINATED BY ','
Wenn man das FILE-Right hat, kann man so eine CSV in ein beliebiges Verzeichnis schreiben.
Man muss nur sicher stellen, dass man die nötigen Rechte im Verzeichnis hat und dass keine Programme wie apparmor das Schreiben verhindern. ERRCODE 13 wäre der Fehlercode der in so einem Fall angezeigt werden würde.
Wenn man nun alle Rechte erteilt hat und es nicht geht und apparmor läuft, kann man kontrollieren, ob MySQL davon überwacht wird.
sudo aa-status
Um den Zugriff auf das Verzeichnis zu erlauben muss man folgendes tun. Man muss /etc/apparmor.d/usr.sbin.mysql.d bearbeiten und einfach den Pfad an die vorhanden anfügen. Dann mit noch mal apparmor neu laden und es sollte gehen.
sudo /etc/init.d/apparmor reload
Blog-entries by search-pattern/Tags:
Möchtest Du AdSense-Werbung erlauben und mir damit helfen die laufenden Kosten des Blogs tragen zu können?