MySQL Dump bei Shopware haben manchmal das Problem, dass DEFINER und Datenbanknamen an Triggers mit exportiert werden, die nicht zur neuen Datenbank passen, wenn die Datenbank anders heißt und man einen anderen Benutzernamen hat.
Man kann dann mit einem Texteditor wie nano, vi oder Notepad++ die Datei durchsuchen und es per Hand fixen. Nur doof wenn die Datei 6GB groß ist und keiner der Editoren mehr so richtig performant mit der Datei arbeiten will.
Dafür gibt es dann in der Linux Kommandozeile sed:
mysqldump -u demouser -p demo_webshop > ./dump_for_65_update.sql
sed 's/`demo_webshop`.//g' dump_for_65_update.sql > dump_for_65_update_clean.sql
sed -i 's|/\*!50017 DEFINER=`demouser`@`localhost`\*/||g' dump_for_65_update_clean.sql
mysql --database=demo65_webshop --user=demouser65 -p < ./dump_for_65_update_clean.sql
Man kann da sicher noch allgemeine Ausdrücke für schreiben, aber das lag dieses mal außerhalb dem was der Kunde bezahlt hätte.
SELECT
table_schema as `Database`,
table_name AS `Table`,
round(((data_length + index_length) / 1024 / 1024), 2) `Size in MB`
FROM information_schema.TABLES
ORDER BY (data_length + index_length) DESC;
Wichtig ist sich aber im Klaren zu sein, dass diese Größen sich rein auf die Daten in der DB beziehen und durch Meta-Daten und Query-Logs die Gesamtgröße auf der Festplatte größer sein kein.
Ich musste mir eine sw2-Datenbank anlegen über root/root, aber an sich sollte es wie in meinem SW5-Environment auf mit der sw-Datenbank und sw/sw gehen.
Viele Probleme lassen sich nicht durch einfache SQL-Queries lösen. Gerade wenn es um Daten-Generierung und Dinge geht, wo man die Logik nicht in allen Clients umsetzen möchte, die auch die Datenbank zugreifen.
Wärend Stored Procedures coole Dinge wie IF, WHILE und so können, haben die doch oft einen schlechten Ruf, weil man dann ja nicht mehr Datenbank unabhängig wäre, wenn man anfängt diese zu verwenden. Aber am Ende wird man sowie so nie wirklich die Datenbank wechseln und sollte man das tun, dann eher von SQL zu NoSQL und da ist es dann auch egal, da man alles alle neu anpassen muss.
Eine einfache Procdure in MySQL:
DELIMITER \\
DROP PROCEDURE IF EXISTS exampleProcedure\\
CREATE PROCEDURE exampleProcedure()
BEGIN
DECLARE checkval INT;
SET expectedval = 0;
START TRANSACTION;
-- implement your logic here
IF checkval = expectedval THEN
COMMIT;
ELSE
ROLLBACK;
END IF;
END \\
DELIMITER ;
CALL exampleProcedure();
DROP PROCEDURE IF EXISTS exampleProcedure;
Es gibt auch die klassischen IN und OUT Parameter. Alles ganz einfach und schnell zu schreiben. Sieht ein wenig aus wie Pascal. ABER im Vergleich zu PL/SQL fehlen doch eine Datentypen wie RECORDs, um komplexere Datenstrukturen anlegen zu können oder direkter mit Tables arbeiten zu können. SELECT .. INTO ist nett, aber RECORDs machen es doch sehr viel einfacher und strukturierter.
Aber an sich kommt man damit zurecht und man sollte doch mehr davon und auch öfters Stored Procedures verwenden, um sich in einigen Teilen das Leben etwas leichter zu machen.
Ich habe PL/SQL immer gerne verwendet, auch wenn ich oft genug daran verzweifelt bin.
Die Differenz zwischen zwei Datumswerten in Tagen:
Java (LocalDateTime)
if(ldt1.isAfter(ldt2)){
days = Duration.between(ldt1.toLocalDate().atStartOfDay(), ldt2.toLocalDate().atStartOfDay()).toDays();
days = Math.abs(days);
}
Java (Date)
long days = TimeUnit.DAYS.convert(date.getTime() - date2.getTime(), TimeUnit.MILLISECONDS);
Um unseren Shopware-Server aufzusetzen brauchen wir
- Apache (mit php7.0-mod)
- PHP7 (common,mysql,gd,xml,json,xsl,intl,gettext,mbstring,zip)
- MySQL Server
- OpenSSH Server
- PHPMyAdmin (wichtig hier Apache2 auszuwählen.. also mit Sternchen sichtbar nicht nur makiert)
Dann folgt die conf für den Apache und der Eintrag in die Host-Datei.
Jetzt können wir Shopware downloaden und installieren
Nun folgt die Netzwerk Konfiguration mit VirtualBox und die Anpassungen der hosts-Datei unter Windows.
Am Ende können wir mit dem Browser auf Shopware und mit WinSCP auf das System und das Shopware-Verzeichnis zugreifen.
So können wir unser lokalen Plugin-Verzeichnis mit dem auf dem Server synchron halten und Änderungen werden automatisch auf den Server gepusht.
Wenn man in Shopware den Cache geleert hat und beim Anlegen der Keywords im Frontend danach eine Exception auftritt, die in etwas so "General error: 1436 Thread stack overrun" lautet, muss man die MySQL-DB anpassen.
Dafür muss man in der my.conf den Wert für den thread_stack erhöhen.
Wenn man alte Software laufen hat, die dann hoffentlich schon PDO nutzt aber nicht jedes SQL-Statement
testen kann, kann man MySQL 5.7 auch dazu bringen sich wie eine alte 5.5 Version zu verhalten. Das kann nötig werden wenn man von Ubuntu 14.04 LTS auf 16.04 LTS upgradet und damit eine aktuellere MySQL-Version installiert wird, die sich meiner Meinung nach sehr viel korrekter mit Werten und Typen verhält als die alte Version.
Wenn man langem ist Oracle gearbeitet hat würde man auch nie auf die Idee kommen für einen Number/Int-Wert '1' an stelle von 1 zu übergeben.
Nachdem ich im letzten halben Jahr mit Neo4j und nun auch mit Elasticsearch zu tun hatte, bin ich was NoSQL-Datenbanken angeht etwas zwiegespalten. Graphen-Datenbanken sind toll um Beziehungen zwischen Entitäten abzubilden. Dokumenten-orientierte Datenbanken wie Elasticsearch ideal um unstrukturierte Daten zu speichern und neben der eigentlichen Abfrage auch z.B. Durchschnittswerte oder Übersichten von der Abdeckung von bestimmten Attributen/Feldern gleich mit abzufragen.
Die Abfragen sind schnell. Aber.. auch sind die Queries komplexer (Neo4j) bis sehr viel komplexer (Elasticsearch). Der Vorteil der NoSQL Datenbanken ist, dass man schon fertige Objekte zurück bekommt und man nicht auf Tabellenstrukturen beschränkt ist. So kann man also Listen mit Objekten die zu einer Entität gehören gleich mit abfragen und erspart sich ein zweites Query und zusätzliches Mapping.
Aber sind die NoSQL Datenbanken wirklich so viel schneller, wie man immer hört? Dafür muss man verschiedene Dinge bedenken. Zuerst ob die Datenbank die primäre Datenquelle ist oder nur zusätzlich zu einem RDBMS verwendet wird. Ich hatte bis jetzt nur mit zusätzlichen Datenbanken zu tun. Deren Daten wurden durch Cronjobs aus dem RDBMS gelesen, aufbereitet und dann in die NoSQL-Datenbank geschrieben.
Entweder per CSV-Import (Neo4J) oder direkt über die REST-API (Elasticsearch). Die gleichen Abfragen waren in der MySQL-Datenbank langsamer. Nicht viel langsamer. Aber es war doch spürbar und lagen bei der Neo4J bei so 30%-40%.
Wenn man nun aber einberechnet wie viel Aufwand der Import darstellt, der bei beiden Anwendungsfällen zwischen 1,5-5min lag, sieht es schon sehr viel anders aus. Die Importscripte reduzierten die Datenmenge natürlich sehr extrem und schrieben nur die nötigsten Daten in die NoSQL-Datenbanken. Bei der Neo4J waren es wirklich nur Ids und Relationen. Die Elasticsearch hatte alle elementaren Felder und auch Unterobjekte, die bei SQL über Joins geladen werden würden. Auf dieser reduzierten und stark vereinfachten Datenbasis waren die Abfragen sehr schnell.
Wenn man mit ein paar SQL-Statements die selben Daten in der MySQL in dem selben Umfang in eigene Tabellen schreibt, ist die MySQL Datenbank meiner Erfahrung nach genau so schnell. Im Vergleich von MySQL und Neo4J muss man sagen, dass für die Abfragen plötzlich viel mehr Daten zur Verfügung stand und diese auf genutzt wurden. Außerdem wurden doppelt so viele Queries verwendet. Am Ende war die MySQL-Lösung langsamer aber auch sehr viel komplexer und in dem Sinne besser.
Ich für meinen Teil sehe in den NoSQL-Datenbank nur einen Vorteil, wenn man die Vorteile derer auch nutzt. Wenn ich keine Graphen brauche, brauche ich auch keine Neo4J-Datenbank. Habe ich nur Entitäten und DTOs die ich schnell speichern und laden möchte, brauche ich keine Elasticsearch. Elasticsearch ist komplex und kann ein paar wirklich interessante Dinge durch deren Aggregations. Wenn ich haufenweise unterschiedliche Daten aus vielen verschiedenen Quellen zusammen fahren möchte bin ich mit Elasticsearch gut beraten. Aber wenn ich nur Geschwindigkeit haben möchte muss ich nur die Datenbasis verringern und vereinfachen. Neo4J ist auch extrem Speicher hungrig. Was bringt es mir wenn ich 96GB an RAM brauche um das zu machen was ich mit 32GB und einer MySQL oder einer Oracle-DB genau so schnell hinbekomme. Wenn ich dann sehr viel RAM habe und ganze Datenbanken im Speicher halten kann, habe ich die selbe Geschwindigkeit und bin mit der größeren Datenbasis sehr viel flexibler. Außerdem ist alles schneller und sicherer was ich direkt innerhalb der Datenbank machen kann. Ein Import von der MySQL in die Neo4J brauchte viel darum herum um sicher zu sein. In einer Oracle würde alles sowie so in einer Transaction laufen, die auch nicht noch den Server der das Script startet belastet.
Wer also in seinem RDBMS Performanceprobleme hat, soll sie auch dort lösen und nicht glauben, dass ein weiteres System anzubinden (und synchron zu halten) dieses Probleme lösen würde.
Beim Umstieg von einer alten PHP Version auf PHP7.0 werden viele sicher gleich sich daran machen auch den ganzen Rest mal auf eine aktuelle Version zu migrieren. Dazu gehört dann oft auch eine aktuelle MySQL-Version.
Aber manchmal laufen dann ältere Queries nicht mehr. Das Beste ich dann natürlich das Query anzupassen. Wenn man aber nicht so viel Zeit hat oder sich erst später mit den Problem beschäftigen möchte, hilft es oft die problematischen neuen Modes zu deaktivieren.
Besipiel wäre hier der ONLY_FULL_GROUP_BY-Mode
SET sql_mode=(SELECT REPLACE(@@sql_mode,'ONLY_FULL_GROUP_BY',''));
Bei PDO kann man das Stament direkt als PDO::MYSQL_ATTR_INIT_COMMAND setzen, so dass es bei jeder neuen Verbindung gleich gesetzt wird.
Das ist jetzt nicht wirklich eine große Entdeckung und ist eigentlich klar, aber auch wenn in der Beschreibung von MySQL nur steht, dass einige Statements nicht mehr mit einem Rollback zurück genommen werden können, wenn man ein DDL-Statement nach einem start transaction; ausführt, bedeutet es einfach, dass die Transaction danach einfach commited wird und alles danach nicht in einer Transaction läuft.
Wenn man sich als bei größeren Imports oder Datenverarbeitungen eine Tabelle zum Auslagern von Daten erzeugt, sollte man das immer zuerst und außerhalb der Transaction machen. Auch wenn man mit PDO arbeitet, muss man darauf achten.
Sich extra Tabellen anlegen ist gut für Dinge, die länger laufen und besonders wenn sie länger laufen als die Timeout-Zeit von PHP oder des DB-Treibers. Den gerade MySQL locked alle Rows, die während einer Transaction geschrieben und auch gelesen werden. Wenn nun jemand in so eine Row schreiben möchte, die in einer langen Transaction auch nur gelesen wurde,kann es schnell zu Problemen kommen.
Erst einmal sollte man sowie so Stammdaten und Bewegungsdaten trennen. Damit minimiert sich das Problem schon mal etwas.
Am Ende kann man auch das Isolation-Level ändern, aber das sollte immer das letzte sein, was man versuchen sollte.
"Ich habe mit Oracle-DB gearbeitet und musste dort Konstrukte drin bauen, die einen 2 Tage Arbeitszeit und tausende Nerven gekostet haben und man 4 mal pro Stunde an sich selbst gezweifelt hat. Manchmal war einem am Ende nicht mal ganz klar, warum es nun wirklich doch funktionierte.
Jetzt arbeite ich mit MySQL/MariaDB... und wünsche mir oft die Oracle-DB zurück, weil ich damit diese Konstrukte überhaupt bauen konnte!"
Mein Kommentar dazu, wenn mal wieder über Oracle und deren doofe DB geweint wird.
Bevor es dann wirklich mal um die Installation von PDT in Eclipse geht, wollen wir uns erst einmal eine MySQL Datenbank einrichten, damit wir auch richtige kleine Anwendungen schreiben können und nur ganz selten kommt man da ohne Datenbank aus. Auch wenn NoSQL Datenbanken wie Neo4J wirklich toll und momentan sehr in sind, bleiben wir bei einem klassischen RDBMS. Weil MySQL gerade im Web sehr verbreitet ist und sich einfach lokal einrichten lässt, bleiben wir auch bei MySQL.
Wer ein Linux benutzt, kann MySQL immer ganz einfach über den Paket-Mananger installieren. Für Windows kann man schnell zu XAMPP greifen. XAMPP enthält alles vom Apache, PHP7 und MySQL bzw MariaDB.
XAMPP funktioniert am Besten wenn man es direkt unter C:\ installiert.
Zum Verwalten der Server-Anwendungen von XAMPP gibt es das XAMPP Control Panel. Hier müssen nur der Apache und der MySQL Server gestartet
werden.
Damit auf die Datenbank zugegriffen werden kann bringt XAMPP phpMyAdmin mit. phpMyAdmin ist eine Datenbankverwaltung die in PHP geschrieben ist und auch von den meisten Hostern angeboten wird.. wenn nicht sogar von allen. Man kann direkt über die URL http://localhost/phpmyadmin darauf zugreifen.
Zuerst erstellen wir eine Datenbank mit dem Namen blog_test.
Damit wir ein paar Daten haben, legen wir uns eine Tabelle mit ein paar wenigen Daten an. Hier ist das SQL-Script dafür:
CREATE TABLE TEST_ITEMS(
ITEM_ID INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
ITEM_VALUE VARCHAR(255) NOT NULL,
PRIMARY KEY(ITEM_ID)
);
INSERT INTO TEST_ITEMS (ITEM_VALUE) VALUES('TEST_01');
INSERT INTO TEST_ITEMS (ITEM_VALUE) VALUES('TEST_02');
INSERT INTO TEST_ITEMS (ITEM_VALUE) VALUES('TEST_03');
INSERT INTO TEST_ITEMS (ITEM_VALUE) VALUES('TEST_04');
INSERT INTO TEST_ITEMS (ITEM_VALUE) VALUES('TEST_05');
INSERT INTO TEST_ITEMS (ITEM_VALUE) VALUES('TEST_06');
INSERT INTO TEST_ITEMS (ITEM_VALUE) VALUES('TEST_07');
INSERT INTO TEST_ITEMS (ITEM_VALUE) VALUES('TEST_08');
INSERT INTO TEST_ITEMS (ITEM_VALUE) VALUES('TEST_09');
Die IDs werden automatisch hochgezählt und müssen deswegen nicht extra angegeben werden.
Nun gehen wir wieder in Eclipse zurück und erstellen uns eine kleine JSP mit einer Datenbankabfrage. Und hier wird es etwas komplizierter mit den verschiedenen ClassLoader des Tomcats und den verschiedenen Config-Dateien. Ich hab die einfachste aber nicht beste Variante gewählt. Die JAR-Datei mit dem JDBC-Treiber kommt direkt in das lib-Verzeichnis des Tomcat und wir passen die zentrale context.xml Datei, die uns von Eclipse zur Verfügung gestellt wird.
Die aktuelle JAR mit dem MySQL-Treiber findet man auf dev.mysql.com.
In die Context-Datei definierten wir die DataSource als Resource.
Nun können wir über JNDI uns diese DataSource in eine JSP holen. Wir erstellen eine ganz einfach Abfrage. Das Ergebnis liefert uns ein Statement in Form eines ResultSets. Ich benutzt hier die Methode über den Index die Ergebnisse zu bekommen, z.B. getString(1) wobei aber auch getString("ITEM_ID") funktioniert und für den zielgerichteten Einsatz sehr viel besser ist, weil man so das SQL-Statement ändern kann und auch die Reihenfolge der Columns ändern, ohne dabei auf den Java-Code achten zu müssen. Hier wird aber nicht zielgerichtet ein Wert ausgelesen und z.B. in ein anderes Object geschrieben sondern einfach alles ausgegeben. Deswegen auch nur getString() und keine anderen Methoden, die einen passenden Datentyp zurück liefern und ein eigenes Casten der Werte unnötig machen.
Der Vorteil die Connection über eine DataSource zu bekommen und nicht jedes mal selbst zu initiieren ist, dass die DataSource ein Pooling der Connections vornimmt und Datenbankverbindungen zur Wiederverwendung offen hält, um den Overhead für Verbindungsaufbauten zu verringern.
SQL-Abfragen direkt in einer JSP-Seite zu ist aber eine schlechte und man sollte so etwas in DAO-Klassen auslagern und in der JSP nur die Ansicht mit schon fertigen Objekten erstellen, die dann vom DAO geliefert werden.
Außerdem werden immer mehr JPA verwendet, wo die SQL-Statements automatisch erzeugt werden. Handgeschriebenes SQL ist in komplexen Fällen meistens schneller und besser, aber ORM-Frameworks erleichtern einen die Arbeit schon sehr und man sollte sich JPA auf jeden Fall einmal
ansehen, bevor man noch direkt mit JDBC und SQL arbeitet.
Im nächsten Teil geht es dann wirklich mit PHP weiter.
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.
Als ich mit PHP anfing war es noch Alltag, dass man wenn eine Datenbank-Abfrage notwendig war diese auch einfach direkt an Ort und Stelle einbaute. Kapselung in einer eigenen Funktion oder Klasse war nicht wirklich verbreitet. Es wurde meistens direkt mit den entsprechenden Datenbank Funktionen gearbeitet und keine Abstraktion verwendet.
Nur in großen Anwendungen war es anders. Vorher hatte ich mit Java, JDBC und DataSources im Tomcat gearbeitet, wo es am Ende egal war welche Datenbank dahinter lief und man dies nur beim Anlegen der DataSource abgeben musste. Zuhause war ich auf Oracle Datenbanken und hatte nur so am Rande mit MySQL zu tun. Ich sollte ein Projekt von MySQL auf Oracle portieren und merkte schnell wie doof es war keine Abstraktion zu haben. Also nahm ich mir vor, sollte ich mal ein PHP anfangen, dass ich es besser machen würde. Genau so einfach wie mit einem Tomcat und den DataSources.
Dann kam in der Berufsschule das Thema Datenbanken, dass wir mit einer XAMPP-Installation bearbeiten sollten. Ein kleines Shop-"System". CRUD, eine Liste laden und wenn wir voll gut waren sogar Joins oder mal ein COUNT mit GROUP BY.. also alles was die meistens aus ihrem normalen Arbeitstag in und auswendig kannten. Ich wollte es dann auch gerne so schreiben, dass ich SQL über eine zentrale Klasse ausführen konnte und Schleifen einfach ausführen konnte und das Problem das bei Oracle ein Array so aussah [column]
und nicht wie bei MySQL
[column] direkt dort in der Implementierung ausgeglichen wurde und man so einfach zwischen den beiden Systemen switchen kontne ohne Code ändern zu müssen.
Aber fangen wir mal an. Zuerst erstellen wir uns eine einfache Datenbank Tabelle mit ein paar Daten. Diese werden dann im Verlaufe dieses Artikels mit PDBC aus der Datenbank laden und einmal ausgeben. Unsere Tabelle sieht wie folgt aus:
CREATE TABLE tests (
test_id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
test_name VARCHAR(255) NOT NULL,
PRIMARY KEY (test_id)
);
INSERT INTO tests(test_name) VALUES ('TEST_01');
INSERT INTO tests(test_name) VALUES ('TEST_02');
INSERT INTO tests(test_name) VALUES ('TEST_03');
INSERT INTO tests(test_name) VALUES ('TEST_04');
INSERT INTO tests(test_name) VALUES ('TEST_05');
Diese Tabelle repräsentiert Daten für eine einfache Klasse mit einer Id und einem Namen. Also das Grundmodel auf dem bestimmt fast 98% aller Entitäten basieren. Das normale vorgehen wäre jetzt diese Daten über ein einfaches SQL Query einzulesen. Dabei enthält man ein ResultSet oder Array mit allen Zeilen, die man dann in einer Schleife durchläuft. Während jedem Durchlauf erzeugt man ein neues Objekt der Klasse und befühlt dieses über die Setter mit den Daten aus dem Zeilen Array.
$result=array();
foreach($data as $row){
$obj=new Test();
$obj->setId($row["test_id"]);
$obj->setName($row["test_name"]);
$result[]=$obj;
}
Man muss viel per Hand schreiben, was sehr zeitintensiv, stupide und fehleranfällig ist. Wenn man an mehreren Stellen so lädt z.B. einmal laden eines Objekt anhand der Id, einmal die Liste und noch die Liste mit einem Pattern für den Namen, muss man alle Methoden und Funktionen wo man die Daten in das Objekt füllt ändern, wenn man mal eine Column der Datenbank hinzufügt, ändert oder entfernt.
Wenn man diesen Teil nun automatisiert, erleichtert einen das Leben schon sehr. Bei einer Klasse mit 10 Attributen sind es dann nicht mehr 12 Zeilen Code sondern nur noch 2. PDBC erledigt für einen diese Schritte. Vom erzeugen der Objekt, die Abfrage der Daten und dann das befüllen der Objekte.
Um nun zu wissen, welches Datenfeld aus der Datenbank in welches Klassen-Attribute gehört werden Annotationen genutzt. Es ist ganz leicht JPA und Hibernate beeinflusst. Wobei PDBC sehr viel weniger kann.
FKs werden nicht in Objekte abgebildet, also ein lazy oder eager Loading. Es werden nur wirklich Daten kopiert. Das SQL muss man komplett per Hand schreiben, was ich aber als Vorteil sehe, weil HQL/JPQL doch mich immer sehr eingeschränkt haben und viele wichtige SQL-Funktionen wie Nested-Queries und gute Joins oft nicht möglich waren. SQL bietet dort sehr viel mehr und performantere Möglichkeiten.
Aber hier geht es erst einmal um einfaches und schnelles Laden der Datensätze.
Dafür brauchen wir erst einmal unsere Klasse mit den entsprechenden Annotationen, die bestimmen zu welcher Column das Klassen-Attribute gehört. Das Schlüsselwort hierbei ist "@dbcolumn". Diese Annotationen werden mit Hilfe der Refelection Klassen von PHP und den DocComments gelesen und dann geparst.
Damit man hier performant bleibt werden die Properties einmal pro Klasse eingelesen und in einer Map gespeichert, so was man schnell über den Column-Name zum Property gelangt. Wenn man dann für die nächste Klasse die Zuordnung braucht hat man diese schon. Da die Map static ist, geschieht das Einlesen auch nur einmal pro Request für eine Klasse.
setAccessible(true) ist hier sehr wichtig, weil wir so private Attribute lesen und schreiben können und dazu bringt es noch einem wirklich spürbaren Performance-Vorteil. Ja.. auch bei PHP. Bei Java gilt genau das Selbe. Die Reflection-Klassen von PHP sind von den Bezeichnungen und Methoden auch sehr an Java angelehnt und wer mit den Relfections in Java sich auskennt, kann sofort in PHP weiter machen, da die wichtigen Methodennamen wirklich zu 100% übereinstimmen.
public function getId(){
return $this->id;
}
public function setId($id){
$this->id=$id;
}
public function getName(){
return $this->name;
}
public function setName($name){
$this->name=$name;
}
}
Man sieht hier die Annotationen an den Attributen der Klasse. Die Werte werden direkt in die Attribute geschrieben. Die Nutzung der Getter und Setter ist hier nicht implementiert. Wobei das große Vorteile hätte da man so z.B. eine "1" aus der Datenbank auf ein "true" mappen könnte. Dafür werde ich mal eine Implementierung mit eignen Properties schreiben, die zusammen mit dem Property auch die Accessor-Methoden mitliefert, wenn diese vorhanden sind.
Jetzt geht es aber weiter mit dem konkreten Beispiel. Zuerst werden wir uns unsere Datenbank-Klasse holen. Dafür muss die Datenbankverbindung in der Config-Datei angegeben sein. Der Name muss eindeutig sein und mit dessen Hilfe wird die Datenquelle auch identifiziert. Die Bezeichnungen in der Config-Datei orientieren sich Oracle, so nennt sich ein Feld "SID", also Service-ID, das entspricht bei MySQL dem Datenbanknamen. Sollte hier der Standard-Port von MySQL verwendet werden, ist dieser normaler weise nicht anzugeben.
Wir haben hier also unsere Datasource (wie im Tomcat) unter dem Namen "ds_test", die auch die lokale MySQL-Instanz und die Datenbank verweist. Wie man am Passwort schon vermuten kann, habe ich bei mir einfach den Bitnami WAMP-Stack verwendet, aber XAMPP oder eine vollständige Installation gehen natürlich genau so gut.
Wir müssen noch angeben wo die Config-Datei und wo die Datenbank-Klassen zu finden sind.
Wenn man PDBC in sein System integrieren will, kann man so die Config-Datei einfach mit im Config-Verzeichnis des Systems unterbringen.
Nun folgt ein kleines und einfaches SQL-Statement:
$sql="SELECT test_id,test_name FROM tests ORDER BY test_name ASC";
Und nun beginnt der spannende Teil, wir übergeben unser Datebank-Object, den SQL-String und den Klassennamen an den PDBCObjectMapper. Dieser nutzt das Interface der DB-Klasse, weswegen ihm egal ist welche Implementation verwendet wird. Dann wird die Klasse analysiert und deren Properties eingelesen und die passenden Spalten wie schon weiter oben beschrieben eingelesen. Der Rest ist eigentlich relativ primitiv. Erst wird eine Instanz der Klasse erzeugt und dann das Array mit dem Datensatz des Resultsets wir mit $key (Spaltenname) und $value (Wert aus der DB) durchlaufen.
Wird ein Property zu dem $key gefunden wird in dieses das $value geschrieben.
private function fillObject($row,$ref){
$obj=$ref->newInstance();
$props=$this->loadProperties($ref);
foreach($row as $key => $value){
if(isset($props[$key])){
$props[$key]->setValue($obj,$value);
}
}
return $obj;
}
public function queryList($db,$sql,$className){
$db->executeQuery($sql);
$ref=new ReflectionClass($className);
$result=array();
for($i=0;$i<$db->getCount();$i++){
$result[count($result)]=$this->fillObject($db->getRow($i),$ref);
}
return $result;
}
Die loadProperties-Methode habe ich ja weiter oben schon beschrieben. Hier wieder sich daran erinnern, dass wir setAccessible(true) für jedes Properties gesetzt haben und somit ohne Probleme in private-Properties schreiben können.
"Test" ist unser Klassename, $sql unser String mit dem Query-Statement und $db unsere oben in der config-Datei definierten Datasource.
Als Rückgabe erhalten wir, wie alle schon sicher richtig vermutet haben, ein Array mit unseren gefüllten Test-Objekten. Um zusehen, dass alles richtig befüllt wurde, geben wir diese einfach mal aus.
foreach($tests as $test){
echo $test->getId()." - ".$test->getName()."<br>\n";
}
Der gesamte Code für unser kleines Test-Script sieht dann also so aus:
Ich hoffe ich konnte einen kleinen Einblick geben, wie man sich die Arbeit mit Datenbanken in PHP einfacher machen kann und einen Teil der Konzepte aus der Java-Welt in PHP implementieren kann. Man kann natürlich noch sehr viel mehr implementieren und ein kompletes und komplexes ORM-System hier draus bauen, aber das würde hier zu weit führen und ich brauchte es auch so erst einmal nicht. Auch Inserts und Updates darüber zu realisieren und Getter und Setter benutzen zu können wäre toll und steht noch auf dem Plan, aber wohl alles erst im Laufe von 2016.
heute habe ich meinen ersten Test mit PHP 7 durch geführt. 1&1 ist so nett und bietet für jeden schon an PHP 7.0RC3 einzustellen. Ich hab mir eine Subdomain angelegt und diese auf PHP 7 konfiguriert. Ich wollte mal sehen, ob mein System mit PHP 7 läuft oder ich noch etwas ändern muss. Bis jetzt lief meine Homepage produktiv mit PHP 5.5 und wurde mit 5.6 entwickelt. Bei PHP7 fallen ein paar Altlasten weg, die man eigentlich schon vor Jahren hätte mal anpassen sollen. Das hatte ich vor paar Wochen gemacht:
Die alten Konstruktoren durch __construct() ersetzen. Ich weiß nicht warum die alten nicht mehr untersützt werden sollen in Java funktioniert die Art und Weise super und auch mit PHP hatte ich keine Probleme.. ist aber eben jetzt anders
Die alten msyql-Function sind nun nicht mehr dabei. Aber dafür gibt es noch die mysqli-Funktionen (auch in Form von Klassen für OOP). Da alles in einer Klasse gekapselt war, bedeutete das nur die Klasse zu kopieren und an jedes "mysql" ein "i" ran zuhängen. Also in 2 Minuten erledigt. Selbst ein automatisches Replace über alle Dateien hätte funktioniert.
Mehr hatte ich nicht geändert und es lief unter 5.6 alles sehr schnell und stabil. Dann folgte der Test auf PHP 7.. und es lauft. Gefühlt ist es immer noch stabil und schnell. Performance-Messungen habe ich noch nicht gemacht, aber werde in den nächsten 1-2 Wochen wohl mal welche nachreichen.
Fazit Ich glaube es wird kaum bis wenige Probleme beim Wechsel geben, wenn der Code strukturiert ist und nicht seit 10 Jahren nicht mehr angefasst wurde. Das "Problem" mit den mysql-Funktionen klingt schlimmer als es ist bei alten Legacy-Code wo die DB-Queries wild im Sourcecode verteilt sind. Ein Replace und es ist erledigt, das Schlimmste was passieren kann ist das man ein paar Kommentare mit umbenennt. Also keine Angst haben und einfach mal ausprobieren.
Um Java-Anwendungen schnell zu bekommen, habe ich schon viel gelernt. Das meiste basiert darauf möglichst viel Caching zu verwenden, andere Serialisierungen (die default Serialisierung von Java ist wirklich extrem langsam), Reflections richtig zu verwenden (method.setAccessible(true)) und am Ende eigentlich so wenig neue Objekte wie möglich zu erzeugen.
Das alles ist toll, aber man muss immer daran denken, dass Datebank- und Dateizugriffe immer sehr aufwenig sind. Dafür gibt es dann Connection-Pooling und NIO.
Bei PHP ist es an sich sehr ähnlich. Nur hat man hier das Problem, dass man keinen Application-Scope hat und somit alles auch mindestes für jeden Benutzer machen muss, um es dann in der Sesion zwischen zu speichern, was am Ende wieder Serialiserung bedeutet.
Mit Aoop habe ich viel über Performance gelernt in einem Framework auf PHP gelernt. Dateizugriffe sind
das schlimmste! SingleTon-Beans helfen um global zu vermeiden oder dass man einige Dinge mehrfach durchführen muss.
Was erstaunlich schnell ist, ist die Datenbank. Natürlich abhängig von den Daten, Struckturen und Queries. In Java wird immer gesagt, man solle ORMs verwenden und dann bei Schulungen zum Thema Performance lernt man die Constructor-Queries kennen und lernt wie man mit nativen SQL arbeitet. In PHP habe ich bis jetzt nur natives SQL verwendet und kann sagen, dass selbst ohne Connection-Pooling, es oft schneller ist als ORMs/JPA in Java.
Aber Connection-Pooling mit PHP wäre echt toll. ES geht.. über Umwege. Vor ein paar Jahren hatte ich mal mit Java-Bridge herum experimentiert und da war es dann auch relative einfach (da man eben Java aus PHP heraus aufrufen kann) auf JDBC zuzugreifen oder eben auch auf das JNDI und dich dort eine DataSource vom Tomcat zu holen. Mit Quercus habe ich hier noch nicht weiter getestet, aber ich gehe mal davon aus, dass man hier sehr viel mehr Performance bekommen kann. Quercus an sich soll schnell sein und mit Connection-Pooling und NIO sollten sich Anwortzeiten und Laufzeiten stark reduzieren lassen.
Vor ein paar Tagen bin ich auf ein seltsames "Problem" gestossen. Vieleicht lag es auch an was anderen aber es hat mich irritiert.
$pattern="";
$pattern=substr($result,$from,$len);
Das obere war immer schneller als wenn ich den Substring direkt zugewiesen habe ohne die Varibale vorher zu initialisieren.
$pattern=substr($result,$from,$len);
Das finde ich seht seltsam. Entweder greift hier die dynamische Typisierung ein... oder.. keine Ahnug.. aber ich werde mal weiter nachforschen.
Auch die moderneren MySQL-Funktionen werde ich mal durch testen. Hier köntne sich auch was getan haben.
Am Ende wäre es aber natürlich toll zu sehen was PHP7 oder die HHVM bringen würden. Wenn sie halten was sie versprechen würde für Lösungen wie Java-Bridge oder Quercus nur noch das Connection-Pooling bei Datenbank-Abfragen sprechen.
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?