НОВОЕ 📢 Ограниченное стартовое предложение: скидка до 70% на первый месяц для любой игры!Посмотреть тарифы

Как использовать базу данных MySQL с ботом grammY (mysql2)

Хостинг Telegram-ботовUpdated Nov 6, 202611 min read

Коротко. Добавьте mysql2 в своего бота на grammY, при запуске бота создайте один пул соединений из переменных окружения, создайте таблицы до bot.start()и выполняйте каждый запрос через пул. Используйте плейсхолдеры ? и храните ID Telegram как BIGINT, а не как INT. В PlayNHost каждый тариф для бота включает одну базу данных MySQL: создайте её на странице Базы данных , вставьте её данные в раздел Окружение , и код ниже заработает как есть. База принимает подключения с ваших серверов PlayNHost и из phpMyAdmin, но не с вашего компьютера. Мы проверили этот код 6 ноября 2026 года с grammY 1.46.0, mysql2 3.24.6 и Node.js 22, на MariaDB 10.11 с учётной записью базы, настроенной как у нас (20 соединений, лимит запроса 120 секунд), и с заменой сервера Bot API Telegram: 8 команд на входе, 8 ответов на выходе, заметки сохранены в базе. Настоящий вход в Telegram в тест не входил.

Что вам нужно

1. Создайте базу данных

  1. Откройте сервер своего бота в панели и нажмите Базы данных.
  2. Нажмите Новая база данных. Поле Метка необязательно; Будет создана как показывает полное имя. Нажмите Создать базу данных.
  3. Откроется страница новой базы (позже нажмите на неё в разделе Ваши базы данных). Её карточка Подключение показывает Хост, Порт, База данных, Имя пользователя и Пароль. В разделе Готовый конфиг, в поле Конфиг для есть сниппет Node.js (mysql2) с уже подставленными теми же значениями.

2. Внесите данные в раздел Окружение

  1. Откройте страницу Бот , раздел Окружениеи нажмите Вставить файл .env.
  2. Вставьте эти пять строк со значениями из карточки Подключение :
    DB_HOST=your-host
    DB_PORT=3306
    DB_NAME=your-database
    DB_USER=your-username
    DB_PASSWORD=your-password
  3. DB_PASSWORD получает значок Секрет : она сохраняется только для записи и больше никогда не показывается. Нажмите Импортировать 5 переменных.

Используйте настоящий порт из карточки Подключение . Токена бота в этих строках нет: токен, который вы сохранили на странице Бот , в разделе Telegram, уже попадает в ваш код как TELEGRAM_BOT_TOKEN, то есть под именем, которое читает код ниже (а также как BOT_TOKEN, TOKEN, TELEGRAM_TOKEN и TELOXIDE_TOKEN). Если в вашем файле .env есть строка с токеном, диалог её пропускает. Подробнее об этом разделе: Как задать переменные окружения и секреты для Discord-бота.

3. Код

Небольшой бот для заметок: /note buy milk сохраняет заметку, /notes показывает ваши последние пять.

package.json

{
  "name": "my-grammy-bot",
  "private": true,
  "main": "index.js",
  "dependencies": {
    "grammy": "^1.46.0",
    "mysql2": "^3.24.6"
  }
}

index.js

const { Bot } = require("grammy");
const mysql = require("mysql2/promise");

// One pool for the whole bot. The values come from the Environment pane.
const pool = mysql.createPool({
  host: process.env.DB_HOST,
  port: Number(process.env.DB_PORT),
  database: process.env.DB_NAME,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  connectionLimit: 5,
});

const bot = new Bot(process.env.TELEGRAM_BOT_TOKEN);

bot.command("note", async (ctx) => {
  const text = ctx.match.trim();
  if (!text) return ctx.reply("Write your note after the command, for example: /note buy milk");
  if (text.length > 500) return ctx.reply("A note can be up to 500 characters.");
  await pool.execute("INSERT INTO notes (user_id, text) VALUES (?, ?)", [ctx.from.id, text]);
  await ctx.reply("Saved. Send /notes to see your last five.");
});

bot.command("notes", async (ctx) => {
  const [rows] = await pool.execute(
    "SELECT text FROM notes WHERE user_id = ? ORDER BY id DESC LIMIT 5",
    [ctx.from.id],
  );
  if (rows.length === 0) return ctx.reply("You have no notes yet.");
  await ctx.reply(rows.map((row) => `- ${row.text}`).join("\n"));
});

// Without this, an error in one command stops the whole bot.
bot.catch((err) => {
  console.error(`Error while handling update ${err.ctx.update.update_id}:`, err.error);
});

async function main() {
  await pool.query(
    "CREATE TABLE IF NOT EXISTS notes (" +
      "id INT AUTO_INCREMENT PRIMARY KEY, " +
      "user_id BIGINT NOT NULL, " +
      "text VARCHAR(500) NOT NULL, " +
      "created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
      "KEY user_id (user_id))",
  );
  console.log("Database ready");
  // bot.start() only returns when the bot stops, so the ready line goes in onStart.
  await bot.start({ onStart: (me) => console.log(`Logged in as @${me.username}`) });
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});

Что он делает: main() создаёт таблицу notes , если её нет, и выводит Database ready до того, как bot.start() начнёт запрашивать у Telegram сообщения. Пул создаётся один раз, в начале файла, и его использует каждая команда: pool.execute() берёт соединение, выполняет запрос и возвращает соединение. ctx.match содержит текст после команды: grammY убирает пробелы перед ним и сохраняет остальное как набрано, включая переводы строк. bot.catch() здесь важен: в документации grammY сказано, что без обработчика ошибок ошибка в хендлере останавливает бота, запущенного через bot.start(), а ошибка базы данных как раз такая.

Плейсхолдеры ? отправляют заметку в базу отдельно от SQL, поэтому заметка вроде '); DROP TABLE notes; -- сохраняется как обычный текст. В нашем тесте она сохранилась как строка 2, а таблица осталась на месте. Никогда не собирайте SQL строкой-шаблоном.

Храните ID Telegram как BIGINT

В документации Bot API Telegram сказано, что ID пользователя или чата «может содержать более 32 значащих бит», но не более 52, так что 64-битного целого достаточно. На странице об ID даны диапазоны: ID пользователей доходят до 1.099.511.627.775, а ID групп, супергрупп и каналов отрицательные. Супергруппы и каналы лежат в диапазоне от -1.000.000.000.001 до -1.997.852.516.352 (например, -1001234567890). Тип INT в MySQL заканчивается на 2.147.483.647. В нашем тесте, с тем же строгим режимом SQL, что и на нашем сервере баз данных, сохранить ID пользователя 7.000.000.001 в столбце INT не удалось. mysql2 вернул код ошибки ER_WARN_DATA_OUT_OF_RANGE (errno 1264) с таким сообщением:

Out of range value for column 'user_id' at row 1

Столбец BIGINT его сохранил, как и -1001234567890 . Используйте обычный BIGINT, а не BIGINT UNSIGNED, чтобы подходили и ID чатов.

4. Запустите

  1. Загрузите index.js и package.json в разделе Файлы. Не загружайте node_modules.
  2. Нажмите Запустить и откройте Консоль. При запуске устанавливаются пакеты из package.json, затем появляются [PlayNHost] Starting your bot, Database ready и Logged in as @your_bot. Эта последняя строка переключает статус в разделе Обзор на Онлайн в Telegram.
  3. В Telegram отправьте своему боту /note buy milk, затем /notes. Чтобы посмотреть строки, откройте страницу базы и нажмите Открыть в phpMyAdmin.

В нашем тесте замена Telegram отправила эти команды и получила эти ответы:

ОтправленоОтвет
/note buy milkSaved. Send /notes to see your last five.
/noteWrite your note after the command, for example: /note buy milk
/note и 501 символA note can be up to 500 characters.
/notesСохранённые заметки, сначала новые
/notes от второго пользователяYou have no notes yet.

Много сообщений одновременно

В документации grammY сказано, что bot.start() обрабатывает все обновления последовательно: одна команда завершается до начала следующей. В нашем тесте после ответов на 8 команд у бота было 1 открытое соединение. connectionLimit: 5 важен, когда ваш код выполняет запросы параллельно или когда вы переходите на grammY runner, который обрабатывает обновления одновременно: пул никогда не открывает больше 5 соединений, а лишний запрос ждёт свободного.

Неудачный запрос не останавливает бота

В нашем тесте мы удалили таблицу, пока бот работал, и отправили /notes. bot.catch() вывел Error while handling update 500001: Error: Table '...notes' doesn't exist с code: 'ER_NO_SUCH_TABLE', бот не отправил ответ на эту команду и продолжил работать: когда таблица вернулась, следующая команда /note сохранилась. Тот, кто отправил команду, ничего не видит, поэтому, если запрос может завершиться ошибкой, оберните его в хендлере в try/catch и ответьте коротким сообщением.

Тихая ночь не ломает пул

Наш сервер баз данных закрывает соединение, простаивавшее 8 часов. Бот, который так долго не получает сообщений, например ночью, обнаруживает, что соединения в пуле закрыты. Пул mysql2 справляется с этим сам. Мы проверили это с лимитом простоя, сниженным до 5 секунд: после 7 тихих секунд следующая команда /note сохранилась без ошибки.

Лимиты, важные для вашего пула

  • 20 соединений одновременно на пользователя базы. Учитывается каждый процесс, использующий тот же логин. Соединение 21 отклоняется с ошибкой ER_USER_LIMIT_REACHED. В нашем тесте пул с connectionLimit: 30 завершился этой ошибкой, когда одновременно выполнялись 30 запросов. Пул из 5 держит бота заметно ниже лимита.
  • Запрос, который выполняется дольше 120 секунд, останавливается. MariaDB завершает его с ошибкой 1969, Query execution was interrupted (max_statement_time exceeded). В нашем более раннем тесте, на учётной записи с такой же настройкой, это произошло через 120,03 секунды. Запросы бота обычно занимают миллисекунды, так что это касается только запроса, вышедшего из-под контроля.
  • 500 МБ на базу данных. Страница Базы данных показывает, сколько занято. При 500 МБ запись останавливается, пока вы не удалите строки.
  • Резервные копии сервера не включают базу данных. Время от времени экспортируйте её из phpMyAdmin.

Ошибки и что они значат

Вот коды ошибок, которые вернул mysql2 в нашем тесте:

  • ER_ACCESS_DENIED_ERROR ... Access denied for user: неверный DB_USER или DB_PASSWORD. После Сбросить пароль на карточке Подключение старый пароль сразу перестаёт работать: замените DB_PASSWORD и перезапустите бота.
  • ECONNREFUSED: по этому хосту и порту соединение никто не принял. Проверьте DB_HOST и DB_PORT. Отсутствующий DB_HOST тоже приводит сюда, потому что mysql2 тогда подключается к localhost, где никакая база не запущена: переменной нет в разделе Окружение, или бота не перезапустили после её добавления.
  • ENOTFOUND: DB_HOST написан с ошибкой, поэтому такого имени не существует.
  • ETIMEDOUT: никто не ответил вовремя. Что бы ни говорило сообщение, подключение с вашего компьютера не удастся: база принимает подключения только с серверов PlayNHost и из phpMyAdmin.
  • ER_USER_LIMIT_REACHED ... has exceeded the 'max_user_connections' resource (current value: 20): открыто слишком много соединений, см. лимиты выше. Например, вторая копия бота или пул, создаваемый на каждое сообщение.
  • ER_WARN_DATA_OUT_OF_RANGE (errno 1264, Out of range value for column 'user_id' at row 1): столбец с ID имеет тип INT. Измените его на BIGINT.
  • ER_NO_SUCH_TABLE (errno 1146): таблицы нет. Код выше создаёт её при запуске; если вы создали её вручную, проверьте имя.

Неверный токен проявляется после части с базой данных. С нашим поддельным токеном против настоящего Telegram бот вывел Database ready, а затем остановился примерно через четверть секунды с ошибкой GrammyError: Call to 'deleteWebhook' failed! (401: Unauthorized). Скопируйте токен заново у @BotFather и вставьте его на странице Бот , в разделе Telegram. Другие ошибки Telegram: Как исправить ошибку 409 Conflict у Telegram-бота.

Сколько памяти он использует

В нашем тесте работающий бот использовал 67,6 МБ памяти (resident) после ответов на 8 команд, с открытым пулом, в контейнере с лимитом 512 МБ, как у нашего самого маленького тарифа для ботов. В нашем более раннем тесте хостинга один grammY занимал 60 МБ (тот бот остановился на отклонённом токене, не обработав ни одного сообщения), так что разница, около 8 МБ, приходится на mysql2, пул и обработанные команды. Установка grammY и mysql2 через npm с нуля достигла пика 140 МБ, включая файловый кэш.

То же самое с другой библиотекой

Нужен сервер для вашего Telegram-бота с включённой базой данных? Посмотреть тарифы для Telegram-ботов.

Ваш бот уже у другого хостинга? Перенесите его к нам

ПОСЛЕДНИЕ БЛОГИ