Hadde

Curator
Nov. 1, 2017
2
1
Einleitung
Ich arbeite schon seit einiger Zeit an Java-Anwendungen. Seitdem ich mehr und mehr Plugins mit der SpigotAPI schreibe, bin ich immer mehr Kontakt mit der großen Community der Plugin-Entwickler.
Während ich den Leuten mit den gleichen Problemen helfe, die ich am Anfang hatte, stolpere ich immer wieder über schlechte Design Pattern und anderes schlechtes Zeug, besonders wenn es um Datenbankverbindungen geht.
Viele Entwickler benutzen einzelne Verbindungen, die sie im besten Fall aufrecht erhalten wollen. Manche erstellen einfach eine Verbindung und hoffen, dass sie nie kaputt geht (Tipp: Sie geht kaputt.)

Als ich anfing, mit Java und Datenbanken zu arbeiten, habe ich auch einige der erwähnten schlechten Pattern verwendet. Aber das war nur, weil es jeder so macht und ich dachte, es wäre okay. Aber alles änderte sich, als ein Freund DataSources erwähnte. Ich war neugierig und entdeckte einen Haufen neuer Möglichkeiten in Sachen Performance und Zuverlässigkeit.

Da nicht jeder Freunde wie mich hat, die manchmal sagen "Was machst du da? Das ist doch Scheiße!", möchte ich dieser Freund für dich sein.

Also lasst mich etwas sagen, wenn ihr eine dieser Fragen mit "Ja" beantworten könnt:

  • Ich habe noch nie von einer DataSource gehört oder sie benutzt
  • Ich habe noch nie von Try-with-Resources in Verbindung mit Datenbankverbindungen und Abfragen gehört
  • Ich habe eine Datei in meinem Plugin namens MySQL.java
  • Ich habe noch nie eine Methode namens close() im Zusammenhang mit Datenbankverbindungen und -abfragen verwendet.
Wenn du eine oder mehrere Fragen mit ja beantwortet hast, ist dies für dich: Was machst du da? Das ist Scheiße!

Okay, lass uns Freunde sein. Ich möchte dir helfen. Lass uns damit beginnen, wie du mit deiner Datenbank umgehst.

DataSource
Eine DataSource ist eine Schnittstelle des javax.sql Pakets. Keine Sorge, ich werde das ganze so einfach wie möglich halten.

Eine DataSource wird durch einen Datenbanktreiber implementiert. Dieser Treiber ist für jede Datenbanksoftware unterschiedlich. Das bedeutet, dass du einen anderen Treiber brauchst, je nachdem welche Datenbank du anbinden möchtest.
Da die meisten von euch MariaDB verwenden, werde ich mich in meinen Beispielen daran halten, wenn ich einige datenbankspezifische Dinge benötige.
Allerdings ist die DataSource so konzipiert, dass es nicht so sehr darauf ankommt, welche Datenbanksoftware du verwendest.

Aber was macht die DataSource?
Eine DataSource stellt eine Verbindung zu deiner Datenbank her. Wenn du das hier liest, benutzt du wahrscheinlich eine einzelne Verbindung, die manchmal wahrscheinlich fehlschlägt oder einen Timeout bekommt. Die DataSource wird dies für dich erledigen.

Neben der DataSource wollen wir Connection Pooling nutzen. Dazu werden wir HikariCP verwenden. HikariCP ist ein Connection Pooling Framework, welches uns erlaubt, einen Pool von Verbindungen zu bekommen, die von Hikari verwaltet werden.
Das bedeutet, dass wir immer eine verfügbare, nicht unterbrochene Datenbankverbindung haben und parallele Lese- und Schreibzugriffe auf unsere Datenbank durchführen können, ohne zu warten, dass die aktuelle Transaktion beendet ist. Erstaunlich oder? Keine Angst davor, andere Anfragen zu blockieren, weil deine Verbindung gerade beschäftigt ist. Benutze einfach eine andere aus dem Pool.

Wie man eine DataSource mit HikariCP erstellt
Okay, kommen wir zum eigentlichen Thema.

Wir werden nun eine Datenbankverbindung zu einer MariaDB mit Hilfe des Hikari ConnectionPools erstellen.
Dazu benötigst du die Daten, die du immer brauchst. Du hast also wahrscheinlich schon eine Sql-Einstellung. Diese kannst du weiter verwenden.

Ich zeige dir zunächst den gesamten Code und erkläre dir anschließend alles.
Es gibt verschiedene Möglichkeiten, einen Connection Pool mit Hikari zu erstellen. Ich werde dir den Weg zeigen, den ich benutze.
Hikari hat vernünftige Standardeinstellungen. Wir werden hier nicht viel ändern.

Java:
 Properties props = new Properties();
   props.setProperty("dataSourceClassName", "org.mariadb.jdbc.MariaDbDataSource");
   props.setProperty("dataSource.serverName", settings.getAddress());
   props.setProperty("dataSource.portNumber", settings.getPort());
   props.setProperty("dataSource.user", settings.getUser());
   props.setProperty("dataSource.password", settings.getPassword());
   props.setProperty("dataSource.databaseName", settings.getDatabase());

   HikariConfig config = new HikariConfig(props);

   config.setMaximumPoolSize(settings.getMaxConnections());

   DataSource source = new HikariDataSource(config);

Und das war's. Wir haben einen Verbindungspool mit einer definierten Anzahl von Verbindungen. Standardmäßig sind das 10, was in den meisten Fällen ausreichen wird, vor allem bei der Programmierung von Spigot Plugins.

Aber lass uns Schritt für Schritt vorgehen.

Wir erstellen ein neues Properties Objekt. Dies ist ein in Java eingebauter Datentyp. Es ist im Grunde eine Key Value Map. Nichts magisches.
Nun konfigurieren wir unsere Datenbank. Die erste Zeile ist das Einzige, was für uns von der Datenbanksoftware abhängt.
Du musst die Treiberklasse für die Datenbank angeben. Dieser Treiber muss in deinem Plugin enthalten sein oder auf eine andere Art und Weise bereitgestellt werden. Eine Liste, welche Treiberklasse für welche Datenbank verwendet werden muss, findest du hier. MariaDB ist bereits in Spigot enthalten.

Der Rest sollte für dich ziemlich alltäglich sein. Wir stellen Adresse, Port, Benutzername, Passwort und den Namen der Datenbank ein.
Wichtig hier: Du kannst auch nur Benutzer und Passwort eingeben und den Rest nicht einstellen. In diesem Fall werden die Standardwerte verwendet. Die Werte werden durch die Standardwerte des verwendeten Datenbanktreibers definiert und unterscheiden sich bei jedem Treiber (Besonders der Port, da jede Datenbank einen anderen Port verwendet).

Nachdem wir unser Property Objekt konfiguriert haben, erstellen wir einfach eine Hikari Config.
Die Hikari Config erlaubt uns die Konfiguration des Connection Pools. Du kannst hier eine Menge definieren. Aber da die meisten Standardwerte ziemlich vernünftig sind, würde ich hier nicht viel ändern.

In unserem Fall definieren wir nur die maximalen aktiven Verbindungen zu unserer Datenbank.

Und mit diesen Einstellungen erstellen wir schließlich unsere HikariDataSource mit einem Connection Pool. Wir sind fertig.

Wie man eine Datenquelle benutzt
Nachdem wir unsere Datenquelle erstellt haben, interessiert uns die darunterliegende Implementierung nicht mehr. Deshalb werde ich von nun an immer von DataSource statt von einer HikariDataSource sprechen.

Um deinen Codefluss sauber zu halten, wirst du hoffentlich die Gelegenheit nutzen, um deine MySQL.java Datei loszuwerden.
Von nun an wirst du die Datasource an die Klasse übergeben, in der sie benötigt wird.

Jetzt wollen wir und aber mal praktisch angucken, wie du eine Verbindung bekommst. Alle Codesnippeds werden in einer Klasse definiert, die wahrscheinlich wie folgt aussieht:

Java:
public class SomeClass {
   DataSource source;

   public SomeClass(DataSource source) {
       this.source = source;
   }
}

Eine Verbindung kann wie folgt abgerufen werden:

Java:
 Connection conn = source.getConnection();

Das ist alles. Aber STOP! Wenn du jetzt aufhörst zu lesen, wirst du alles nur noch schlimmer machen.
Um mit Datenbankverbindungen richtig zu arbeiten, musst du Try-with-Resources verwenden.

Und das ist es, was wir uns als nächstes anschauen.

Warum Try-with-Resources benutzen
Eine Connection, Statement (PreparedStatement) und ResultSet sind AutoCloseable.
Das bedeutet, dass sie geschlossen, aber auch automatisch geschlossen werden können (offensichtlich...).

Wenn du eine Connection öffnest, bleibt diese offen, bis sie geschlossen wird.
Eine Statement muss gecached werden, bis es geschlossen wird.
Ein ResultSet wird ebenfalls gecached, bis es geschlossen wird.

Wenn du es versäumst, sie zu schließen, hast du ein MemoryLeak und du blockierst Verbindungen und/oder Cache.
Du könntest irgendwann auch keine freien Verbindungen mehr haben.

Um dies zu vermeiden, solltest du einen Try-with-Ressourcen verwenden.
Das stellt sicher, dass alle Closeables geschlossen werden, wenn du den Codeblock verlässt.

Hier ist etwas "Pseudo-Code", der dir den Vorteil eines Auto-Closeables zeigt.

Momentan machst du so etwas wahrscheinlich ohne eine Datenquelle oder ein Try-with-Resources. Du bekommst deine Verbindung von irgendwoher. Diese Verbindung ist wahrscheinlich static und anderes böses Zeug.

Java:
try {
   Connection conn = getConnection();
   PreparedStatement stmt = conn.prepareStatement("SELECT some from stuff");
   stmt.setSomething(1, irgendwas);
   ResultSet rs = stmt.exeuteQuery();
   // etwas mit dem ResultSet machen

   // Der folgende Teil wird in den meisten Fällen übersehen. Viele ppl vergessen, ihren Mist zu schließen.
   conn.close();
   stmt.close(); // Das Schließen des Statements schließt auch das ResultSet des Statements.
} catch (SQLException e){
   e.printStackstrace(); // Dies sollte mit einer propperen Logging-Lösung ersetzt werden. Tue dies nicht.
}

Mit AutoCloseable musst du dich nicht mehr um das Schließen deiner Daten kümmern.
Wir wird auch eine DataSource namens source verwenden, die wir irgendwo innerhalb unserer Klasse hinterlegt haben (Nein, wir holen sie nicht über eine statische Variable von irgendwoher. Das ist schlechtes Design...)

Java:
try (Connection conn = source.getConnection(); PreparedStatement stmt = conn.prepareStatement("SELECT some from stuff")) {
   stmt.setSomething(1, irgendwas);
   ResultSet rs = stmt.exeuteQuery();
   // etwas mit dem ResultSet machen
} catch (SQLException e) {
   e.printStackstrace(); // Dies sollte mit einer korrekten Logging-Lösung ersetzt werden. Mach sowas nicht.
}

Wie du sehen kannst, schließen wir hier nichts, weil wir es nicht brauchen. Jedes Objekt, das du innerhalb der try-Klammern zuweist, wird geschlossen, wenn du den Codeblock verlässt.
Dies wird die Verbindung zu unserem Connection Pool zurückgeben. Gib den blockierten Speicher für den Ergebnis-Cache und das Statement frei und wir sind bereit für die nächste Anfrage.
NAtürlich muss das Objekt, das innerhalb der geschweiften Klammern zugewiesen wird, vom Typ AutoCloseable sein.
(Hinweis: Viele weitere Klassen sind AutoCloseable. Wie zum Beispiel Input und Output Streams. Schau dir die Sachen an, die du benutzt und benutze Try-with-Resources wo immer du kannst).

Eine weitere Ergänzung hier. Das ResultSet ist auch ein AutoCloseable, aber wir erstellen sie nicht innerhalb der try-Klammern. Es wird trotzdem geschlossen. Lasst uns einen Blick in die ResultSet Dokumentation werfen.
Ein ResultSet-Objekt wird automatisch geschlossen, wenn das Statement-Objekt, das es erzeugt hat, geschlossen, erneut ausgeführt oder verwendet wird, um das nächste Ergebnis aus einer Folge von mehreren Ergebnissen abzurufen.
ResultSet (Java Platform SE 7 )

Und das wars auch schon. Das ist Try-with-Resources. Deine Verbindung, Anweisung und Ergebnismenge werden freigegeben, wenn du den Codeblock verlässt und du musst dich nicht mehr darum kümmern.
Jetzt geh da raus und repariere dein kaputtes Zeug. (Nein ein MySQL.java ist nicht gut...)

Hast du ein paar Beispiele?
Aber natürlich! Vielleicht möchtest du in diesen Code schauen. Er ist nicht perfekt, aber besser als 99% der Dinge, die ich täglich sehe.

Beispielimplementierung einer DataSource für MariaDB und PostgreSQL

Beispielimplementierung und Verwendung einer DataSource mit Beispielen für das Ausprobieren von Ressourcen und grundlegenden Sql-Abfragen

Wenn du sehen willst, wie sehr eine DataSource und ein ConnectionPool deine Anwendung beschleunigen können, kannst du diesen kleinen Benchmark ausführen

Oracle Dokumentation und Beispiele für Try-with-Resources

Oracle Dokumentation für DataSources

Fühlt euch frei, Erweiterungen in das Git Repo einzureichen. Ich bin immer offen dafür, dieses Zeug zu verbessern.

Beschwerden zum Text bitte an DeepL. Das war Lazy übersetzung. Die Originalversion in Englisch gibts hier:
 

Adwirawien

Moderator
Jan. 16, 2019
2
Lieber neuer Freund,
diese sehr detailreich ausgeführte Vorgehensweise mit Datenbanken im Java-Umfeld ist einfach unglaublich 😍🤯

Mit diesen Tipps muss mein armer Datenbankserver nicht mehr jahrelang mehr Strom als all die Bitcoin Miner verbrauchen nur um die alte Connection offen zu halten! Auch Nitrado dürfte sich über die sinkenden Stromkosten freuen.

Diesen Beitrag mit allen Tutorial-Nachprogrammierern zu teilen ist absolut Pflicht!
 
  • Like
Reaktionen: Hadde

Users who are viewing this thema