یادم می‌آید اولین اسکریپت پایتونی که برای یک پروژه‌ی واقعی نوشتم، قرار بود گزارش هفتگی فروش را از دیتابیس MySQL یک فروشگاه بیرون بکشد و در قالب CSV تحویل دهد. سه ساعت روی اتصال گیر کردم، ولی مشکل از سینتکس نبود؛ از یک جزئیات ساده غافل شده بودم: کاراکترست دیتابیس روی latin1 تنظیم بود و متن‌های فارسی، به شکل کاراکترهای عجیب ذخیره شده بودند. وقتی به مدیر فنی پروژه گفتم، لبخندی زد و گفت: «همه‌ی ما یک بار این مسیر را رفته‌ایم.» آن تجربه به من یاد داد که اتصال پایتون به MySQL فقط یک خط connect نیست؛ مجموعه‌ای از تصمیم‌هاست که اگر از همان اول درست گرفته نشوند، در ماه ششم پروژه به یک بحران تبدیل می‌شوند. در این مقاله، همان مسیری را می‌روم که در پروژه‌های واقعی طی کرده‌ام: از انتخاب کتابخانه و اولین کوئری تا پارامتریک‌سازی، تراکنش، utf8mb4 برای متن فارسی و connection pool.

چرا اتصال پایتون به MySQL، مهارتی پایه است؟

اگر با مفاهیم پایه‌ی پایتون آشنا نیستید، اول آموزش پایتون از صفر را بخوانید. اما فرض کنیم پایتون را می‌شناسید و می‌خواهید از یک اسکریپت ساده عبور کنید. در واقعیت، تقریباً هر اسکریپت جدی، در نقطه‌ای با دیتابیس سر و کار دارد: خواندن داده برای تحلیل، ذخیره‌ی نتایج پردازش، همگام‌سازی بین سیستم‌ها، یا ساخت یک API ساده.

MySQL همچنان یکی از پرکاربردترین دیتابیس‌های متن‌باز است و در پروژه‌های وردپرسی، فروشگاهی و سازمانی، سهم بزرگی دارد. اگر با بخش PHP این بستر آشنا هستید، اتصال PHP به MySQL هم مسیر مشابهی را نشان می‌دهد. تفاوت این‌جاست که پایتون، سه کتابخانه‌ی اصلی برای این کار دارد و انتخاب اشتباه، پروژه را در میانه‌ی راه متوقف می‌کند.

اتصال به دیتابیس، پلی است که یک‌بار درست ساخته می‌شود و هزار بار استفاده؛ هزینه‌ی ساختِ اشتباهش، در هر کوئری پرداخت می‌شود.

سه کتابخانه، سه سناریو

در پایتون، سه کتابخانه‌ی اصلی برای اتصال به MySQL وجود دارد که هرکدام برای سناریوی متفاوتی مناسب است:

کتابخانهمناسب برایمزیتهشدار
mysql-connector-pythonراه‌حل رسمی و پشتیبانی‌شدهپشتیبانی مستقیم Oracleنصب سنگین‌تر
PyMySQLسبک و سریعخالص پایتون، نصب آسانکمی کندتر در حجم بالا
SQLAlchemyپروژه‌های بزرگ و ORMلایه‌ی انتزاعی قویمنحنی یادگیری

انتخاب من در پروژه‌های واقعی: برای اسکریپت‌های کوچک و تحلیل داده، PyMySQL؛ برای پروژه‌هایی که آینده‌ی بزرگ دارند و به ORM نیاز دارند، SQLAlchemy؛ برای پروژه‌های سازمانی که به پشتیبانی تجاری نیاز دارند، mysql-connector-python. تفاوت عملی بین این سه، در روز اول محسوس نیست، ولی در ماه ششم، وقتی کد شما سه برابر شده، انتخاب اشتباه، هزینه‌ی بازنویسی می‌سازد.

اگر با مفهوم ORM آشنا نیستید، ORM چیست و چگونه کار با دیتابیس را ساده می‌کند تفاوت ORM با اتصال مستقیم را روشن می‌کند. در این مقاله، ابتدا روی اتصال مستقیم تمرکز می‌کنم چون پایه‌ی همه‌چیز است، و در بخش انتهایی به SQLAlchemy برمی‌گردم.

نصب و اولین اتصال

مثل هر پروژه‌ی پایتونی، از یک محیط مجازی شروع کنید:

python -m venv venv
source venv/bin/activate        # در ویندوز: venv\Scripts\activate

pip install PyMySQL
pip freeze > requirements.txt

اولین اتصال، در ساده‌ترین شکل:

import pymysql

connection = pymysql.connect(
    host="localhost",
    user="db_user",
    password="db_password",
    database="my_database",
    charset="utf8mb4",
    cursorclass=pymysql.cursors.DictCursor,
)

with connection:
    with connection.cursor() as cursor:
        cursor.execute("SELECT VERSION()")
        result = cursor.fetchone()
        print(result)

سه نکته‌ی مهم در همین چند خط که در پروژه‌های واقعی به‌کارم آمده:

  • charset="utf8mb4": برای پشتیبانی کامل از یونیکد (شامل ایموجی و کاراکترهای نادر فارسی) حتماً utf8mb4 را صریح مشخص کنید. پیش‌فرض در بعضی نسخه‌ها latin1 است و با آن، متن فارسی به‌شکل کاراکترهای عجیب ذخیره می‌شود.
  • cursorclass=DictCursor: نتایج را به‌شکل دیکشنری برمی‌گرداند نه تاپل. خوانایی کد بالا می‌رود و row["name"] خواناتر از row[2] است.
  • استفاده از with connection: این ساختار، اتصال را در پایان بلوک به‌طور خودکار می‌بندد و تراکنش را در صورت خطا rollback می‌کند. همیشه از آن استفاده کنید — همان اصلی که در کار با فایل‌ها در پایتون برای with open رویش تأکید کرده‌ام.

اگر با خطای اتصال روبرو شدید، دو مقاله‌ی رفع خطای Access denied برای کاربر MySQL و رفع خطای Can`t connect to MySQL server مسیر تشخیص را گام‌به‌گام نشان می‌دهند. تجربه‌ی من: در ۸۰٪ موارد، خطای اتصال به دو دلیل است — رمز عبور اشتباه، یا سرور MySQL فقط اتصال از localhost را می‌پذیرد و شما از یک IP بیرونی تلاش می‌کنید.

cursor و اجرای کوئری

cursor، شیئی است که کوئری‌ها را روی آن اجرا می‌کنید و نتایج را می‌خوانید. چهار متد اصلی که در پروژه‌های واقعی دائماً استفاده می‌کنم:

with connection.cursor() as cursor:
    # اجرای کوئری بدون نتیجه
    cursor.execute("SELECT id, name FROM users WHERE age > %s", (18,))

    # خواندن یک ردیف
    row = cursor.fetchone()

    # خواندن چند ردیف مشخص
    rows = cursor.fetchmany(10)

    # خواندن همه‌ی ردیف‌ها
    all_rows = cursor.fetchall()

سه نکته‌ی مهم در استفاده از cursor که در پروژه‌های واقعی به آن‌ها رسیده‌ام:

  • cursor باید بسته شود: with این کار را خودکار انجام می‌دهد. اگر از with استفاده نکنید، cursor باز می‌ماند و منابع سرور را می‌خورد — دقیقاً همان مشکلی که در بخش Too many connections در رفع خطای Too many connections در MySQL علتش را توضیح داده‌ام.
  • fetchall روی کوئری بزرگ خطرناک است: اگر جدول شما یک میلیون ردیف دارد و fetchall می‌زنید، کل آن در حافظه‌ی پایتون بار می‌شود. برای داده‌ی حجیم، از fetchmany در حلقه استفاده کنید یا cursor را به‌شکل server-side اجرا کنید. اگر با خطای حافظه روبرو شده‌اید، رفع خطای MemoryError در پایتون راه‌های تشخیص را نشان می‌دهد.
  • همیشه از %s استفاده کنید، نه ?: در PyMySQL و mysql-connector-python، placeholder با %s است. در SQLite و بعضی کتابخانه‌های دیگر، ?. اشتباه گرفتن این دو، خطای مبهم می‌دهد.

پارامتریک‌سازی: مرز امنیت و فاجعه

مهم‌ترین بخش این مقاله، همین بخش است. اشتباه در پارامتریک‌سازی، مستقیماً به SQL Injection می‌انجامد که در مقالات امنیتی بارها رویش تأکید کرده‌ام — مثلاً در امنیت در PHP همان اصول را در بستر PHP توضیح داده‌ام.

مقایسه کنید:

# خطرناک: چسباندن رشته
user_id = input("User ID: ")
query = f"SELECT * FROM users WHERE id = {user_id}"
cursor.execute(query)  # اگر user_id = "1 OR 1=1" باشد، فاجعه

در این کد، اگر مهاجم ورودی 1 OR 1=1 -- را وارد کند، کوئری به SELECT * FROM users WHERE id = 1 OR 1=1 -- تبدیل می‌شود که تمام کاربران را برمی‌گرداند. حتی بدتر: با ورودی مناسب، می‌توان DROP TABLE اجرا کرد.

راه درست، پارامتریک‌سازی است:

user_id = 1  # همیشه به‌عنوان پارامتر، نه رشته
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))

سه نکته‌ی حیاتی در پارامتریک‌سازی:

  • پارامترها همیشه tuple هستند: حتی اگر یک پارامتر دارید، کاما را فراموش نکنید: (user_id,) نه (user_id). این خطا در پروژه‌های واقعی زیاد دیده می‌شود و باعث خطای عجیب می‌شود.
  • placeholder برای IN را دستی بسازید: کوئری WHERE id IN (%s) با یک پارامتر، فقط یک مقدار می‌پذیرد. برای لیست، باید تعداد علامت‌ها را به‌اندازه‌ی لیست بسازید:
ids = [1, 2, 3, 4]
placeholders = ", ".join(["%s"] * len(ids))
query = f"SELECT * FROM users WHERE id IN ({placeholders})"
cursor.execute(query, ids)
  • هرگز نام ستون یا جدول را پارامتریک نکنید: پارامتریک‌سازی فقط برای مقادیر کار می‌کند، نه برای نام جدول یا ستون. اگر نام جدول از ورودی کاربر می‌آید، آن را در برابر یک whitelist بررسی کنید:
allowed_tables = {"users", "orders", "products"}
if table_name not in allowed_tables:
    raise ValueError("Invalid table")

query = f"SELECT COUNT(*) FROM {table_name}"
cursor.execute(query)

این نوع محافظت، تفاوت بین کدی است که در پروژه‌ی خودتان کار می‌کند و کدی که در برابر مهاجم امن است. اصل کلی، همان است که در نوشتن کد PHP امن رویش تأکید کرده‌ام — فقط با سینتکس متفاوت.

هر بار که با f-string یا format کوئری می‌سازید، یک درِ پشتی برای مهاجم باز می‌کنید؛ پارامتریک، تنها راه امن است — نه یک راه خوب‌تر.

CRUD کامل با پایتون

چهار عملیات پایه‌ای که در هر پروژه‌ای لازم می‌شود:

ایجاد (Insert)

with connection.cursor() as cursor:
    sql = "INSERT INTO users (name, email, age) VALUES (%s, %s, %s)"
    cursor.execute(sql, ("Ali", "ali@example.com", 30))
    connection.commit()
    new_id = cursor.lastrowid

خواندن (Select)

with connection.cursor() as cursor:
    sql = "SELECT id, name, email FROM users WHERE age > %s ORDER BY name"
    cursor.execute(sql, (18,))
    users = cursor.fetchall()

for user in users:
    print(user["name"])

به‌روزرسانی (Update)

with connection.cursor() as cursor:
    sql = "UPDATE users SET email = %s WHERE id = %s"
    cursor.execute(sql, ("new@example.com", 1))
    connection.commit()
    print(f"Updated {cursor.rowcount} rows")

حذف (Delete)

with connection.cursor() as cursor:
    sql = "DELETE FROM users WHERE id = %s"
    cursor.execute(sql, (5,))
    connection.commit()

سه نکته‌ی مهم در CRUD که در پروژه‌های واقعی به آن‌ها رسیده‌ام:

  • cursor.rowcount: بعد از UPDATE یا DELETE، این ویژگی تعداد ردیف‌های تأثیرگرفته را می‌دهد. اگر صفر باشد، یعنی کوئری شرطی را پیدا نکرده — که خودش یک سیگنال مهم است.
  • cursor.lastrowid: بعد از INSERT، شناسه‌ی ردیف جدید را برمی‌گرداند. در پروژه‌های واقعی، بی‌نهایت مفید است — چون به‌جای یک کوئری جدا برای گرفتن MAX(id)، همان‌جا id را دارید.
  • executemany برای bulk insert: اگر باید هزار ردیف درج کنید، از executemany استفاده کنید نه حلقه:
data = [
    ("Ali", "ali@example.com", 30),
    ("Sara", "sara@example.com", 25),
    ("Reza", "reza@example.com", 35),
]

sql = "INSERT INTO users (name, email, age) VALUES (%s, %s, %s)"
cursor.executemany(sql, data)
connection.commit()

تفاوت سرعت بین حلقه و executemany، روی هزار ردیف، می‌تواند ده برابر باشد. علتش این است که executemany از قابلیت‌های بومی دیتابیس استفاده می‌کند.

تراکنش و commit/rollback

تراکنش، مجموعه‌ای از عملیات است که یا همه اعمال می‌شوند یا هیچ‌کدام. مثال کلاسیک، انتقال پول بین دو حساب است: کم‌کردن از یکی و اضافه‌کردن به دیگری، باید با هم انجام شوند. اگر یکی شکست بخورد، هیچ‌کدام نباید اعمال شود.

try:
    with connection.cursor() as cursor:
        cursor.execute("UPDATE accounts SET balance = balance - %s WHERE id = %s", (100, 1))
        cursor.execute("UPDATE accounts SET balance = balance + %s WHERE id = %s", (100, 2))
        connection.commit()
except pymysql.Error as e:
    connection.rollback()
    print(f"Transaction failed: {e}")

سه نکته‌ی مهم در تراکنش:

  • commit را فراموش نکنید: در PyMySQL و mysql-connector-python، به‌طور پیش‌فرض autocommit=False است. یعنی اگر commit نزنید، تغییرات شما در دیتابیس ذخیره نمی‌شود. این باگ در پروژه‌های واقعی بسیار رایج است — کدی که در تست کار می‌کند، در تولید داده ذخیره نمی‌کند.
  • rollback در except: اگر در میانه‌ی تراکنش خطایی رخ دهد، باید rollback بزنید تا تغییرات نیمه‌کاره لغو شوند. with connection این کار را خودکار انجام می‌دهد، ولی اگر دستی مدیریت می‌کنید، خودتان باید بنویسید.
  • محدوده‌ی تراکنش را کوچک نگه دارید: تراکنش طولانی، جدول‌ها را قفل می‌کند و در پروژه‌های پربازدید به خطای Lock wait timeout می‌انجامد. اگر تراکنش شما بیش از چند صد میلی‌ثانیه طول می‌کشد، ساختار را بازبینی کنید.

برای درک عمیق‌تر مفهوم تراکنش و سطوح Isolation، تراکنش‌ها در MySQL جزئیات بیشتری دارد. اگر با مفاهیم SQL آشنایی کم دارید، آموزش MySQL از صفر پیش‌نیاز خوبی است.

charset و درد اختصاصی متن فارسی

همان مشکلی که در مقدمه گفتم، به‌قدری رایج است که ارزش یک بخش جداگانه را دارد. اگر با متن فارسی کار می‌کنید، سه کار را از همان روز اول انجام دهید:

۱) دیتابیس، جدول و ستون‌ها را با utf8mb4 بسازید

CREATE DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

نکته‌ی مهم: utf8mb4 را انتخاب کنید نه utf8. در MySQL، utf8 فقط ۳ بایت را پشتیبانی می‌کند و کاراکترهای بالای BMP (شامل بعضی ایموجی‌ها و کاراکترهای نادر) را نمی‌پذیرد. utf8mb4 نسخه‌ی کامل ۴ بایتی است و استاندارد امروز.

۲) در اتصال پایتون، charset را صریح مشخص کنید

connection = pymysql.connect(
    host="localhost",
    user="db_user",
    password="db_password",
    database="my_database",
    charset="utf8mb4",
    use_unicode=True,
)

۳) collation را جدی بگیرید

برای متن فارسی، utf8mb4_unicode_ci یا utf8mb4_persian_ci انتخاب‌های درست هستند. تفاوت در مرتب‌سازی و مقایسه است — persian_ci برای زبان فارسی بهینه‌تر است، ولی unicode_ci عمومی‌تر و سازگارتر با داده‌های چندزبانه است. تجربه‌ی من: اگر داده‌ی شما فقط فارسی است، persian_ci؛ اگر با متن چندزبانه (شامل عربی و انگلیسی) سر و کار دارید، unicode_ci.

اگر با خطای Incorrect string value روبرو شده‌اید، رفع خطای Incorrect string value در MySQL مسیر تشخیص را نشان می‌دهد. تجربه‌ی من: در نود درصد موارد، علت این خطا، charset اشتباه در سطح جدول یا اتصال است.

Connection Pool در پروژه‌های پربازدید

باز و بستن اتصال در هر درخواست، هزینه‌ی سنگینی دارد. در پروژه‌های پربازدید، از connection pool استفاده می‌کنید: مجموعه‌ای از اتصال‌های باز که بین درخواست‌ها به اشتراک گذاشته می‌شوند. در پایتون، DBUtils ابزار رایج برای این کار است:

from dbutils.pooled_db import PooledDB
import pymysql

pool = PooledDB(
    creator=pymysql,
    maxconnections=10,
    mincached=2,
    maxcached=5,
    blocking=True,
    host="localhost",
    user="db_user",
    password="db_password",
    database="my_database",
    charset="utf8mb4",
)

connection = pool.connection()

سه پارامتر که در تنظیم pool اهمیت دارند:

  • maxconnections: حداکثر اتصال همزمان. اگر این عدد را بالا ببرید، ممکن است سرور MySQL را تحت فشار بگذارید — پس با ظرفیت واقعی سرور تنظیم کنید.
  • mincached: تعداد اتصال‌هایی که در pool آماده نگه داشته می‌شوند. برای کاهش تأخیر اولین درخواست، عدد کوچکی بگذارید.
  • blocking=True: اگر pool پر باشد، درخواست جدید منتظر می‌ماند به‌جای این‌که خطا بدهد. برای پروژه‌های API، این انتخاب معمولاً درست است.

یک نکته‌ی مهم: pool باید در سطح ماژول یا اپلیکیشن ساخته شود، نه در هر درخواست. اگر در هر تابع pool جدید بسازید، عملاً هیچ مزیتی از pooling نمی‌گیرید و همان هزینه را پرداخت می‌کنید. اگر با Flask یا Django کار می‌کنید، هر دو از connection pooling داخلی پشتیبانی می‌کنند و نیازی به پیاده‌سازی دستی ندارید — آموزش فلاسک در پایتون نمونه‌های عملی این را نشان می‌دهد.

SQLAlchemy و ORM در پایتون

اگر پروژه‌ی شما به‌مرور بزرگ می‌شود و کوئری‌های خام دستی جوابگو نیستند، SQLAlchemy انتخاب استاندارد است. دو لایه دارد: Core (کوئری‌ساز) و ORM (نقشه‌برداری شیء-رابطه).

from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import declarative_base, sessionmaker

Base = declarative_base()

class User(Base):
    __tablename__ = "users"

    id = Column(Integer, primary_key=True)
    name = Column(String(100), nullable=False)
    email = Column(String(150), unique=True)

engine = create_engine(
    "mysql+pymysql://user:pass@localhost/mydb?charset=utf8mb4",
    echo=False,
    pool_pre_ping=True,
)

Session = sessionmaker(bind=engine)
session = Session()

new_user = User(name="Ali", email="ali@example.com")
session.add(new_user)
session.commit()

users = session.query(User).filter(User.name.like("A%")).all()

سه مزیت ORM که در پروژه‌های واقعی محسوس است:

  • جداسازی منطق از SQL: کد شما از جزئیات دیتابیس مستقل می‌شود. اگر بعداً دیتابیس را تغییر دهید، کوئری‌ها باید دست‌نخورده بمانند.
  • امنیت پیش‌فرض: ORM به‌طور خودکار پارامتریک‌سازی را انجام می‌دهد — خطای SQL Injection که در کوئری خام ممکن است، در ORM بسیار سخت‌تر می‌شود.
  • مدیریت روابط: در دیتابیس‌های با روابط پیچیده، ORM لایه‌ی تمیزی برای JOINهای مکرر می‌سازد.

نقطه‌ی ضعف ORM که در پروژه‌های واقعی به آن برخورده‌ام: مشکل کلاسیک N+1 در روابط. اگر در حلقه روی یک رابطه پیمایش کنید، به‌ازای هر رکورد یک کوئری جدا اجرا می‌شود. راه‌حل، استفاده از joinedload و selectinload است. تفاوت این رفتار با کوئری‌های خام، دقیقاً همان درسی است که در بهینه‌سازی کوئری‌های وردپرس با کدنویسی هم دیده‌ام — N+1 در هر بستری هزینه دارد.

اگر در پروژه‌ای با Django کار می‌کنید، ORM داخلی Django معادل مشابهی دارد — آموزش جنگو برای مبتدیان نمونه‌های عملی آن را نشان می‌دهد. انتخاب بین SQLAlchemy و ORM جنگو، بیشتر به بستر پروژه بستگی دارد تا به خود ORM.

مدیریت خطاهای اتصال

هر عملیات دیتابیس می‌تواند شکست بخورد. جدول خطاهای رایج و راه‌حلشان:

خطاعلت رایجراه‌حل
OperationalErrorاتصال به سرور برقرار نشدبررسی هاست، پورت، فایروال
IntegrityErrorنقض قید یکتایی یا FKبررسی داده قبل از insert
ProgrammingErrorخطای سینتکس SQLبازبینی کوئری و پارامترها
DataErrorداده‌ی نامعتبر برای ستونبررسی طول و نوع داده
InterfaceErrorمشکل در لایه‌ی رابطبررسی charset و نسخه‌ی کتابخانه

الگوی درست مدیریت:

import pymysql
import logging

logger = logging.getLogger(__name__)

def get_user(user_id):
    try:
        with connection.cursor() as cursor:
            cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
            return cursor.fetchone()
    except pymysql.err.IntegrityError as e:
        logger.error("Integrity error: %s", e)
        raise
    except pymysql.err.OperationalError as e:
        logger.error("DB connection error: %s", e)
        raise
    except pymysql.Error as e:
        logger.exception("Unexpected database error")
        raise

سه نکته در مدیریت خطا که در پروژه‌های واقعی به آن‌ها رسیده‌ام:

  • خطا را خفه نکنید: اگر خطا گرفتید، لاگ کنید و معمولاً دوباره پرتاب کنید. خفه کردن خطای دیتابیس، به سرعت به باگ‌های پنهانی تبدیل می‌شود که در تولید خودش را نشان می‌دهد.
  • خطاهای گذرا را retry کنید: اگر خطای OperationalError از جنس قطعی گذرای شبکه باشد، یک بار تلاش دوباره معمولاً مشکل را حل می‌کند. الگوی retry با تأخیر نمایی را در مدیریت خطا در پایتون توضیح داده‌ام.
  • connection را بعد از خطای جدی، دوباره بسازید: اگر خطای جدی مثل قطعی اتصال رخ داد، همان connection قبلی دیگر قابل استفاده نیست. آن را ببندید و اتصال جدید بسازید. pool_pre_ping=True در SQLAlchemy این کار را خودکار انجام می‌دهد.

اشتباهاتی که در پروژه‌های واقعی دیده‌ام

در بازبینی پروژه‌های پایتونی که با MySQL کار می‌کنند، این اشتباهات را زیاد دیده‌ام:

  • چسباندن رشته در کوئری: همان اشتباه مهلک که در بخش امنیت مفصلاً گفتم. حتی اگر داده از کاربر نمی‌آید، پارامتریک کنید — چون یک روز داده از کاربر می‌آید.
  • فراموش کردن commit: داده در دیتابیس ذخیره نمی‌شود و ساعت‌ها دیباگ می‌گیرد. عادت کنید: هر عملیات تغییردهنده، یک commit می‌خواهد.
  • نبود charset="utf8mb4" در اتصال: متن فارسی به‌شکل کاراکترهای عجیب ذخیره می‌شود یا خطا می‌دهد.
  • fetchall روی جدول حجیم: حافظه پر می‌شود و برنامه کرش می‌کند. از fetchmany یا streaming استفاده کنید.
  • نبود connection pool: در هر درخواست اتصال جدید می‌سازند. در پروژه‌های پربازدید، تأخیر و بار سرور به‌شدت بالا می‌رود.
  • باز گذاشتن cursor و connection: منبع سرور را می‌خورد و در نهایت به Too many connections می‌رسد. همیشه از with استفاده کنید.
  • hard-code کردن اطلاعات اتصال: رمز دیتابیس در کد. همیشه از متغیرهای محیطی یا فایل پیکربندی خارج از مخزن استفاده کنید.
  • نبود try except روی عملیات دیتابیس: یک خطای گذرا، کل برنامه را متوقف می‌کند.
  • پیمایش حلقه‌ای روی کوئری‌ها: N+1 در هر بستری. راه‌حل، کوئری ترکیبی یا eager loading است.
  • بی‌توجهی به تراکنش در عملیات چندمرحله‌ای: داده‌ی نیمه‌کاره در دیتابیس می‌ماند. عملیات مرتبط را در یک تراکنش بگذارید.

یک توصیه‌ی عملی از تجربه: قبل از استقرار روی سرور، برنامه را با یک کپی از داده‌ی واقعی و از یک IP خارجی تست کنید. این تست کوچک، تفاوت‌های محیطی مثل دسترسی شبکه، charset، و مجوزها را قبل از بحران تولید، لویش می‌دهد. اگر با pandas کار می‌کنید و می‌خواهید داده را مستقیم از دیتابیس بخوانید، pd.read_sql روی یک connection معتبر کار می‌کند — نمونه‌های بیشتر در کتابخانه pandas در پایتون آمده است.

سخن آخر

اتصال پایتون به MySQL، از یک خط connect شروع می‌شود ولی در پروژه‌های واقعی، به یک لایه‌ی معماری تبدیل می‌شود. سه نکته‌ی اصلی که در این مقاله به آن‌ها رسیدیم: اول، پارامتریک‌سازی نه یک انتخاب سبک، بلکه یک الزام امنیتی است — هر کوئری با چسباندن رشته، یک حفره‌ی بالقوه است؛ دوم، charset="utf8mb4" و collation مناسب، برای متن فارسی حیاتی است — این یک خط، جلوی سردردهای طولانی ماه‌های بعد را می‌گیرد؛ سوم، انتخاب کتابخانه (PyMySQL، mysql-connector، یا SQLAlchemy) باید بر اساس اندازه و آینده‌ی پروژه انجام شود، نه بر اساس آموزشگاه آخر.

اگر امروز می‌خواهید شروع کنید، سه کار کوچک پیشنهاد می‌کنم: یک دیتابیس تستی با utf8mb4 بسازید، یک جدول کوچک با چند رکورد متن فارسی در آن درج کنید، و از پایتون آن را بخوانید و در کنسول چاپ کنید. همین پروژه‌ی کوچک، همه‌ی مفاهیم پایه‌ی این مقاله را زنده می‌کند. اگر تجربه‌ای از اتصال پایتون به MySQL در پروژه‌های خودتان دارید — مخصوصاً اگر با چالش charset، تراکنش، یا connection pool روبرو شده‌اید — در دیدگاه‌ها بنویسید؛ همین نکته‌های میدانی، برای خواننده‌ی بعدی از هر مستند رسمی ارزشمندتر است. 🐍