3. Код
Небольшой бот для заметок: /note buy milk сохраняет заметку, /notes показывает ваши последние пять.
requirements.txt
aiogram>=3,<4
aiomysql
bot.py
import asyncio
import logging
import os
import sys
import aiomysql
from aiogram import Bot, Dispatcher
from aiogram.filters import Command, CommandObject
from aiogram.types import Message
dp = Dispatcher()
@dp.message(Command("note"))
async def note(message: Message, command: CommandObject, pool: aiomysql.Pool) -> None:
text = (command.args or "").strip()
if not text:
await message.answer("Write your note after the command, for example: /note buy milk")
return
if len(text) > 500:
await message.answer("A note can be up to 500 characters.")
return
async with pool.acquire() as conn:
async with conn.cursor() as cur:
await cur.execute(
"INSERT INTO notes (user_id, text) VALUES (%s, %s)",
(message.from_user.id, text),
)
await message.answer("Saved. Send /notes to see your last five.")
@dp.message(Command("notes"))
async def notes(message: Message, pool: aiomysql.Pool) -> None:
async with pool.acquire() as conn:
async with conn.cursor() as cur:
await cur.execute(
"SELECT text FROM notes WHERE user_id = %s ORDER BY id DESC LIMIT 5",
(message.from_user.id,),
)
rows = await cur.fetchall()
if not rows:
await message.answer("You have no notes yet.")
return
await message.answer("\n".join(f"- {text}" for (text,) in rows))
async def main() -> None:
# One pool for the whole bot. The values come from the Environment pane.
pool = await aiomysql.create_pool(
host=os.environ["DB_HOST"],
port=int(os.environ["DB_PORT"]),
user=os.environ["DB_USER"],
password=os.environ["DB_PASSWORD"],
db=os.environ["DB_NAME"],
autocommit=True,
maxsize=5,
)
async with pool.acquire() as conn:
async with conn.cursor() as cur:
await cur.execute(
"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))"
)
print("Database ready")
bot = Bot(os.environ["BOT_TOKEN"])
try:
# Every keyword passed here reaches the handlers that ask for it by name.
await dp.start_polling(bot, pool=pool)
finally:
pool.close()
await pool.wait_closed()
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO, stream=sys.stdout)
asyncio.run(main())
Что он делает: main() открывает пул, создаёт таблицу notes если её нет, и выводит Database ready до того, как бот начнёт запрашивать сообщения у Telegram. dp.start_polling(bot, pool=pool) служит собственным способом aiogram передавать объект: каждый дополнительный именованный аргумент, который вы передаёте, становится данными, которые aiogram отдаёт любому обработчику с параметром с таким именем, поэтому note() и notes() получают один и тот же пул. Когда бот останавливается, блок finally закрывает пул.
Плейсхолдеры %s подставляет драйвер, поэтому заметка вроде '); DROP TABLE notes; -- сохранится как обычный текст. В нашем тесте она сохранилась как строка 2, а таблица осталась на месте. Никогда не собирайте SQL через f-строку.
Храните 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 не удалось. aiomysql выбросил:
pymysql.err.DataError: (1264, "Out of range value for column 'user_id' at row 1")
Столбец BIGINT сохранил его, как и -1001234567890. Берите обычный BIGINT, а не BIGINT UNSIGNED, чтобы поместились и ID чатов.
4. Запустите
- Загрузите
bot.py и requirements.txt в разделе Файлы.
- Нажмите Запустить и откройте Консоль. При запуске устанавливаются пакеты из
requirements.txt, затем появляются [PlayNHost] Starting your bot, Database ready и Run polling for bot @your_bot. Последняя строка переключает статус на вкладке Обзор на Онлайн в Telegram.
- В Telegram отправьте боту
/note buy milk, затем /notes. Чтобы посмотреть строки, откройте страницу базы и нажмите Открыть в phpMyAdmin.
Начиная со второго запуска консоль также показывает Warning: Table 'notes' already exists, перед которым стоит путь к файлу cursors.py из aiomysql. Это замечание MariaDB для CREATE TABLE IF NOT EXISTS, переданное драйвером. Всё в порядке.
В нашем тесте заглушка Telegram отправила эти команды и получила такие ответы:
| Отправлено | Ответ |
/note buy milk | Saved. Send /notes to see your last five. |
/note | Write 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. |
Много сообщений сразу
aiogram обрабатывает каждое входящее обновление как отдельную задачу, поэтому несколько команд могут выполнять свои запросы одновременно. В нашем тесте 4 команды, пришедшие вместе, обрабатывались примерно по 50 мс каждая. maxsize=5 ограничивает пул 5 подключениями; шестой запрос ждёт свободного, а не открывает новое. После теста у бота было открыто 2 подключения.
Ловушка autocommit
aiomysql открывает подключения с выключенным autocommit. Тогда INSERT остаётся внутри открытой транзакции. Когда подключение возвращается в пул с ещё открытой транзакцией, aiomysql закрывает его, а MariaDB выбрасывает неподтверждённую строку. В нашем тесте строку, вставленную в пул без autocommit=True увидели 0 других подключений. Либо передайте autocommit=True в create_pool, как выше, либо вызывайте await conn.commit() после записей.
Неудачный запрос не останавливает бота
В нашем тесте мы удалили таблицу, пока бот работал, и отправили /notes. aiogram записал ошибку в лог с трассировкой, заканчивающейся на pymysql.err.ProgrammingError: (1146, "Table '...notes' doesn't exist"), не отправил ответ на эту команду и продолжил работать: когда таблица вернулась, следующий /note был сохранён. Тот, кто отправил команду, ничего не видит, поэтому, если запрос может завершиться ошибкой, перехватывайте её в обработчике и отвечайте коротким сообщением.
Тихая ночь не ломает пул
Наш сервер базы данных закрывает подключение, которое простаивало 8 часов. Бот, который так долго не получает сообщений, например ночью, обнаруживает свои подключения в пуле закрытыми. aiomysql проверяет это, когда обработчик запрашивает подключение, и открывает новое вместо закрытого. Мы проверили это с лимитом простоя, сниженным до 5 секунд: после 7 секунд тишины следующий /note был сохранён без ошибок.
Лимиты, важные для вашего пула
- 20 одновременных подключений на пользователя базы данных. Считается каждый процесс, который использует тот же логин. Подключение 21 отклоняется с
(1226, "User '...' has exceeded the 'max_user_connections' resource (current value: 20)"). maxsize=5 держит бота намного ниже этого предела.
- Запрос, который выполняется дольше 120 секунд, останавливается. MariaDB завершает его с ошибкой 1969,
Query execution was interrupted (max_statement_time exceeded). В одном из наших прошлых тестов, на аккаунте, настроенном так же, это произошло через 120,03 секунды. Запросы бота обычно занимают миллисекунды, так что это касается только запроса, ушедшего вразнос.
- 500 МБ на базу. На странице Базы данных видно, сколько занято. При 500 МБ запись останавливается, пока вы не удалите строки.
- Резервные копии сервера не включают базу данных. Время от времени экспортируйте её из phpMyAdmin.
Ошибки и что они значат
Вот ошибки, которые aiomysql выдал в нашем тесте, записанные так, как они завершают трассировку в консоли (они приходят из PyMySQL, который aiomysql использует под капотом):
pymysql.err.OperationalError: (1045, "Access denied for user ..."): неверный DB_USER или неверный DB_PASSWORD. После Сбросить пароль на карточке Подключение, старый пароль сразу перестаёт работать: замените DB_PASSWORD и перезапустите бота.
pymysql.err.OperationalError: (2003, "Can't connect to MySQL server on ..."): неверный хост или порт, либо подключение извне PlayNHost. База принимает подключения только с серверов PlayNHost и из phpMyAdmin, поэтому то же самое вы получите и на своём компьютере.
pymysql.err.OperationalError: (1226, ... 'max_user_connections' ...): открыто слишком много подключений, см. лимиты выше. Например, вторая копия бота или пул, открываемый на каждое сообщение.
pymysql.err.DataError: (1264, "Out of range value for column 'user_id' at row 1"): столбец с ID имеет тип INT. Смените его на BIGINT.
pymysql.err.ProgrammingError: (1146, "Table '...' doesn't exist"): таблицы нет. Код выше создаёт её при запуске; если вы создали её вручную, проверьте имя.
Если переменной нет в разделе Окружение бот останавливается ещё до подключения, с KeyError: 'DB_HOST' или именем недостающей переменной, потому что код читает каждое значение через os.environ[...]. Добавьте её и перезапустите бота: переменные применяются при следующем запуске.
Неверный токен проявляется после части с базой. С нашим поддельным токеном против настоящего Telegram бот вывел Database ready, затем остановился с aiogram.exceptions.TelegramUnauthorizedError: Telegram server says - Unauthorized. Скопируйте токен заново у @BotFather и вставьте его на странице Бот, раздел Telegram. Другие ошибки Telegram: Как исправить ошибку 409 Conflict у Telegram-бота.
Сколько памяти он занимает
В нашем тесте работающий бот занял 174 МБ памяти (resident) после ответа на 8 команд, с открытым пулом, в контейнере, ограниченном 512 МБ, как наш самый маленький тариф для ботов. Большая часть приходится на сам aiogram: в нашем прошлом тесте хостинга один aiogram показал 173 МБ (тот бот остановился на отклонённом токене, не успев обработать ни одного сообщения, так что цифры близки, но это не точное сравнение). Установка aiogram и aiomysql через pip с нуля достигла пика в 163 МБ, включая файловый кэш. Это оставляет место для вашего кода на самом маленьком тарифе. Руководство по хостингу сравнивает библиотеки: python-telegram-bot или aiogram на PlayNHost.
То же самое с другой библиотекой
Нужен сервер для вашего бота Telegram с включённой базой данных? Смотреть тарифы для ботов Telegram.