3. Der Code
Ein kleiner Notizen-Bot: /note buy milk speichert eine Notiz, /notes zeigt deine letzten fünf Notizen.
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())
Was er tut: main() öffnet den Pool, legt die notes Tabelle an, falls sie fehlt, und gibt Database ready aus, bevor der Bot bei Telegram Nachrichten abholt. dp.start_polling(bot, pool=pool) ist die eigene Art von aiogram, ein Objekt zu teilen: Jedes zusätzliche Schlüsselwort, das du übergibst, wird zu Daten, die aiogram an jeden Handler mit einem Parameter dieses Namens weitergibt, sodass note() und notes() denselben Pool bekommen. Wenn der Bot stoppt, schließt der finally-Block den Pool.
Die %s-Platzhalter werden vom Treiber eingesetzt, deshalb wird eine Notiz wie '); DROP TABLE notes; -- als normaler Text gespeichert. In unserem Test landete sie als Zeile 2, und die Tabelle blieb bestehen. Baue das SQL nie mit einem f-String zusammen.
Telegram-IDs als BIGINT speichern
In der Bot-API-Dokumentation von Telegram steht, eine Nutzer- oder Chat-ID „kann mehr als 32 signifikante Bits haben“, aber höchstens 52, daher ist eine 64-Bit-Ganzzahl sicher. Die Seite zu den IDs nennt die Bereiche: Nutzer-IDs reichen bis 1.099.511.627.775, während Gruppen-, Supergruppen- und Kanal-IDs negativ sind. Supergruppen und Kanäle laufen von -1.000.000.000.001 bis -1.997.852.516.352 (zum Beispiel -1001234567890). In MySQL endet INT bei 2.147.483.647 Schluss. In unserem Test schlug das Speichern der Nutzer-ID 7.000.000.001 in einer INT-Spalte fehl, mit demselben strikten SQL-Modus wie auf unserem Datenbankserver. aiomysql meldete:
pymysql.err.DataError: (1264, "Out of range value for column 'user_id' at row 1")
Eine BIGINT-Spalte speicherte sie, und -1001234567890 ebenfalls. Nimm ein einfaches BIGINT, nicht BIGINT UNSIGNED, damit auch Chat-IDs hineinpassen.
4. Ausführen
- Lade
bot.py und requirements.txt unter Dateien hoch.
- Drücke Starten und öffne Konsole. Beim Start werden die Pakete aus
requirements.txt installiert, danach siehst du [PlayNHost] Starting your bot, Database ready und Run polling for bot @your_bot. Die letzte Zeile setzt den Status unter Übersicht auf Online in Telegram.
- Sende deinem Bot in Telegram
/note buy milk, dann /notes. Um die Zeilen anzusehen, öffne die Seite der Datenbank und klicke auf In phpMyAdmin öffnen.
Ab dem zweiten Start zeigt die Konsole außerdem Warning: Table 'notes' already exists, mit dem Pfad von aiomysqls cursors.py davor. Das ist der Hinweis von MariaDB zu CREATE TABLE IF NOT EXISTS, vom Treiber weitergereicht. Es ist nichts kaputt.
In unserem Test schickte der Telegram-Ersatz diese Befehle und bekam diese Antworten:
| Gesendet | Antwort |
/note buy milk | Saved. Send /notes to see your last five. |
/note | Write your note after the command, for example: /note buy milk |
/note und 501 Zeichen | A note can be up to 500 characters. |
/notes | Die gespeicherten Notizen, neueste zuerst |
/notes von einem zweiten Nutzer | You have no notes yet. |
Viele Nachrichten gleichzeitig
aiogram verarbeitet jedes eingehende Update als eigene Task, daher können mehrere Befehle ihre Abfragen gleichzeitig ausführen. In unserem Test wurden 4 Befehle, die zusammen eintrafen, jeweils in etwa 50 ms verarbeitet. maxsize=5 begrenzt den Pool auf 5 Verbindungen; eine sechste Abfrage wartet auf eine freie, statt eine weitere zu öffnen. Nach dem Test hielt der Bot 2 offene Verbindungen.
Die Autocommit-Falle
aiomysql öffnet Verbindungen mit ausgeschaltetem Autocommit. Ein INSERT bleibt dann in einer offenen Transaktion. Geht die Verbindung mit noch offener Transaktion zurück in den Pool, schließt aiomysql sie, und MariaDB wirft die nicht bestätigte Zeile weg. In unserem Test wurde eine Zeile, die über einen Pool ohne autocommit=True eingefügt wurde, von 0 anderen Verbindungen gesehen. Übergib entweder autocommit=True an create_pool, wie oben, oder rufe await conn.commit() nach deinen Schreibvorgängen auf.
Eine fehlgeschlagene Abfrage stoppt den Bot nicht
In unserem Test haben wir die Tabelle gelöscht, während der Bot lief, und /notes gesendet. aiogram protokollierte den Fehler mit einem Traceback, der auf pymysql.err.ProgrammingError: (1146, "Table '...notes' doesn't exist") endet, schickte keine Antwort auf diesen Befehl und lief weiter: Sobald die Tabelle wieder da war, wurde das nächste /note gespeichert. Wer den Befehl gesendet hat, sieht nichts; fange den Fehler also im Handler ab und antworte mit einer kurzen Nachricht, wenn eine Abfrage fehlschlagen kann.
Eine ruhige Nacht legt den Pool nicht lahm
Unser Datenbankserver schließt eine Verbindung, die 8 Stunden untätig war. Ein Bot, der so lange keine Nachrichten bekommt, etwa über Nacht, findet seine gepoolten Verbindungen geschlossen. aiomysql prüft das, wenn ein Handler eine Verbindung anfordert, und öffnet eine neue anstelle einer geschlossenen. Wir haben es mit einem auf 5 Sekunden gesenkten Leerlauflimit getestet: Nach 7 ruhigen Sekunden wurde das nächste /note ohne Fehler gespeichert.
Limits, die für deinen Pool zählen
- 20 Verbindungen gleichzeitig pro Datenbankbenutzer. Jeder Prozess, der dasselbe Login nutzt, zählt mit. Verbindung 21 wird abgelehnt mit
(1226, "User '...' has exceeded the 'max_user_connections' resource (current value: 20)"). maxsize=5 hält den Bot weit darunter.
- Eine Abfrage, die länger als 120 Sekunden läuft, wird gestoppt. MariaDB beendet sie mit Fehler 1969,
Query execution was interrupted (max_statement_time exceeded). In einem früheren Test von uns kam das auf einem gleich eingerichteten Konto nach 120,03 Sekunden. Die Abfragen eines Bots dauern normalerweise Millisekunden, deshalb trifft das nur eine außer Kontrolle geratene Abfrage.
- 500 MB pro Datenbank. Auf der Seite Datenbanken zeigt, wie viel belegt ist. Bei 500 MB werden Schreibzugriffe gestoppt, bis du Zeilen löschst.
- Server-Backups enthalten die Datenbank nicht. Exportiere sie ab und zu aus phpMyAdmin.
Fehler und was sie bedeuten
Das sind die Fehler, die aiomysql in unserem Test gab, so geschrieben, wie sie in der Konsole einen Traceback beenden (sie stammen von PyMySQL, das aiomysql darunter nutzt):
pymysql.err.OperationalError: (1045, "Access denied for user ..."): falscher DB_USER oder DB_PASSWORD. Nach Passwort zurücksetzen auf der Karte Verbindung funktioniert das alte Passwort sofort nicht mehr: Ersetze DB_PASSWORD und starte den Bot neu.
pymysql.err.OperationalError: (2003, "Can't connect to MySQL server on ..."): falscher Host oder Port, oder eine Verbindung von außerhalb von PlayNHost. Die Datenbank akzeptiert nur Verbindungen von PlayNHost-Servern und phpMyAdmin, das bekommst du also auch auf deinem eigenen Computer.
pymysql.err.OperationalError: (1226, ... 'max_user_connections' ...): zu viele offene Verbindungen, siehe die Limits oben. Zum Beispiel eine zweite Kopie des Bots oder ein Pool, der für jede Nachricht geöffnet wird.
pymysql.err.DataError: (1264, "Out of range value for column 'user_id' at row 1"): eine ID-Spalte ist INT. Ändere sie in BIGINT.
pymysql.err.ProgrammingError: (1146, "Table '...' doesn't exist"): die Tabelle fehlt. Der Code oben legt sie beim Start an; wenn du sie von Hand erstellt hast, prüfe ihren Namen.
Eine in Umgebung fehlende Variable stoppt den Bot, bevor er sich verbindet, mit KeyError: 'DB_HOST' oder dem Namen der fehlenden, weil der Code jeden Wert mit os.environ[...] liest. Füge sie hinzu und starte den Bot neu: Variablen gelten ab dem nächsten Start.
Ein falscher Token fällt erst nach dem Datenbankteil auf. Mit unserem falschen Token gegen das echte Telegram gab der Bot Database ready aus und stoppte dann mit aiogram.exceptions.TelegramUnauthorizedError: Telegram server says - Unauthorized. Kopiere den Token erneut von @BotFather und füge ihn auf der Seite Bot, Bereich Telegram. Mehr Telegram-Fehler: Telegram-Bot-Fehler 409 Conflict beheben.
Wie viel Speicher er braucht
In unserem Test belegte der laufende Bot 174 MB Speicher (resident), nachdem er die 8 Befehle beantwortet hatte, mit offenem Pool, in einem auf 512 MB begrenzten Container wie bei unserem kleinsten Bot-Tarif. Das meiste davon ist aiogram selbst: In unserem früheren Hosting-Test maß aiogram allein 173 MB (dieser Bot stoppte an einem abgelehnten Token, bevor er eine Nachricht bearbeitete, die beiden Zahlen liegen also nah beieinander, sind aber kein exakter Vergleich). Die Installation von aiogram und aiomysql mit pip von Grund auf erreichte 163 MB, Dateicache eingerechnet. Das lässt auf dem kleinsten Tarif Platz für deinen eigenen Code. Die Hosting-Anleitung vergleicht die Bibliotheken: python-telegram-bot vs aiogram bei PlayNHost.
Dasselbe mit einer anderen Bibliothek
Brauchst du einen Server für deinen Telegram-Bot mit enthaltener Datenbank? Zu den Telegram-Bot-Tarifen.