Zum Inhalt springen

· Entwicklung

Deinem Plugin eine Datenbank geben: SQLite oder MySQL

SQLite-Datenbankschema, verbunden mit einem Paper-Minecraft-Server

Die meisten Tutorials zum Speichern von Daten in einem Minecraft-Plugin zeigen dir, wie du deinen Server kaputt machst. Sie packen einen Treiber ins .jar, den Paper längst mitliefert. Sie nehmen die UUID des Spielers als Primärschlüssel, ohne ein Wort über den Offline-Modus zu verlieren. Und sie feuern ihre Abfragen mitten aus der Spielschleife. Mit zwei Testern sieht das gut aus, dann verliert das Plugin Daten und drückt an einem vollen Abend die TPS. Hier sind die drei Entscheidungen, auf die es wirklich ankommt, samt Code.

Was die echte Server-Landschaft sagt, nicht die Doku

Drei Zahlen, die dein Schema bestimmen sollten

Fangen wir mit einer Messung an, denn genau die fehlt überall sonst. In einer Erhebung über 62.948 Server, die an bStats melden, ist 1.21.11 mit 36,4 % die am häufigsten eingesetzte Version. Danach kommt 26.1.2 mit 14,9 %, dann 26.2 mit 7,4 % und 1.21.4 mit 7,2 %. Anders gesagt: Auch nach dem Wechsel von Minecraft auf kalendarische Versionsnummern ist die 1.21-Familie klar in der Mehrheit.

AnteilWas das für deine Datenbank bedeutet
1.21.1136,4 %Dein Standard-Kompatibilitätsziel
26.1.214,9 %Das sinnvolle Produktionsziel im Kalender-Zweig
26.27,4 %Kein stabiler Paper-Build, in Produktion meiden
1.21.47,2 %Fällt noch ins Gewicht

Zwei Folgerungen. Erstens: Wer 26.2 anvisiert, zielt auf eine Version, für die Paper keinen stabilen Build hat. Das vernünftige Produktionsziel ist heute 26.1.2. Zweitens: Java 25 ist die Untergrenze der kalendarischen Versionen, trotzdem bleibt es richtig, dein Plugin gegen Java 21 zu kompilieren. Das läuft auf dem größten Teil der Server, und ein .jar für Java 21 lädt problemlos in einer neueren Laufzeitumgebung. Umgekehrt gilt das nicht, und genau dieser Fehler erzeugt beim Start einen UnsupportedClassVersionError.

Die dritte Zahl wiegt am schwersten, und kein Datenbank-Tutorial erwähnt sie: 75,1 % der Server laufen im Offline-Modus. Dazu kommen wir gleich, denn genau das entscheidet über dein Schema.

Paper liefert den SQLite-Treiber schon mit

Für SQLite brauchst du keine einzige Abhängigkeit

Das ist der Punkt, den die Hälfte der Tutorials übersieht: Der SQLite-JDBC-Treiber wird mit Paper schon ausgeliefert. Nichts einpacken, nichts deklarieren, nichts umbenennen. Du öffnest eine Verbindung und es läuft. HikariCP dagegen ist nicht dabei, und dieser eine Unterschied verändert, wie du deine plugin.yml schreibst.

plugin.yml
name: PlayerData
version: 1.0.0
main: fun.example.playerdata.PlayerDataPlugin
api-version: '1.21'
folia-supported: true
# Kein libraries-Block fuer SQLite: Paper liefert den Treiber schon mit.

Das Öffnen kostet rund fünfzehn Zeilen. Drei PRAGMA-Anweisungen solltest du sofort setzen, und die dritte überrascht selbst erfahrene Entwickler: In SQLite sind Fremdschlüssel standardmäßig ausgeschaltet. Dein ON DELETE CASCADE tut also gar nichts, solange du nicht PRAGMA foreign_keys = ON ausführst, und zwar bei jeder Verbindung neu.

Java
package fun.example.playerdata;

import org.bukkit.plugin.java.JavaPlugin;

import java.io.File;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;

public final class PlayerDataPlugin extends JavaPlugin {

    private Connection connection;
    private PlayerStore store;

    @Override
    public void onEnable() {
        if (!getDataFolder().exists() && !getDataFolder().mkdirs()) {
            getLogger().severe("Datenordner konnte nicht erstellt werden, Plugin aus.");
            getServer().getPluginManager().disablePlugin(this);
            return;
        }

        File file = new File(getDataFolder(), "data.db");
        try {
            // Paper liefert den Treiber. Dieser Aufruf erzwingt nur seine
            // Registrierung, wenn der Classloader des Plugins ihn noch nicht kennt.
            Class.forName("org.sqlite.JDBC");
            connection = DriverManager.getConnection("jdbc:sqlite:" + file.getAbsolutePath());

            try (Statement st = connection.createStatement()) {
                st.execute("PRAGMA journal_mode = WAL");
                st.execute("PRAGMA busy_timeout = 5000");
                st.execute("PRAGMA foreign_keys = ON");
            }
        } catch (ClassNotFoundException | SQLException e) {
            getLogger().severe("Datenbank nicht verfuegbar, Plugin aus: " + e.getMessage());
            getServer().getPluginManager().disablePlugin(this);
            return;
        }

        store = new PlayerStore(this, connection);
        store.createTables();

        getServer().getPluginManager().registerEvents(
            new PlayerDataListener(this, store), this);
    }

    @Override
    public void onDisable() {
        if (store != null) {
            store.flushBlocking();
        }
        if (connection != null) {
            try {
                connection.close();
            } catch (SQLException ignored) {
                // Beim Herunterfahren ist hier nichts mehr zu retten.
            }
        }
    }
}

Achte auf das disablePlugin in beiden Fehlerzweigen. Ein Plugin, das seine Datenbank nicht öffnen kann, soll sich abschalten und nicht still weiterlaufen, um bei jedem Join eine NullPointerException zu werfen. Achte außerdem darauf, dass die Listener in onEnable registriert werden: Ein Plugin, das das vergisst, kompiliert einwandfrei und tut überhaupt nichts. Die Methodensignaturen findest du in der Javadoc von Paper, die alte Bukkit-API auf hub.spigotmc.org.

Den SQLite-Treiber niemals ins .jar packen

Wenn du sqlite-jdbc in dein .jar packst, obwohl Paper ihn schon mitliefert, hast du zwei Kopien desselben Treibers, geladen von zwei verschiedenen Classloadern. Das Ergebnis ist eine ClassNotFoundException oder ein No suitable driver, das beim Lesen des Codes keinen Sinn ergibt, plus ein unnötig dickes .jar. Für alles, was nicht mitgeliefert wird, ersetzt der Block libraries: in der plugin.yml das Einpacken: Du deklarierst die Abhängigkeit, der Server lädt sie beim Start herunter. Nachzulesen auf docs.papermc.io.

Im Offline-Modus ist eine UUID keine Identität

Die echte Falle, und sie trifft drei von vier Servern

Jedes Tutorial schreibt dieselbe Zeile: player_uuid TEXT PRIMARY KEY. Auf einem Server im Online-Modus ist das richtig, dort kommt die UUID von Mojang und ändert sich nie. Aber 75,1 % der Server laufen im Offline-Modus, und dort wird die UUID nicht von Mojang vergeben, sie wird aus dem Namen abgeleitet. Der Name ist die Wahrheit, die UUID nur ein Abdruck davon. Das Verhalten der Einstellung online-mode ist im offiziellen Wiki beschrieben.

Zwei völlig alltägliche Fälle reichen dann, um deine Daten zu vernichten. Ein Spieler wechselt seinen Namen: Die UUID ändert sich, er kommt mit einer frischen Identität zurück, und seine Wirtschaft und sein Fortschritt liegen verwaist in der Tabelle. Oder der Betreiber stellt den Server auf Online-Modus um, meist weil ein Proxy dazukommt oder weil die Namen endlich von Mojang geprüft werden sollen. An diesem Tag ändern sich alle UUIDs auf einen Schlag, und keine einzige Zeile in der Datenbank passt noch zu einem bekannten Spieler. Niemand hat etwas gelöscht, und trotzdem ist alles weg.

Das Symptom, das du wirklich sehen wirst

Im Ticket steht nie „UUID-Problem“. Da steht „ich habe mein Geld verloren, nachdem ich meinen Namen geändert habe“ oder „seit dem Update fangen alle wieder bei null an“. Das ist dieselbe Ursache. Und weil die alten Zeilen noch in der Tabelle stehen, hilft dir das Zurückspielen eines Backups überhaupt nicht.
Java
package fun.example.playerdata;

import org.bukkit.Bukkit;
import org.bukkit.entity.Player;

import java.util.Locale;

public final class Identity {

    private Identity() {
    }

    /**
     * Im Online-Modus kommt die UUID von Mojang und uebersteht einen Namenswechsel.
     * Im Offline-Modus wird sie aus dem Namen abgeleitet, also wechselt sie mit.
     * Beide Spalten behalten, und keine Fachtabelle an eine davon haengen.
     */
    public static String offlineKey(Player player) {
        return player.getName().toLowerCase(Locale.ROOT);
    }

    public static boolean onlineMode() {
        return Bukkit.getServer().getOnlineMode();
    }

    public static void warnIfRisky(org.bukkit.plugin.Plugin plugin) {
        if (!onlineMode()) {
            plugin.getLogger().warning(
                "Server im Offline-Modus: UUIDs werden aus Namen abgeleitet. "
                    + "Daten haengen an offline_key, nicht an der UUID.");
        }
    }
}

Eine Warnung beim Start, wenn der Server im Offline-Modus läuft, kostet drei Zeilen und erspart dem Nächsten, der dein Plugin übernimmt, Stunden Rätselraten.

Das Schema, das einen Namenswechsel übersteht

Ein interner Schlüssel, zwei Identitätsspalten

Die Lösung ist nicht, zwischen UUID und Namen zu wählen. Die Lösung ist, deine Fachtabellen an keines von beiden zu hängen. Du legst einen internen Bezeichner an, eine schlichte automatisch hochzählende Zahl, und jede Datentabelle zeigt darauf. Die Identitätsspalten stehen an genau einer Stelle, in der Spielertabelle. Ändert sich eine Identität, schreibst du ein UPDATE auf eine einzige Zeile, und die ganze Historie hängt weiter am selben Schlüssel.

SQL
CREATE TABLE IF NOT EXISTS players (
  id           INTEGER PRIMARY KEY AUTOINCREMENT,
  mojang_uuid  TEXT    UNIQUE,
  offline_key  TEXT    UNIQUE,
  last_name    TEXT    NOT NULL,
  first_seen   INTEGER NOT NULL,
  updated_at   INTEGER NOT NULL
);

CREATE TABLE IF NOT EXISTS balances (
  player_id  INTEGER PRIMARY KEY REFERENCES players(id) ON DELETE CASCADE,
  coins      INTEGER NOT NULL DEFAULT 0
);

CREATE INDEX IF NOT EXISTS idx_players_last_name ON players(last_name);

Mit diesem Schema verweisen balances, homes, stats und alles, was du später ergänzt, auf players.id. Ein Server vom Offline- in den Online-Modus zu bringen wird damit zu einer gewöhnlichen Wartungsaufgabe statt zu einem Datenverlust. Das ist der einzige strukturelle Unterschied zwischen einem Tutorial-Plugin und einem Plugin, das auf Dauer hält.

Halte last_name immer aktuell

Der aktuelle Name ist nicht nur Anzeigekomfort, er ist dein einziger Weg, einen Spieler zu finden, wenn ein Admin sagt: gib Kevin sein Geld zurück. Aktualisiere ihn bei jedem Join, im selben UPSERT wie alles andere.
Java
package fun.example.playerdata;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.UUID;

public final class PlayerStore {

    private static final String UPSERT = """
        INSERT INTO players (mojang_uuid, offline_key, last_name, first_seen, updated_at)
        VALUES (?, ?, ?, ?, ?)
        ON CONFLICT(offline_key) DO UPDATE SET
          mojang_uuid = excluded.mojang_uuid,
          last_name   = excluded.last_name,
          updated_at  = excluded.updated_at
        """;

    private final PlayerDataPlugin plugin;
    private final Connection connection;

    public PlayerStore(PlayerDataPlugin plugin, Connection connection) {
        this.plugin = plugin;
        this.connection = connection;
    }

    public int loadCoins(UUID uuid, String offlineKey) {
        String sql = """
            SELECT b.coins FROM balances b
            JOIN players p ON p.id = b.player_id
            WHERE p.offline_key = ? OR p.mojang_uuid = ?
            """;
        // Immer PreparedStatement: ein Spielername ist ungepruefte Nutzereingabe.
        try (PreparedStatement ps = connection.prepareStatement(sql)) {
            ps.setString(1, offlineKey);
            ps.setString(2, uuid.toString());
            try (ResultSet rs = ps.executeQuery()) {
                return rs.next() ? rs.getInt(1) : 0;
            }
        } catch (SQLException e) {
            plugin.getLogger().warning("Kontostand nicht lesbar: " + e.getMessage());
            return 0;
        }
    }
}

Ein Datenbankzugriff berührt niemals den Haupt-Thread

Die Regel ohne Ausnahme

Der Haupt-Thread führt die Spielschleife aus. Solange er beschäftigt ist, bewegt sich nichts: keine Mobs, keine Redstone, keine Netzwerkpakete. Eine Abfrage aus einem Event-Listener hält also den ganzen Server an, bis die Antwort da ist. Bei lokalem SQLite merkst du das im Test nicht, an einem gut besuchten Abend sehr wohl. Bei einer entfernten MySQL-Datenbank ist jeder Weg zum Server und zurück ein sichtbarer TPS-Einbruch. Datenbankzugriffe dürfen den Haupt-Thread nie blockieren, ohne Ausnahme.

Das Muster, das funktioniert, hat drei Regeln. Beim Join asynchron lesen, den Wert während der Sitzung heiß im Speicher halten, beim Verlassen asynchron schreiben. Im laufenden Spiel spricht kein Befehl und kein Event mit der Datenbank, sondern nur mit dem Cache.

Java
package fun.example.playerdata;

import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;

import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

public final class PlayerDataListener implements Listener {

    private final PlayerDataPlugin plugin;
    private final PlayerStore store;
    private final Map<UUID, Integer> cache = new ConcurrentHashMap<>();

    public PlayerDataListener(PlayerDataPlugin plugin, PlayerStore store) {
        this.plugin = plugin;
        this.store = store;
    }

    @EventHandler(priority = EventPriority.MONITOR)
    public void onJoin(PlayerJoinEvent event) {
        Player player = event.getPlayer();
        UUID uuid = player.getUniqueId();
        String key = Identity.offlineKey(player);

        // Das Lesen verlaesst den Spiel-Thread. Der Spieler spielt schon, er wartet nicht.
        Bukkit.getAsyncScheduler().runNow(plugin, task -> {
            int coins = store.loadCoins(uuid, key);
            cache.put(uuid, coins);

            // Zurueck auf den Thread, der die Entity besitzt. Gilt auch auf Folia.
            player.getScheduler().run(plugin, back -> {
                if (player.isOnline()) {
                    player.sendMessage(Component.text(
                        "Kontostand geladen: " + coins, NamedTextColor.YELLOW));
                }
            }, null);
        });
    }

    @EventHandler
    public void onQuit(PlayerQuitEvent event) {
        UUID uuid = event.getPlayer().getUniqueId();
        Integer coins = cache.remove(uuid);
        if (coins == null) {
            return;
        }
        String key = Identity.offlineKey(event.getPlayer());
        Bukkit.getAsyncScheduler().runNow(plugin, task -> store.saveCoins(uuid, key, coins));
    }

    /** Sofortiger Lesezugriff aus dem Speicher: kein Datenbankzugriff auf dem Spiel-Thread. */
    public int coins(UUID uuid) {
        return cache.getOrDefault(uuid, 0);
    }
}

Zwei Details lohnen einen Blick. Bukkit.getAsyncScheduler() ist der asynchrone Scheduler von Paper, und er funktioniert auf einem klassischen Server genauso wie auf Folia, anders als das alte runTaskAsynchronously aus den Tutorials. Und der Rückweg zum Spieler läuft über player.getScheduler(), nicht über den Haupt-Thread: Auf Folia ist der Thread, der einen Spieler besitzt, nicht zwangsläufig der, den du erwartest.

Drei klassische Fehler in diesem Muster

Die Verbindung in onDisable schließen, während asynchrone Schreibvorgänge noch unterwegs sind. Dann verlierst du die letzten Speicherungen, und genau das verhindert flushBlocking. Die Bukkit-API aus dem asynchronen Thread aufrufen, was zu zufälligen Exceptions führt, die niemand nachstellen kann. Und einen Spielernamen in eine Abfrage hineinschreiben statt ein PreparedStatement zu nehmen: Im Offline-Modus ist ein Name eine beliebige Nutzereingabe, also eine offene Flanke für SQL-Injection.

Wann MySQL sich wirklich lohnt

Die einzige echte technische Grenze

MySQL ist nicht die erwachsene Fassung von SQLite. Es ist ein Werkzeug für ein einziges Problem: mehrere Prozesse, die dieselben Daten lesen und schreiben müssen. Eine SQLite-Datei ist für einen Prozess gebaut. Zwei Minecraft-Server auf derselben Datei, besonders über eine Netzwerkfreigabe, beschädigen die Datei mit Ansage, und die Grenzen der Sperrmechanik beschreibt SQLite selbst.

SQLiteMySQL
TreiberSchon in PaperIn libraries: deklarieren
AufbauKeiner, eine DateiEin Server zum Betreiben
Ein ServerDie richtige WahlUnnötige Komplexität
Mehrere ServerDatenschaden garantiertDer eigentliche Zweck
BackupDatei kopierenDump einplanen
FehlerquellenKeine zusätzlicheNetzwerk und Datenbank

Wenn du auf MySQL wechselst, brauchst du einen Verbindungspool, also HikariCP, und der wird nicht mitgeliefert. Genau dafür gibt es den Block libraries:. Deklariere den MySQL-Treiber gleich mit, statt anzunehmen, dass er vorhanden ist.

plugin.yml
name: PlayerData
version: 1.0.0
main: fun.example.playerdata.PlayerDataPlugin
api-version: '1.21'
folia-supported: true
libraries:
  - com.zaxxer:HikariCP:5.1.0
  - com.mysql:mysql-connector-j:8.4.0
Java
package fun.example.playerdata;

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;

public final class Pool {

    private Pool() {
    }

    public static HikariDataSource create(String host, int port, String database,
                                          String user, String password) {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:mysql://" + host + ":" + port + "/" + database);
        config.setUsername(user);
        config.setPassword(password);

        // Ein Minecraft-Server hat einen Spiel-Thread. Ein riesiger Pool macht ihn
        // nicht schneller, er macht nur Verbindungslecks schwerer zu finden.
        config.setMaximumPoolSize(6);
        config.setMinimumIdle(2);
        config.setConnectionTimeout(5000);
        config.setPoolName("PlayerData");
        config.addDataSourceProperty("cachePrepStmts", "true");
        return new HikariDataSource(config);
    }
}

Für Wirtschaftsdaten bleibt Vault die richtige Tür

Wenn deine Daten ein Geldkonto sind, schau dir zuerst Vault an, den Standard für Zusammenspiel im Ökosystem. Ein Plugin, das seine Wirtschaft über Vault anbietet, passt mit allem anderen auf dem Server zusammen. Eine eigene isolierte Geldtabelle bedeutet, dass dein Plugin mit nichts anderem zusammenspielt.

Folia und die Regionen

Mehrere Threads ändern die Regeln

Folia führt die Regionen der Welt auf mehreren Threads aus. Ein Plugin, das von einem einzigen Haupt-Thread ausgeht, wird nicht langsam, es bringt den Server zum Absturz. Die gute Nachricht: Das oben gezeigte Muster ist schon kompatibel. Die Datenbankarbeit läuft asynchron, und der Rückweg geht über den Scheduler der betroffenen Entity. Für alles, was an einem Block oder einer Position hängt statt an einer Entity, nimm den RegionScheduler, nie den globalen Scheduler. Die Referenz dazu steht auf docs.papermc.io.

folia-supported: true in die plugin.yml zu schreiben, ohne diese Arbeit gemacht zu haben, ist schlimmer als gar nichts zu deklarieren: Der Server glaubt dir und lädt das Plugin.

Was in der Praxis kaputtgeht

Die Grenzen, auf die du stoßen wirst

Hier ist die Liste der Dinge, die in Produktion scheitern und die kein Tutorial erwähnt.

  • Code aus einem 1.21-Tutorial kompiliert nicht mehr. Paper 26.x bringt Adventure 5 mit, und dort wurden APIs entfernt, die vorher nur als veraltet markiert waren. Alles rund um Nachrichten und Text für Spieler trifft es zuerst.
  • Die Deobfuskierung hat die Lage verändert. Minecraft ist seit 26.1 deobfuskiert, und den Remapper von Paper gibt es nicht mehr. Die alten Rezepte für Zugriffe auf Server-Interna, die überall weiter kopiert werden, beschreiben die Realität nicht mehr.
  • SQLITE_BUSY unter Last. Zwei Threads aus deinem eigenen Plugin, die gleichzeitig schreiben, reichen schon. Der WAL-Modus und ein busy_timeout fangen das Meiste ab, du musst sie aber ausdrücklich setzen.
  • Der Befehl /reload. Er lässt Verbindungen offen und Tasks verwaist zurück. Bei einem Plugin mit eigener Datenbank erzeugt er Datenschäden, deren Ursache kaum noch zu finden ist. Starte den Server stattdessen neu.
  • Ein Cache, der nie geleert wird. Eine HashMap, die beim Join gefüllt und beim Verlassen nicht aufgeräumt wird, ist ein langsames Speicherleck. Es zeigt sich nach Tagen Laufzeit, nie im Test.
  • Kein Backup-Plan. Eine SQLite-Datei zu kopieren, während der Server hineinschreibt, ergibt ein unbrauchbares Backup. Kopiere sie bei gestopptem Server, oder nutze den Online-Backup-Mechanismus von SQLite.

Die Entscheidung, die ich treffen würde

Meine Meinung, ohne unnötiges Abwägen

Ein einzelner Minecraft-Server, egal bei welcher Spielerzahl: SQLite. Der Treiber ist schon da, es gibt nichts zu betreiben, nichts zu überwachen, das Backup ist ein cp, und die einzige zusätzliche Fehlerquelle ist eine Datei auf deiner Platte. Ich habe noch keinen einzelnen Server gesehen, der eine echte Grenze von SQLite erreicht hat, die nicht in Wahrheit von einer Abfrage auf dem Haupt-Thread oder einem fehlenden Index kam.

SQLite, was du gewinnst

Treiber von Paper mitgeliefert

Kein Dienst zu betreiben

Backup durch Dateikopie

Keine Netzwerklatenz

SQLite, was du aufgibst

Nur ein schreibender Prozess

Kein Teilen zwischen Servern

Fremdschlüssel sind aus

Sperren selbst einstellen

Ab dem zweiten Server, der dieselben Daten sehen muss, etwa ein Lobby- und ein Survival-Server hinter einem Proxy, ist MySQL das richtige und einzig sinnvolle Werkzeug. Vorher nicht. Und wenn du zögerst, weil „später sind es vielleicht mehrere“, dann schreib deinen Datenzugriff hinter eine kleine Schnittstelle: Der Wechsel kostet dich dann eine Klasse, keine Neuentwicklung.

Ein Plugin mit Datenbank, ohne die Verkabelung selbst zu schreiben

Beschreibe, was dein Plugin sich merken soll. Minax schreibt den Java-Code, kümmert sich um die Speicherung und gibt dir ein kompiliertes .jar, das du online testen kannst.

Plugin erstellen

Fazit

Drei Entscheidungen trennen ein Plugin, das hält, von einem, das Daten verliert. Nimm SQLite, solange du einen Server hast, denn der Treiber ist schon in Paper. Häng deine Fachtabellen nie an eine Spieler-UUID, denn auf 75,1 % der Server ist diese UUID nur ein Abdruck eines Namens. Und lass nie eine Abfrage den Haupt-Thread berühren. Alles andere, Indizes, Migrationen, Verbindungspools, folgt aus diesen drei Entscheidungen. Für den nächsten Schritt deckt unser Fehler-Guide die häufigsten Startprobleme ab.

Ähnliche Artikel