Repository navigation
Databases
HLib включает HikariCP, MySQL Connector/J, MariaDB driver, Jedis и Commons Pool. Эти библиотеки уже находятся в HLib.jar, поэтому отдельное встраивание в ваш plugin JAR не требуется.
| Библиотека | Версия в сборке |
|---|---|
| HikariCP | 4.0.3 |
| MySQL Connector/J | 8.0.31 |
| MariaDB Connector/J | 3.1.4 |
| Jedis | 4.2.2 |
| Commons Pool | 2.11.1 |
Все DB-вызовы HLib синхронные. Планирование вне main thread — ответственность вызывающего кода.
HLib сам не открывает SQL- или Redis-подключения. Их создаёт и настраивает использующий библиотеку плагин; для обычных утилит база данных не нужна.
MySQL mysql = new MySQL(
"127.0.0.1:3306",
"minecraft",
"plugin",
"password",
null,
"mariadb",
300,
5,
1800,
10
);
if (!mysql.isEnabled()) {
throw new IllegalStateException("Подключение к MariaDB не создано");
}Аргументы конструктора:
| Аргумент | Значение | Default при null/пустом значении |
|---|---|---|
address |
host:port |
обязателен |
database |
имя базы | обязателен |
username, password
|
учётные данные | передаются как есть |
url |
JDBC template с {driver}, {address}, {database}
|
стандартный JDBC URL |
driver |
mysql или mariadb
|
mysql |
keepaliveTime |
секунды | 300 |
connectionTimeout |
секунды | 5 |
maxLifetime |
секунды | 1800 |
poolSize |
maximum pool size | 10 |
Отсутствующие параметры JDBC URL дополняются connectTimeout=5000 и socketTimeout=60000 в миллисекундах для MySQL и MariaDB. Первый ограничивает подключение TCP, второй — ожидание данных из socket, включая ответ SQL и commit. Это отдельные настройки от connectionTimeout конструктора, который ограничивает только ожидание свободного connection в HikariCP. Socket timeout не задаёт общий deadline всей транзакции и не отменяет блокирующие callbacks вызывающего кода.
Явные параметры URL сохраняются, включая повторения и значение 0, отключающее соответствующий timeout. Имена сопоставляются по правилам используемого драйвера: MariaDB без учёта регистра, MySQL с точным написанием connectTimeout/socketTimeout; исходный URL не переписывается. Для пользовательского URL без таймаутов действуют те же defaults. Запросы и транзакции автоматически не повторяются: при обрыве во время commit результат может быть неопределённым. Если требуются длительные операции, задайте подходящий socketTimeout явно; значение по умолчанию оставляет запас для поддерживаемого GET_LOCK до 30 секунд. Сетевые ограничения описаны в MariaDB Connector/J и MySQL Connector/J.
Если HLib-компоненты должны использовать этот pool, зарегистрируйте его:
Tools.setMysqlInstance(mysql);Это один глобальный пул HLib. Не заменяйте чужое подключение, если нескольким плагинам нужен общий доступ. Для собственных таблиц можно хранить MySQL локально и вообще не регистрировать его в Tools.
Scheduler.doAsync(() -> {
AtomicReference<String> name = new AtomicReference<>();
boolean success = mysql.query("SELECT name FROM players WHERE id = ?")
.setArgs(playerId)
.setResult(resultSet -> {
if (resultSet.next()) {
name.set(resultSet.getString("name"));
}
})
.execute();
Scheduler.Bukkit.doSync(() -> {
String result = success ? name.get() : null;
Informer.send(player, Informer.component("&7Результат: &f" + result));
});
});Callback setResult выполняется в том же потоке, что и запрос, пока ResultSet открыт. Не сохраняйте ResultSet для последующего использования.
Optional<Integer> id = mysql.query("INSERT INTO players (name) VALUES (?)")
.setArgs(name)
.executeInsert();Пустой Optional означает либо отсутствие generated key, либо ошибку. Эти состояния API не различает.
int[] result = mysql.query("INSERT INTO player_tags (player_id, tag) VALUES (?, ?)")
.addBatch(10, "builder")
.addBatch(11, "admin")
.executeBatch();Batch выполняется без явной транзакции. Пустой массив означает и пустой batch, и ошибку.
Для атомарного изменения нескольких записей используйте MySQL.transaction(...). Callback получает один MySQL.Transaction, связанный с одним JDBC connection и текущим потоком:
long purchaseId = mysql.transaction(transaction -> {
int changed = transaction.update(
"UPDATE wallets SET balance = balance - ? WHERE name = ? AND balance >= ?",
price, name, price
);
if (changed != 1) throw new java.sql.SQLException("Недостаточный баланс или неизвестный игрок");
return transaction.insert(
"INSERT INTO purchases (name, price) VALUES (?, ?)",
name, price
);
});| Метод | Результат |
|---|---|
transaction.update(sql, args...) |
Количество изменённых строк как int
|
transaction.insert(sql, args...) |
Generated key как long; требует ровно одну вставленную строку и непустой ключ |
transaction.query(sql, reader, args...) |
Значение, возвращённое reader.apply(ResultSet)
|
mysql.transaction(action) |
Значение callback после успешного commit |
Оба callback используют MySQL.ThrowingFunction<T, R> и могут выбрасывать SQLException. ResultSet открыт только во время reader; сохраняйте извлечённые значения, а не JDBC-объекты. При выходе из основного callback транзакция закрывается для последующих вызовов, даже если её объект был сохранён. Использование из другого потока запрещено.
При нормальном возврате callback выполняется commit. SQLException, RuntimeException и Error приводят к попытке rollback и передаются вызывающему коду. Ошибка rollback добавляется к исходной как suppressed; statements, result sets и connection закрываются автоматически. Автоматических повторов нет. Ошибка связи во время commit может означать неопределённый результат на стороне сервера: не повторяйте денежную операцию вслепую.
Все вызовы остаются синхронными; выполняйте их через async Scheduler. Не вызывайте команды Bukkit, внешние HTTP API или выдачу предметов внутри callback: SQL rollback не отменяет эти действия. Транзакции гарантируют атомарность только для transactional таблиц (например, InnoDB); DDL и другие команды с неявным commit в callback не допускаются. Вложенный mysql.query(...) открывает отдельное соединение и не входит в текущую транзакцию — используйте только переданный объект Transaction.
Новое API не подавляет и не логирует SQL-ошибки: обработка и безопасное сообщение об ошибке остаются ответственностью вызывающего кода. Старый builder MySQL.Query сохраняет прежнее поведение.
HLib 7.9 добавляет mysql.lockedTransaction(lockName, timeoutSeconds, callback). API получает MySQL/MariaDB GET_LOCK на том же выделенном connection до callback и освобождает RELEASE_LOCK во внешнем finally после commit либо rollback. Это подходит для общей квоты, которую одновременно применяют несколько сервисов.
mysql.lockedTransaction("example:registration", 5, transaction -> {
long count = transaction.query("SELECT COUNT(*) FROM registration_event WHERE registered_at >= ?", result -> {
result.next();
return result.getLong(1);
}, since);
if (count >= maximum) throw new java.sql.SQLException("Registration quota reached");
transaction.update("INSERT INTO registration_event (registered_at) VALUES (?)", now);
return null;
});Имя допускает 1–64 символа a-z A-Z 0-9 : _ -, ожидание — 0–30 секунд. Timeout либо ошибка получения lock не запускает callback. Ошибка освобождения закрывает физическое соединение через abort, чтобы блокировка не осталась в пуле. Первичная ошибка сохраняется; ошибки rollback/release/abort/close добавляются как suppressed. Нельзя вручную освобождать lock или выполнять COMMIT внутри callback. Обычный transaction сохраняет прежнюю семантику. HTTP-политику и другие внешние чтения готовьте до захвата блокировки, а актуальные SQL-счётчики читайте внутри неё.
public enum Tables implements MySQLTable {
PLAYER_TAGS("CREATE TABLE IF NOT EXISTS player_tags (player_id INT NOT NULL, tag VARCHAR(64) NOT NULL)");
private final String value;
Tables(String value) {
this.value = value;
}
@Override
public String getValue() {
return value;
}
}mysql.query(Tables.PLAYER_TAGS).setQuiet(false).execute();Overload query(MySQLTable) включает quiet-режим автоматически. setQuiet(false) нужен, чтобы ошибка DDL была видна в логе и execute() вернул false.
При использовании встроенных моделей после регистрации глобального пула отдельно инициализируйте нужные таблицы:
Scheduler.doAsync(HPlayer::initMySQL);
Cooldowner.initMySQL();HPlayer.initMySQL() синхронно создаёт и загружает hlib_player; поэтому в примере он запускается вне main thread. Cooldowner.initMySQL() сам планирует асинхронную загрузку hlib_cooldown. Не используйте эти подсистемы до завершения их загрузки; единого callback готовности нет.
HPlayer.createHPlayer(name) возвращает известный профиль из кэша; при промахе выполняет INSERT имени с no-op при дубликате и SELECT id/name FOR UPDATE в одной SQL-транзакции. Профиль, созданный сайтом или другим потоком, использует реальный ID и сохранённый регистр имени; незавершённая внешняя вставка сначала должна commit/rollback. Объект публикуется в кэше только после успешного commit. Ошибка SQL сохраняет исходную причину в IllegalStateException и не добавляет ложный профиль. getHPlayer(name) и getHPlayer(id) остаются cache-only и могут вернуть null.
Создание и публикация синхронизированы общим закрытым lock; одинаковая пара ID/имя при повторной загрузке принимается идемпотентно, фабрики возвращают уже известный объект. Public-конструктор также синхронизирует обе карты и может публиковать уже подтверждённую транзакцией пару без нового SQL. Конфликт ID с другим именем или имени с другим ID отклоняется без подмены кэша. Это не API переименования: уже закэшированный профиль автоматически не перечитывается при внешнем SQL rename/delete. HLib не регистрирует обработчики входа игроков для заполнения модели — это делает использующий её плагин. Все операции SQL синхронны; вызывающий код обязан использовать рабочий поток и не запускать parent INSERT отдельно от зависимых записей.
-
MySQL.Queryизменяемый и не должен использоваться одновременно из нескольких потоков. - SELECT требует
setResult; иначе HLib вызываетexecuteUpdate. - Query с
setResultвсегда используетexecuteQuery, даже если SQL является UPDATE. -
query(sql, true).execute()возвращаетtrueи приSQLException; quiet-режим не подходит для контроля успеха. - Batch не открывает явную транзакцию.
- Аргументы SQL-ошибки выводятся в лог. Не передавайте секреты как параметры запросов, если их нельзя логировать.
-
setEnabled(true)не восстанавливает уже закрытый pool. - Вызывайте
mysql.shutdown()при остановке владельца подключения.
Redis redis = new Redis("127.0.0.1:6379", "");
try (Jedis jedis = redis.getPool().getResource()) {
String pong = jedis.ping();
if (!"PONG".equals(pong)) {
throw new IllegalStateException("Redis не ответил PONG");
}
}
Tools.setRedisInstance(redis);Constructor не проверяет соединение. Выполните PING самостоятельно. IPv6-адреса текущим parser не поддерживаются.
Scheduler.doAsync(() -> redis.publish("example:updates", "reload"));При ошибке publish подавляет исключение и закрывает Redis. Вызывающий код не получает результат публикации.
Jedis.subscribe блокирует поток до unsubscribe. Используйте отдельный executor, а не main thread и не общий короткоживущий task.
ExecutorService subscriberExecutor = Executors.newSingleThreadExecutor();
subscriberExecutor.execute(() -> {
try (Jedis jedis = redis.getPool().getResource()) {
jedis.subscribe(new Redis.PubSub() {
@Override
public void onMessage(String channel, String message) {
Informer.send(channel + ": " + message);
}
}, "example:updates");
}
});При shutdown вызовите:
redis.shutdown();
subscriberExecutor.shutdown();Все Redis.PubSub хранятся в одном глобальном set и не удаляются автоматически. Shutdown одного Redis вызывает unsubscribe для активных subscribers всех экземпляров; предпочтительно иметь один общий Redis instance.
На Bukkit HLib закрывает зарегистрированные глобальные SQL/Redis-пулы при собственном отключении. На Velocity такого закрытия в entrypoint нет; владелец подключений должен закрывать их сам. Отдельные, не зарегистрированные в Tools экземпляры всегда закрывает их владелец.