Модуль динамической базы данных NextLib предоставляет легковесную ORM-систему для работы с реляционными базами данных. Вместо ручного написания SQL-запросов, вы описываете сущности через обычные Java-классы, а библиотека автоматически создаёт таблицы и предоставляет Fluent API для CRUD-операций.
- MySQL - production-ready СУБД
- PostgreSQL - современная СУБД с расширенными возможностями
- SQLite - встроенная БД для небольших проектов
// Регистрация менеджера базы данных
DatabaseManager manager = new DatabaseManager();
// MySQL конфигурация
DatabaseConfig mysqlConfig = DatabaseConfig.builder(DatabaseType.MYSQL)
.host("localhost")
.port(3306)
.database("nexttraps")
.username("root")
.password("password")
.property("maximumPoolSize", "10")
.property("minimumIdle", "2")
.property("connectionTimeout", "30000")
.build();
DatabaseClient client = manager.register("main", mysqlConfig);
// SQLite конфигурация (для разработки)
DatabaseConfig sqliteConfig = DatabaseConfig.builder(DatabaseType.SQLITE)
.file(plugin.getDataFolder() + "/database.db")
.build();
DatabaseClient devClient = manager.register("dev", sqliteConfig);Создайте Java-класс с аннотацией @PrimaryKey для первичного ключа:
import io.github.chi2l3s.nextlib.api.database.dynamic.PrimaryKey;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.UUID;
@AllArgsConstructor
@Getter
public class PlayerEntity {
@PrimaryKey
private final UUID playerId;
private final String nickname;
private final String trapSkinId;
private final Integer coins;
private final Long lastLogin;
}Важно:
- Класс должен иметь конструктор, принимающий все поля в порядке объявления
- Поле с
@PrimaryKeyстановится первичным ключом таблицы - Если аннотация отсутствует, первое поле будет использовано как первичный ключ
- Используйте
finalполя для неизменяемых сущностей (рекомендуется)
DynamicDatabase database = new DynamicDatabase(client);
// Автоматическое имя таблицы: player_entitys
DynamicTable<PlayerEntity> players = database.register(PlayerEntity.class);
// Или с custom именем таблицы
DynamicTable<PlayerEntity> players = database.register("players", PlayerEntity.class);При регистрации автоматически создаётся таблица, если её нет.
UUID playerId = player.getUniqueId();
PlayerEntity entity = new PlayerEntity(
playerId,
"chi2l3s",
"default_trap",
1000,
System.currentTimeMillis()
);
int rows = players.create(entity);
// rows = 1 если успешноOptional<PlayerEntity> result = players.findFirst()
.where("playerId", playerId)
.execute();
result.ifPresent(entity -> {
player.sendMessage("Welcome back, " + entity.getNickname());
player.sendMessage("Coins: " + entity.getCoins());
});// Найти всех игроков с определённым скином
List<PlayerEntity> withTrap = players.findMany()
.where("trapSkinId", "lava_trap")
.execute();
// Найти всех богатых игроков
List<PlayerEntity> richPlayers = players.findMany()
.where("coins", 10000)
.execute();// Обновить количество монет
int updated = players.update()
.set("coins", 1500)
.where("playerId", playerId)
.execute();
// Обновить несколько полей
int updated = players.update()
.set("trapSkinId", "ice_trap")
.set("coins", 2000)
.set("lastLogin", System.currentTimeMillis())
.where("playerId", playerId)
.execute();// Найти игроков без скина
List<PlayerEntity> noSkin = players.findMany()
.where("trapSkinId", null)
.execute();
// Установить поле в NULL
players.update()
.set("trapSkinId", null)
.where("playerId", playerId)
.execute();| Java тип | SQL тип (MySQL) | SQL тип (PostgreSQL) | SQL тип (SQLite) |
|---|---|---|---|
| UUID | VARCHAR(36) | UUID | TEXT |
| String | TEXT | TEXT | TEXT |
| Integer | INT | INTEGER | INTEGER |
| Long | BIGINT | BIGINT | INTEGER |
| Double | DOUBLE | DOUBLE PRECISION | REAL |
| Boolean | BOOLEAN | BOOLEAN | INTEGER |
| byte[] | BLOB | BYTEA | BLOB |
// Сравнения с операторами
List<PlayerEntity> richPlayers = players.findMany()
.where("coins", QueryOperator.GREATER_THAN, 1000)
.execute();
List<PlayerEntity> lowLevel = players.findMany()
.where("level", QueryOperator.LESS_THAN_OR_EQUALS, 10)
.execute();
// LIKE для поиска по шаблону
List<PlayerEntity> searchResults = players.findMany()
.whereLike("nickname", "%chi%")
.execute();
// IN для списка значений
List<PlayerEntity> specificSkins = players.findMany()
.whereIn("trapSkinId", "ice_trap", "lava_trap", "poison_trap")
.execute();
// BETWEEN для диапазона
List<PlayerEntity> mediumPlayers = players.findMany()
.whereBetween("coins", 100, 1000)
.execute();
// IS NULL и IS NOT NULL
List<PlayerEntity> noSkin = players.findMany()
.whereIsNull("trapSkinId")
.execute();
List<PlayerEntity> withSkin = players.findMany()
.whereIsNotNull("trapSkinId")
.execute();
// Комбинации (все условия через AND)
List<PlayerEntity> result = players.findMany()
.where("coins", QueryOperator.GREATER_THAN, 500)
.whereLike("nickname", "Pro%")
.whereIsNotNull("trapSkinId")
.execute();
// SQL: SELECT * FROM players WHERE coins > ? AND nickname LIKE ? AND trapSkinId IS NOT NULLQueryOperator.EQUALS // =
QueryOperator.NOT_EQUALS // !=
QueryOperator.GREATER_THAN // >
QueryOperator.GREATER_THAN_OR_EQUALS // >=
QueryOperator.LESS_THAN // <
QueryOperator.LESS_THAN_OR_EQUALS // <=
QueryOperator.LIKE // LIKE
QueryOperator.NOT_LIKE // NOT LIKE
QueryOperator.IN // IN (list)
QueryOperator.NOT_IN // NOT IN (list)
QueryOperator.BETWEEN // BETWEEN x AND y
QueryOperator.IS_NULL // IS NULL
QueryOperator.IS_NOT_NULL // IS NOT NULLclient.withConnection(connection -> {
connection.setAutoCommit(false);
try {
// Создание нескольких записей
players.create(entity1);
players.create(entity2);
connection.commit();
return true;
} catch (Exception e) {
connection.rollback();
throw e;
}
});DynamicDatabase database = new DynamicDatabase(client);
DynamicTable<PlayerEntity> players = database.register(PlayerEntity.class);
DynamicTable<TrapEntity> traps = database.register(TrapEntity.class);
DynamicTable<SettingsEntity> settings = database.register(SettingsEntity.class);
// Получение зарегистрированной таблицы
DynamicTable<PlayerEntity> playersTable = database.get("player_entitys", PlayerEntity.class);DatabaseConfig config = DatabaseConfig.builder(DatabaseType.MYSQL)
.host("localhost")
.database("mydb")
.username("user")
.password("pass")
// Пул соединений
.property("maximumPoolSize", "20") // Максимум соединений
.property("minimumIdle", "5") // Минимум idle соединений
.property("connectionTimeout", "30000") // Таймаут подключения (мс)
.property("idleTimeout", "600000") // Таймаут idle (мс)
.property("maxLifetime", "1800000") // Максимальное время жизни (мс)
.property("leakDetectionThreshold", "60000") // Детекция утечек
// MySQL специфичные
.property("cachePrepStmts", "true")
.property("prepStmtCacheSize", "250")
.property("prepStmtCacheSqlLimit", "2048")
.build();DatabaseManager manager = new DatabaseManager();
try {
// Работа с БД
} finally {
manager.close(); // Закрывает все пулы соединений
}HikariCP уже настроен по умолчанию. Не создавайте новые соединения вручную:
// ❌ Плохо
try (Connection conn = DriverManager.getConnection(...)) { }
// ✅ Хорошо
DatabaseClient client = manager.getDefault();
client.withConnection(connection -> {
// Используйте connection
return result;
});// ❌ Плохо
PlayerEntity entity = players.findFirst()
.where("playerId", playerId)
.execute()
.get(); // Может бросить NoSuchElementException
// ✅ Хорошо
PlayerEntity entity = players.findFirst()
.where("playerId", playerId)
.execute()
.orElseThrow(() -> new PlayerNotFoundException(playerId));
// Или
players.findFirst()
.where("playerId", playerId)
.execute()
.ifPresent(entity -> {
// Работа с entity
});// ✅ Рекомендуется
@AllArgsConstructor
@Getter
public class PlayerEntity {
private final UUID playerId; // final поля
private final String nickname;
private final Integer coins;
}
// ❌ Не рекомендуется
public class PlayerEntity {
private UUID playerId; // mutable поля
private String nickname;
private Integer coins;
// setters...
}Хотя DynamicDatabase не создаёт индексы автоматически, вы можете создать их вручную:
client.execute(
"CREATE INDEX idx_player_nickname ON players(nickname)",
null
);- Ограниченная поддержка связей (relationships) - поддержка JOIN и связей находится в базовой стадии
Только базовые WHERE условия- ✅ ИСПРАВЛЕНО в v1.0.7: добавлены операторы>,<,>=,<=,LIKE,IN,BETWEEN- Ручные миграции - при изменении схемы нужно обновлять таблицы вручную (система миграций в разработке)
- Eager loading по умолчанию - все данные загружаются сразу
- Ограниченная поддержка вложенных объектов - сложные вложенные структуры требуют ручной обработки
При изменении структуры сущности:
// 1. Создайте миграцию вручную
client.execute("ALTER TABLE players ADD COLUMN premium BOOLEAN DEFAULT FALSE", null);
// 2. Обновите сущность
@AllArgsConstructor
@Getter
public class PlayerEntity {
@PrimaryKey
private final UUID playerId;
private final String nickname;
private final Integer coins;
private final Boolean premium; // Новое поле
}
// 3. Перерегистрируйте таблицу (опционально)
database.register(PlayerEntity.class);Убедитесь, что драйвер БД добавлен в зависимости:
dependencies {
implementation 'com.zaxxer:HikariCP:7.0.2'
implementation 'mysql:mysql-connector-java:8.0.33' // MySQL
implementation 'org.postgresql:postgresql:42.6.0' // PostgreSQL
implementation 'org.xerial:sqlite-jdbc:3.43.0.0' // SQLite
}Проверьте, что:
- Класс имеет конструктор со всеми полями
- Порядок параметров конструктора совпадает с порядком полей
- Типы параметров совпадают с типами полей
// ✅ Правильно
@AllArgsConstructor // Lombok генерирует конструктор
public class PlayerEntity {
private final UUID id;
private final String name;
}
// ❌ Неправильно - порядок не совпадает
public class PlayerEntity {
private final UUID id;
private final String name;
public PlayerEntity(String name, UUID id) { // Неверный порядок!
this.id = id;
this.name = name;
}
}- Добавьте индексы на часто используемые поля
- Увеличьте размер пула соединений
- Используйте batch операции для массовых вставок
// Batch insert
List<SqlConsumer<PreparedStatement>> binders = entities.stream()
.map(entity -> stmt -> bindEntity(stmt, entity))
.collect(Collectors.toList());
client.executeBatch(insertSql, binders);