Skip to main content
Represents a simple database where values are flattened and stored by UUID. This class automatically handles table creation, data serialization to JSON, and expiration of old entries. The table structure is automatically created as follows:
UUID varchar(64) | Name text       | Data text      | Updated bigint
--------------------------------------------------------------------
Player's uuid    | Last known name | {json data}    | Date of last save
This class uses JSON to flatten values and provides onLoad() and onSave() methods for easy data persistence. For more control over table structure, use SimpleDatabase instead.

Setup

Extend SimpleFlatDatabase with your cache type and implement the required methods.
public class PlayerDatabase extends SimpleFlatDatabase<PlayerCache> {
    
    public PlayerDatabase() {
        // Required: set the table name variable
        addVariable("table", "player_data");
    }
    
    public void connect(DatabaseSettings settings) {
        connect(
            settings.getHost(),
            settings.getPort(),
            settings.getDatabase(),
            settings.getUser(),
            settings.getPassword()
        );
    }
    
    @Override
    protected void onLoad(SerializedMap map, PlayerCache cache) {
        // Deserialize data from map into cache
        cache.setBalance(map.getDouble("balance", 0.0));
        cache.setLevel(map.getInteger("level", 1));
        cache.setLastSeen(map.getLong("last_seen"));
    }
    
    @Override
    protected SerializedMap onSave(PlayerCache cache) {
        // Serialize cache data to map
        return SerializedMap.of(
            "balance", cache.getBalance(),
            "level", cache.getLevel(),
            "last_seen", System.currentTimeMillis()
        );
    }
}

Required implementation

onLoad(SerializedMap map, T data)

Deserializes data from the database into your cache object.
map
SerializedMap
required
The map automatically converted from the JSON array stored in the database
data
T
required
Your cache object to populate with data
@Override
protected void onLoad(SerializedMap map, PlayerCache cache) {
    // Load primitive values
    cache.setBalance(map.getDouble("balance", 0.0));
    cache.setLevel(map.getInteger("level", 1));
    
    // Load collections
    cache.setHomes(map.getMap("homes", String.class, Location.class));
    cache.setPermissions(map.getList("permissions", String.class));
    
    // Load custom objects
    if (map.containsKey("settings")) {
        cache.setSettings(map.get("settings", PlayerSettings.class));
    }
}

onSave(T data)

Serializes your cache object to a map for database storage.
data
T
required
Your cache object to serialize
@Override
protected SerializedMap onSave(PlayerCache cache) {
    // Return null or empty map to delete the row
    if (cache.isEmpty()) {
        return null;
    }
    
    return SerializedMap.of(
        "balance", cache.getBalance(),
        "level", cache.getLevel(),
        "homes", cache.getHomes(),
        "permissions", cache.getPermissions(),
        "settings", cache.getSettings()
    );
}
Returning an empty SerializedMap or null from onSave() will automatically delete the player’s row from the database.

Loading data

load(Player player, T cache)

Loads data for a player into the cache asynchronously.
player
Player
required
The player to load data for
cache
T
required
Your cache object to populate
PlayerCache cache = new PlayerCache();
database.load(player, cache);

load(Player player, T cache, Runnable runAfterLoad)

Loads data with a callback that runs on the main thread after loading completes.
player
Player
required
The player to load data for
cache
T
required
Your cache object to populate
runAfterLoad
Runnable
Callback executed on the main thread when loading is complete
PlayerCache cache = new PlayerCache();

database.load(player, cache, () -> {
    // This runs on the main thread after data is loaded
    player.sendMessage("Welcome back! Balance: " + cache.getBalance());
});

load(UUID uuid, T cache)

Loads data by UUID instead of Player object.
uuid
UUID
required
The player’s unique ID
cache
T
required
Your cache object to populate
UUID uuid = UUID.fromString("...");
PlayerCache cache = new PlayerCache();
database.load(uuid, cache);

load(UUID uuid, T cache, Runnable runAfterLoad)

Loads data by UUID with a completion callback.
uuid
UUID
required
The player’s unique ID
cache
T
required
Your cache object to populate
runAfterLoad
Runnable
Callback executed on the main thread when loading is complete
database.load(uuid, cache, () -> {
    Common.log("Data loaded for " + uuid);
});

Saving data

save(Player player, T cache)

Saves player data to the database asynchronously.
player
Player
required
The player to save data for
cache
T
required
Your cache object to save
database.save(player, cache);

save(Player player, T cache, Runnable runAfterSave)

Saves data with a callback that runs on the main thread after saving completes.
player
Player
required
The player to save data for
cache
T
required
Your cache object to save
runAfterSave
Runnable
Callback executed on the main thread when saving is complete
database.save(player, cache, () -> {
    player.sendMessage("Your data has been saved!");
});

save(String name, UUID uuid, T cache)

Saves data using name and UUID.
name
String
required
The player’s name (stored for reference)
uuid
UUID
required
The player’s unique ID
cache
T
required
Your cache object to save
database.save("Steve", uuid, cache);

save(String name, UUID uuid, T cache, Runnable runAfterSave)

Saves data with name, UUID, and a completion callback.
name
String
required
The player’s name
uuid
UUID
required
The player’s unique ID
cache
T
required
Your cache object to save
runAfterSave
Runnable
Callback executed on the main thread when saving is complete
database.save(playerName, uuid, cache, () -> {
    Common.log("Saved data for " + playerName);
});

Lifecycle hooks

onConnectFinish()

Called after the connection is established and the table is created and purged.
@Override
protected void onConnectFinish() {
    // Run any initialization code
    Common.log("Database connected and ready!");
    
    // Load global data, run migrations, etc.
}

getExpirationDays()

Defines how many days of inactivity before a row is automatically deleted.
@Override
protected int getExpirationDays() {
    return 180; // Keep data for 180 days
}
Rows are deleted based on the Updated column, which is set whenever save() is called. Inactive players will have their data automatically removed.

Complete example

public class PlayerDatabase extends SimpleFlatDatabase<PlayerCache> {
    
    private static PlayerDatabase instance;
    
    public PlayerDatabase() {
        addVariable("table", "player_data");
    }
    
    public static PlayerDatabase getInstance() {
        if (instance == null) {
            instance = new PlayerDatabase();
        }
        return instance;
    }
    
    public void connect() {
        String host = Settings.Database.HOST;
        int port = Settings.Database.PORT;
        String database = Settings.Database.DATABASE;
        String user = Settings.Database.USER;
        String password = Settings.Database.PASSWORD;
        
        connect(host, port, database, user, password);
    }
    
    @Override
    protected void onConnectFinish() {
        Common.log("Player database connected successfully!");
    }
    
    @Override
    protected int getExpirationDays() {
        return 365; // Keep data for 1 year
    }
    
    @Override
    protected void onLoad(SerializedMap map, PlayerCache cache) {
        // Load basic data
        cache.setBalance(map.getDouble("balance", 0.0));
        cache.setLevel(map.getInteger("level", 1));
        cache.setExperience(map.getInteger("experience", 0));
        
        // Load collections
        cache.setHomes(map.getMap("homes", String.class, Location.class));
        cache.setFriends(map.getList("friends", UUID.class));
        
        // Load nested objects
        if (map.containsKey("statistics")) {
            cache.setStatistics(map.get("statistics", PlayerStatistics.class));
        }
        
        // Load timestamps
        cache.setFirstJoin(map.getLong("first_join", System.currentTimeMillis()));
        cache.setLastSeen(map.getLong("last_seen", System.currentTimeMillis()));
    }
    
    @Override
    protected SerializedMap onSave(PlayerCache cache) {
        // Don't save empty/default data
        if (cache.isDefault()) {
            return null; // This will delete the row
        }
        
        return SerializedMap.of(
            "balance", cache.getBalance(),
            "level", cache.getLevel(),
            "experience", cache.getExperience(),
            "homes", cache.getHomes(),
            "friends", cache.getFriends(),
            "statistics", cache.getStatistics(),
            "first_join", cache.getFirstJoin(),
            "last_seen", System.currentTimeMillis()
        );
    }
}

// Usage in your plugin
public class PlayerListener implements Listener {
    
    private final PlayerDatabase database = PlayerDatabase.getInstance();
    private final Map<UUID, PlayerCache> cache = new HashMap<>();
    
    @EventHandler
    public void onJoin(PlayerJoinEvent event) {
        Player player = event.getPlayer();
        PlayerCache playerCache = new PlayerCache();
        
        database.load(player, playerCache, () -> {
            // This runs on main thread after loading
            cache.put(player.getUniqueId(), playerCache);
            player.sendMessage("Welcome! Your balance: " + playerCache.getBalance());
        });
    }
    
    @EventHandler
    public void onQuit(PlayerQuitEvent event) {
        Player player = event.getPlayer();
        PlayerCache playerCache = cache.remove(player.getUniqueId());
        
        if (playerCache != null) {
            database.save(player, playerCache, () -> {
                Common.log("Saved data for " + player.getName());
            });
        }
    }
}

Best practices

Load and save operations are asynchronous. Use callbacks to ensure data is ready before using it.
// Good
database.load(player, cache, () -> {
    // Data is loaded, safe to use
    player.sendMessage("Balance: " + cache.getBalance());
});

// Bad
database.load(player, cache);
player.sendMessage("Balance: " + cache.getBalance()); // Cache might be empty!

Build docs developers (and LLMs) love