جديد 📢 عرض الإطلاق لفترة محدودة: خصم حتى 70% على شهرك الأول في كل لعبة.عرض الباقات

كيفية استخدام قاعدة بيانات MySQL مع بوت python-telegram-bot (aiomysql)

استضافة بوتات TelegramUpdated Nov 6, 202611 min read

الجواب المختصر. أضف aiomysql إلى بوت python-telegram-bot الخاص بك، وافتح مجمّع اتصالات واحدًا بالإعداد autocommit=True داخل post_init، واحتفظ به في application.bot_data كي يصل إليه كل معالج، وأغلقه في post_shutdown. استخدم عناصر الاستبدال %s واحفظ معرّفات Telegram بالنوع BIGINT لا INT. في PlayNHost تتضمن كل باقة بوت قاعدة بيانات MySQL واحدة: أنشئها من صفحة قواعد البيانات، والصق بياناتها في لوحة البيئة، فيعمل الكود أدناه كما هو. تقبل القاعدة الاتصالات من خوادم PlayNHost الخاصة بك ومن phpMyAdmin، ولا تقبلها من جهازك الشخصي. اختبرنا هذا الكود في 6 نوفمبر 2026 مع python-telegram-bot 22.8 وaiomysql 0.3.2 وPython 3.12، على MariaDB 10.11 بحساب قاعدة بيانات مُعدّ مثل حسابنا (20 اتصالًا، وحد زمني للاستعلام 120 ثانية) وبديل لخادم Bot API الخاص بـ Telegram: 8 أوامر دخلت و8 ردود خرجت، وحُفظت الملاحظات في القاعدة. لم يكن تسجيل دخول حقيقي إلى Telegram جزءًا من الاختبار.

ما تحتاج إليه

1. أنشئ قاعدة البيانات

  1. افتح خادم البوت في اللوحة وانقر قواعد البيانات.
  2. انقر قاعدة بيانات جديدة. حقل التسمية اختياري؛ ويعرض ستُنشأ باسم الاسم الكامل. انقر إنشاء قاعدة بيانات.
  3. تُفتح صفحة القاعدة الجديدة (وفي وقت لاحق انقر عليها ضمن قواعد بياناتك). تعرض بطاقة الاتصال فيها المضيف والمنفذ وقاعدة البيانات واسم المستخدم وكلمة المرور. وتحت إعداد جاهز يحتوي إعداد لـ على مقطع Python (aiomysql) بالقيم نفسها مُعبّأة مسبقًا.

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 يعرض آخر خمس ملاحظات لك.

requirements.txt

python-telegram-bot>=22,<23
aiomysql

bot.py

import os

import aiomysql
from telegram import Update
from telegram.ext import Application, CommandHandler, ContextTypes


async def post_init(application: Application) -> 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))"
            )
    application.bot_data["pool"] = pool
    print("Database ready")
    print(f"Logged in as @{application.bot.username}")


async def post_shutdown(application: Application) -> None:
    pool = application.bot_data.get("pool")
    if pool is not None:
        pool.close()
        await pool.wait_closed()


async def note(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    text = " ".join(context.args).strip()
    if not text:
        await update.effective_message.reply_text("Write your note after the command, for example: /note buy milk")
        return
    if len(text) > 500:
        await update.effective_message.reply_text("A note can be up to 500 characters.")
        return
    pool = context.bot_data["pool"]
    async with pool.acquire() as conn:
        async with conn.cursor() as cur:
            await cur.execute(
                "INSERT INTO notes (user_id, text) VALUES (%s, %s)",
                (update.effective_user.id, text),
            )
    await update.effective_message.reply_text("Saved. Send /notes to see your last five.")


async def notes(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    pool = context.bot_data["pool"]
    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",
                (update.effective_user.id,),
            )
            rows = await cur.fetchall()
    if not rows:
        await update.effective_message.reply_text("You have no notes yet.")
        return
    await update.effective_message.reply_text("\n".join(f"- {text}" for (text,) in rows))


def main() -> None:
    application = (
        Application.builder()
        .token(os.environ["TELEGRAM_BOT_TOKEN"])
        .post_init(post_init)
        .post_shutdown(post_shutdown)
        .build()
    )
    application.add_handler(CommandHandler("note", note))
    application.add_handler(CommandHandler("notes", notes))
    application.run_polling()


if __name__ == "__main__":
    main()

ماذا يفعل: تسأل run_polling() أولًا Telegram عن هوية البوت، ثم تشغّل post_init، ولا تبدأ بجلب الرسائل إلا بعد ذلك. تفتح post_init المجمّع، وتنشئ الجدول notes إن لم يكن موجودًا، وتضع المجمّع في application.bot_data["pool"]، وتطبع Database ready وLogged in as @your_bot. يحصل كل معالج على القاموس نفسه باسم context.bot_data. وعندما يتوقف البوت، تشغّل run_polling() الدالة post_shutdown التي تغلق المجمّع.

context.args هو النص الذي يلي الأمر، مقسّمًا إلى كلمات، ولذلك يعيد الكود ضمّه بمسافات مفردة: تتحول فواصل الأسطر والمسافات المزدوجة داخل الملاحظة إلى مسافة واحدة. وإذا احتجت إلى الملاحظة تمامًا كما كُتبت، فخذ update.effective_message.text واقتطع الأمر بنفسك.

يملأ المشغّل عناصر الاستبدال %s، ولذلك تُحفظ ملاحظة مثل '); DROP TABLE notes; -- كنص عادي. وفي اختبارنا حُفظت على أنها الصف 2 وبقي الجدول. لا تبنِ استعلام SQL باستخدام f-string أبدًا.

احفظ معرّفات Telegram بالنوع BIGINT

تقول وثائق Bot API الخاصة بـ Telegram إن معرّف المستخدم أو المحادثة «قد يحتوي على أكثر من 32 بتًا ذا دلالة» لكنه لا يتجاوز 52، لذا فالعدد الصحيح ذو 64 بت آمن. وتعطي صفحتها عن المعرّفات النطاقات: تصل معرّفات المستخدمين إلى 1.099.511.627.775، بينما معرّفات المجموعات والمجموعات الخارقة والقنوات سالبة. وتمتد المجموعات الخارقة والقنوات من -1.000.000.000.001 إلى -1.997.852.516.352 (مثلًا -1001234567890). أما INT في MySQL فيتوقف عند 2.147.483.647. في اختبارنا، وبوضع SQL الصارم نفسه الذي يستخدمه خادم قواعد البيانات لدينا، فشل حفظ معرّف المستخدم 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، كي تتسع معرّفات المحادثات أيضًا.

4. شغّله

  1. ارفع bot.py وrequirements.txt في الملفات.
  2. اضغط تشغيل وافتح وحدة التحكم. يثبّت التشغيل الحزم من requirements.txt، ثم يعرض [PlayNHost] Starting your bot وDatabase ready وLogged in as @your_bot. وهذا السطر الأخير يحوّل الحالة في نظرة عامة إلى متصل في Telegram.
  3. في Telegram أرسل إلى بوتك /note buy milk، ثم /notes. وللاطلاع على الصفوف افتح صفحة القاعدة وانقر فتح في phpMyAdmin.

ابتداءً من التشغيل الثاني تعرض وحدة التحكم أيضًا Warning: Table 'notes' already exists، يسبقه مسار ملف cursors.py الخاص بـ aiomysql. وهذه ملاحظة من MariaDB بشأن CREATE TABLE IF NOT EXISTS يمرّرها المشغّل. لا يوجد خطأ.

في اختبارنا أرسل بديل 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.

تحديث واحد في كل مرة

بإعداداته الافتراضية يعالج python-telegram-bot تحديثًا واحدًا في كل مرة: ينتظر الأمر التالي حتى ينتهي المعالج السابق. في اختبارنا أُجيب عن 4 أوامر وصلت معًا بترتيب وصولها، وبعد الاختبار كان لدى البوت اتصال واحد مفتوح (1). يبقى maxsize=5 مهمًا إذا فعّلت concurrent_updates في Application.builder()، أو إذا استعلمت المهام والمعالجات في الوقت نفسه: فالمجمّع لا يفتح أكثر من 5 اتصالات، وينتظر أي استعلام إضافي اتصالًا حرًّا.

فخ autocommit

يفتح aiomysql الاتصالات مع إيقاف autocommit. فيبقى INSERT داخل معاملة مفتوحة. وحين يعود الاتصال إلى المجمّع وهذه المعاملة ما زالت مفتوحة، يغلقه aiomysql وتتخلص MariaDB من الصف غير المثبّت. في اختبارنا لم يرَ أي اتصال آخر (0) صفًّا أُدرج في مجمّع بدون autocommit=True. إما أن تمرّر autocommit=True إلى create_pool كما في الأعلى، أو تستدعي await conn.commit() بعد عمليات الكتابة.

الاستعلام الفاشل لا يوقف البوت

في اختبارنا حذفنا الجدول والبوت يعمل، وأرسلنا /notes. طبع python-telegram-bot الرسالة No error handlers are registered, logging exception. وتتبّعًا ينتهي بـ pymysql.err.ProgrammingError: (1146, "Table '...notes' doesn't exist")، ولم يرسل ردًّا على ذلك الأمر، وواصل العمل: وعندما عاد الجدول حُفظ الأمر /note التالي. لا يرى من أرسل الأمر شيئًا، لذا التقط الخطأ داخل المعالج، أو سجّل معالج أخطاء عبر application.add_error_handler()، وردّ برسالة قصيرة.

الليلة الهادئة لا تكسر المجمّع

يغلق خادم قواعد البيانات لدينا أي اتصال ظلّ خاملًا 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 MB لكل قاعدة بيانات. تعرض صفحة قواعد البيانات المساحة المستخدمة. وعند 500 MB تتوقف عمليات الكتابة حتى تحذف صفوفًا.
  • النسخ الاحتياطية للخادم لا تتضمن قاعدة البيانات. صدّرها من 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"): عمود المعرّف من النوع INT. غيّره إلى BIGINT.
  • pymysql.err.ProgrammingError: (1146, "Table '...' doesn't exist"): الجدول مفقود. ينشئه الكود أعلاه عند التشغيل؛ وإذا أنشأته يدويًا فتحقق من اسمه.

المتغير المفقود من البيئة يوقف البوت قبل أن يتصل، مع KeyError: 'DB_HOST' أو اسم المتغير المفقود، لأن الكود يقرأ كل قيمة عبر os.environ[...]. أضفه ثم أعد تشغيل البوت: تُطبَّق المتغيرات عند التشغيل التالي.

الرمز الخاطئ يوقف البوت قبل جزء قاعدة البيانات، لأن post_init لا يعمل إلا بعد أن يقبل Telegram الرمز. وبرمزنا المزيف أمام Telegram الحقيقي لم يطبع البوت Database ready أبدًا، وتوقف بعد نحو 1 ثانية مع telegram.error.InvalidToken: The token `...` was rejected by the server. تحتوي هذه الرسالة على الرمز كاملًا، فلا تلصق مخرجات وحدة التحكم التي تتضمنه في أي مكان عام. انسخ الرمز مجددًا من @BotFather والصقه في صفحة بوت، لوحة Telegram. المزيد من أخطاء Telegram: إصلاح خطأ 409 Conflict في بوت Telegram.

كم من الذاكرة يستخدم

في اختبارنا استخدم البوت العامل 48.5 MB من الذاكرة (المقيمة) بعد الرد على 8 أوامر، والمجمّع مفتوح، داخل حاوية محدودة بـ 512 MB مثل أصغر باقات البوت لدينا. وفي اختبار الاستضافة السابق لنا قاس python-telegram-bot وحده 44 MB (توقف ذلك البوت عند رمز مرفوض، قبل معالجة أي رسالة)، لذا فإن الفرق، نحو 4.5 MB، يغطي aiomysql والمجمّع والأوامر المعالَجة. وبلغ تثبيت python-telegram-bot وaiomysql عبر pip من الصفر ذروة 73 MB، بما فيها ذاكرة الملفات المؤقتة. المكتبات جنبًا إلى جنب: python-telegram-bot مقابل aiogram على PlayNHost.

الأمر نفسه مع مكتبة أخرى

هل تحتاج إلى خادم لبوت Telegram مع قاعدة بيانات مضمّنة؟ اطّلع على باقات بوتات Telegram.

بوتك مستضاف في مكان آخر بالفعل؟ انقله إلينا

أحدث مقالات المدونة