در میان تمام تصمیم‌های فنی که یک توسعه‌دهنده‌ی PHP در طول کار خود می‌گیرد، انتخاب بین mysqli و PDO یکی از آن‌هایی است که سال‌ها با شما می‌ماند. من در اوایل کارم با mysqli شروع کردم؛ چون ساده‌تر به نظر می‌رسید، مستندات واضحی داشت، و مثال‌های زیادی در اینترنت برایش پیدا می‌شد. اما وقتی اولین پروژه‌ی جدی‌ام را به یک تیم تحویل دادم و آن‌ها خواستند کد را به یک دیتابیس دیگر منتقل کنند، فهمیدم که کد من، نه به اندازه‌ی کافی انعطاف‌پذیر است، نه به اندازه‌ی کافی خوانا. آن روز اولین روزی بود که با PDO آشنا شدم — و از آن روز به بعد، تقریبا تمام پروژه‌های جدیدم را با PDO شروع کرده‌ام.

در این مقاله، همان مسیری را باز می‌کنم که در پروژه‌های واقعی برای تسلط بر PDO طی کردم: از اتصال و مدیریت خطا، تا Prepared Statements و تراکنش‌ها، تا الگوهای حرفه‌ای در پروژه‌های بزرگ. اگر با مبانی PHP آشنایی ندارید، پیشنهاد می‌کنم پیش از این مقاله، آموزش PHP از صفر برای مبتدیان را بخوانید. و اگر با مبانی MySQL هم آشنا نیستید، آموزش MySQL از صفر مکمل دقیق این مقاله است.

PDO چیست و چه تفاوتی با mysqli دارد؟

PDO مخفف PHP Data Objects است و در PHP ۵.۱ معرفی شد. هدف اصلی PDO، فراهم کردن یک «لایه‌ی انتزاعی» برای اتصال به دیتابیس است. یعنی به جای اینکه کد شما به‌طور مستقیم با توابع خاص یک دیتابیس کار کند، با یک رابط یکسان کار می‌کند و پشت صحنه، درایورِ مخصوص هر دیتابیس، کار را انجام می‌دهد. این ایده، ساده اما بسیار قدرتمند است.

تفاوت اصلی PDO با mysqli در همین لایه‌ی انتزاعی است. mysqli فقط برای MySQL طراحی شده و کد شما را به MySQL گره می‌زند. در مقابل، PDO از چندین دیتابیس پشتیبانی می‌کند: MySQL، PostgreSQL، SQLite، Oracle، SQL Server و چند مورد دیگر. اگر روزی بخواهید دیتابیس سایت خود را از MySQL به PostgreSQL مهاجرت دهید، با PDO فقط باید یک خط DSN را تغییر دهید؛ با mysqli باید کل کد را بازنویسی کنید.

تفاوت دوم، در سبک کدنویسی است. mysqli هم به صورت رویه‌ای (procedural) و هم به صورت شی‌گرا کار می‌کند، در حالی که PDO فقط شی‌گرا است. این ویژگی، در ابتدا ممکن است برای تازه‌کارها سخت‌تر به نظر برسد، اما در پروژه‌های بزرگ، شی‌گرا بودن PDO یک مزیت جدی است — چرا که با معماری مدرن PHP هم‌خوانی طبیعی دارد. اگر با مفهوم شی‌گرایی در PHP آشنایی ندارید، آموزش شی‌گرایی در PHP پیش‌نیاز ضروری این تصمیم است.

نکته‌ی ظریف: در PHP ۸.۴ و نسخه‌های اخیر، PDO به استاندارد واقعی اکوسیستم PHP تبدیل شده و اکثر فریم‌ورک‌ها و ابزارهای مدرن، آن را به عنوان پیش‌فرض استفاده می‌کنند. mysqli هنوز پشتیبانی می‌شود، اما انتخاب کردنش برای پروژه‌ی جدید، شبیه به انتخاب یک زبان برنامه‌نویسی قدیمی است — کار می‌کند، اما انعطاف کافی برای آینده ندارد.

PDO به شما یک زبان مشترک می‌دهد که با آن می‌توانید با هر دیتابیسی حرف بزنید. mysqli فقط به شما اجازه می‌دهد با MySQL حرف بزنید. تفاوت این دو، تفاوت بین «یک رابطه» و «توانایی برقراری هر رابطه» است.

چهار دلیل که PDO را انتخاب اول من کرده

در سال‌ها کار با هر دو ابزار، چهار دلیل مشخص باعث شده که PDO را برای پروژه‌های جدید انتخاب کنم:

  • انتقال‌پذیری کد: اگر روزی نیاز داشته باشید دیتابیس را عوض کنید، با تغییر یک خط DSN، بقیه‌ی کد دست‌نخورده می‌ماند. در تجربه‌ی من، این ویژگی در پروژه‌های بلندمدت بسیار ارزشمند است.
  • Named Parameters: در PDO می‌توانید از پارامترهای نام‌دار (مثل :name) استفاده کنید که خوانایی کد را به‌شدت بالا می‌برد. در mysqli فقط پارامترهای موقعیتی (?) وجود دارند.
  • Prepared Statements یکسان: در PDO، Prepared Statements در همه‌ی درایورها یکسان است. اگر با MySQL کار می‌کنید یا PostgreSQL یا SQLite، نحو کار یکی است.
  • شی‌گرا بودن طبیعی: PDO فقط شی‌گرا است و در پروژه‌های مدرن، این هم‌خوانی بسیار ارزشمند است. کد شما با استانداردهای امروز PHP هم‌راستا است.

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

DSN: زبان اتصال در PDO

در قلب هر اتصال PDO، یک DSN (Data Source Name) قرار دارد. DSN، یک رشته‌ی متنی است که به PDO می‌گوید به چه دیتابیسی، روی چه سروری، و با چه تنظیماتی وصل شود. ساختار کلی DSN برای MySQL:

mysql:host=localhost;dbname=shop_db;charset=utf8mb4

سه بخش اصلی این DSN:

  • mysql: — نوع دیتابیس. می‌تواند mysql، pgsql، sqlite و چند مورد دیگر باشد. این بخش، همان چیزی است که PDO را از یک ابزار اختصاصی، به یک لایه‌ی انتزاعی تبدیل می‌کند.
  • host=localhost — آدرس سرور دیتابیس. در محیط لوکال معمولا localhost است، اما در محیط‌های ابری، آدرس IP یا نام میزبان سرور را می‌گیرد.
  • dbname=shop_db — نام دیتابیس. دقت کنید که این باید با نام واقعی دیتابیسی که ساخته‌اید مطابق باشد.
  • charset=utf8mb4 — کاراکترست. همیشه از utf8mb4 استفاده کنید، نه utf8 ساده. چون utf8mb4 از کاراکترهای چهاربایتی (مثل ایموجی و بسیاری از کاراکترهای فارسی خاص) هم پشتیبانی می‌کند. تجربه‌ی من: بی‌توجهی به این نکته، در سایت‌های فارسی چندین بار باعث نمایش نادرست کاراکترها و حتی خطا در درج داده شده است.

نکته‌ی مهم در مورد DSN: همیشه آن را به‌عنوان یک متغیر جدا نگه دارید، نه اینکه در خط اتصال بنویسید. این کار، در زمان تغییر یا انتقال، فقط یک خط تغییر می‌خواهد. اگر با مفهوم utf8mb4 یا InnoDB آشنا نیستید، تفاوت InnoDB و MyISAM و آموزش MySQL از صفر این دو را به‌تفصیل باز کرده‌اند.

اتصال با PDO: اولین قدم

ساختار پایه‌ی اتصال با PDO در سه خط خلاصه می‌شود:

<?php
$dsn  = 'mysql:host=localhost;dbname=shop_db;charset=utf8mb4';
$user = 'your_username';
$pass = 'your_password';

try {
    $pdo = new PDO( $dsn, $user, $pass );
    $pdo->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
    $pdo->setAttribute( PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC );
    $pdo->setAttribute( PDO::ATTR_EMULATE_PREPARES, false );
    echo 'اتصال با موفقیت برقرار شد.';
} catch ( PDOException $e ) {
    error_log( 'DB Error: ' . $e->getMessage() );
    die( 'سرویس موقتا در دسترس نیست.' );
}

در این کد، نکته‌ی مهم این است که اتصال در یک بلوک try/catch قرار گرفته است. اگر اتصال شکست بخورد، PDO یک استثنا پرتاب می‌کند که در بلوک catch گرفته می‌شود. این رویکرد، خطاها را مدیریت‌پذیر می‌کند. اما نکته‌ی حتی مهم‌تر، سه setAttribute است که در ادامه به تفصیل باز می‌کنم. اگر تازه با PHP شروع کرده‌اید، نمونه‌های بیشتری در اتصال PHP به MySQL آورده‌ام؛ اما این مقاله، تمرکز عمیق‌تری روی خودِ PDO دارد.

تنظیمات حیاتی اتصال

سه تنظیمات اساسی که بعد از ساخت اتصال باید تعیین کنید، تفاوت بین یک کد آماتور و یک کد حرفه‌ای را می‌سازند:

تنظیماتمقدار پیشنهادیچرا مهم است
ATTR_ERRMODEERRMODE_EXCEPTIONخطاها به‌جای سکوت، به‌صورت استثنا پرتاب می‌شوند
ATTR_DEFAULT_FETCH_MODEFETCH_ASSOCنتیجه به‌صورت آرایه‌ی انجمنی خوانا برمی‌گردد
ATTR_EMULATE_PREPARESfalsePrepared Statements واقعی، نه شبیه‌سازی در PHP

تنظیم ATTR_ERRMODE: در PHP قدیمی، پیش‌فرض PDO این بود که خطاها را در سکوت نادیده بگیرد. یعنی اگر کوئری شما شکست می‌خورد، هیچ خطایی نمایش داده نمی‌شد و شما نمی‌فهمیدید که کوئری اصلا اجرا شده یا نه. با تنظیم ERRMODE_EXCEPTION، PDO در صورت خطا یک استثنا پرتاب می‌کند و کد شما مجبور می‌شود آن را مدیریت کند. تجربه‌ی من: این تنظیم، تفاوت بین دیباگ چند دقیقه‌ای و چند ساعته را می‌سازد.

تنظیم ATTR_DEFAULT_FETCH_MODE: پیش‌فرض PDO، نتیجه را هم به‌صورت آرایه‌ی عددی و هم آرایه‌ی انجمنی برمی‌گرداند. این دوگانگی، در کدهای بزرگ باعث سردرگمی می‌شود. با تنظیم FETCH_ASSOC، فقط آرایه‌ی انجمنی برمی‌گردد که بسیار خواناتر است — دسترسی به داده با نام ستون، نه با شماره.

تنظیم ATTR_EMULATE_PREPARES: این یکی از پرتکرارترین اشتباهات توسعه‌دهندگان است. در پیش‌فرض قدیمی PHP، PDO Prepared Statements را در سطح PHP شبیه‌سازی می‌کرد، نه در سطح MySQL. این شبیه‌سازی، باعث می‌شود که امنیت Prepared Statements به‌طور کامل تأمین نشود و خطاهای دقیق‌تری هم گزارش نشود. با تنظیم false، PDO از Prepared Statements واقعی استفاده می‌کند که هم امن‌تر و هم دقیق‌تر است. برای درک عمیق‌تر این آسیب‌پذیری، SQL Injection چیست و چگونه از آن جلوگیری کنیم نقطه‌ی شروع دقیقی است.

سه تنظیم، سه لایه‌ی دفاعی: خطاها را پنهان نکن، نتیجه را خوانا بگیر، و Prepared Statements را جدی بگیر. این سه، تفاوت بین کدی است که با شما حرف می‌زند و کدی که ساکت می‌ماند.

متدهای اصلی کوئری

PDO چند متد اصلی برای اجرای کوئری دارد. تفاوت این متدها، در نحوه‌ی استفاده و نوع خروجی است:

متد exec(): برای کوئری‌هایی که نتیجه‌ی جدولی برنمی‌گردانند (مثل INSERT، UPDATE، DELETE و CREATE). خروجی آن، تعداد رکوردهای تحت تأثیر است:

$affected = $pdo->exec( 'DELETE FROM products WHERE stock = 0' );
echo $affected . ' محصول حذف شد.';

متد query(): برای کوئری‌های SELECT که نتیجه برمی‌گردانند. خروجی، یک PDOStatement است که می‌توانید با fetch() یا fetchAll() از آن استفاده کنید:

$stmt = $pdo->query( 'SELECT * FROM products LIMIT 10' );
$products = $stmt->fetchAll();

متد prepare(): برای کوئری‌هایی که ورودی کاربر دارند. این متد، هسته‌ی امنیت PDO است و در بخش بعدی به تفصیل باز می‌کنم:

$stmt = $pdo->prepare( 'SELECT * FROM products WHERE price > :min_price' );
$stmt->execute( [ ':min_price' => 500000 ] );
$products = $stmt->fetchAll();

نکته‌ی حیاتی: هرگز از query() یا exec() برای کوئری‌هایی که ورودی کاربر دارند استفاده نکنید. ورودی کاربر باید همیشه از مسیر prepare() عبور کند. تجربه‌ی من: تفاوت بین یک کد امن و یک کد آسیب‌پذیر، اغلب در همین انتخاب یک متد است.

حالت‌های Fetch: قلب خوانایی نتیجه

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

  • PDO::FETCH_ASSOC: نتیجه به‌صورت آرایه‌ی انجمنی با کلید ستون‌ها. این حالت پیش‌فرض من است چون خواناترین حالت است. مثلا $row['name'].
  • PDO::FETCH_NUM: نتیجه به‌صورت آرایه‌ی عددی. مناسب برای پردازش داده‌های عددی مثل گزارش‌گیری.
  • PDO::FETCH_OBJ: هر رکورد به‌صورت یک آبجکت. مناسب برای کد شی‌گرا. مثلا $row->name.
  • PDO::FETCH_CLASS: هر رکورد به‌صورت نمونه‌ای از یک کلاس مشخص. بسیار مفید در پروژه‌های شی‌گرا.
  • PDO::FETCH_KEY_PAIR: برای کوئری‌هایی که دقیقا دو ستون دارند و شما می‌خواهید یک آرایه‌ی کلید-مقدار بگیرید.

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

class Product {
    public int $id;
    public string $name;
    public float $price;
}

$stmt = $pdo->prepare( 'SELECT id, name, price FROM products WHERE id = :id' );
$stmt->execute( [ ':id' => 5 ] );
$product = $stmt->fetchObject( Product::class );

echo $product->name;

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

Prepared Statements: چرا اینقدر مهم است؟

اگر بخواهم یک ویژگی PDO را به‌عنوان مهم‌ترین عامل انتخاب آن معرفی کنم، Prepared Statements است. ایده‌ی اصلی ساده است: به جای اینکه ورودی کاربر را داخل کوئری بچسبانید، ابتدا کوئری را با جای‌نگهدار (placeholder) آماده می‌کنید و سپس مقادیر را جداگانه ارسال می‌کنید. سرور دیتابیس، ورودی شما را به‌عنوان «داده» پردازش می‌کند، نه به‌عنوان «کد».

تفاوت عملی را در این دو مثال ببینید:

// روش ناامن (هرگز استفاده نکنید)
$name = $_POST['name'];
$query = "SELECT * FROM products WHERE name = '$name'";
$result = $pdo->query( $query );

// روش امن (Prepared Statement)
$stmt = $pdo->prepare( 'SELECT * FROM products WHERE name = :name' );
$stmt->execute( [ ':name' => $_POST['name'] ] );
$result = $stmt->fetchAll();

در روش اول، اگر کاربر مقدار ' OR '1'='1 را وارد کند، کوئری نهایی به این شکل می‌شود:

SELECT * FROM products WHERE name = '' OR '1'='1'

و این کوئری، تمام رکوردهای جدول را برمی‌گرداند — بدون اینکه کاربر کلمه‌ی عبوری وارد کرده باشد. بدتر، اگر کاربر مقدار '; DROP TABLE products; -- را وارد کند، جدول شما کاملا حذف می‌شود.

در روش دوم (Prepared Statement)، همان ورودی، به‌عنوان یک رشته‌ی ساده‌ی «داده» پردازش می‌شود و هیچ‌وقت به کد تبدیل نمی‌شود. این تفاوت، همان چیزی است که Prepared Statements را به استاندارد طلایی امنیت تبدیل می‌کند.

نکته‌ی مهم: Prepared Statements را باید در تمام کوئری‌هایی که ورودی کاربر دارند استفاده کنید، نه فقط در SELECT. در INSERT، UPDATE و DELETE هم همین قانون است. تجربه‌ی من: توسعه‌دهندگانی که فقط در SELECT از Prepared Statements استفاده می‌کنند، در معرض خطر جدی هستند — چون درج داده و به‌روزرسانی، اگر از ورودی کاربر تغذیه کنند، می‌توانند دیتابیس را خراب کنند.

Prepared Statements، سه کار را با هم انجام می‌دهد: جلوگیری از SQL Injection، افزایش کارایی با آماده‌سازی مکرر، و خوانایی بیشتر کد. سه برنده در یک ابزار.

Named در برابر Positional: کدام بهتر است؟

PDO دو راه برای تعریف جای‌نگهدارها دارد: پارامترهای موقعیتی (positional) با ? و پارامترهای نام‌دار (named) با :name. مقایسه‌ی این دو، یکی از پرتکرارترین سوالات در کار با PDO است:

معیارPositional (?)Named (:name)
خواناییپایین — باید ترتیب را به خاطر بسپاریدبالا — نام پارامتر خودش توضیح می‌دهد
خطر اشتباه ترتیببالاپایین
تکرار پارامترنیاز به ارسال مکرریک‌بار ارسال، چند‌بار استفاده
کاربرد رایجکوئری‌های ساده با پارامتر کمکوئری‌های پیچیده و طولانی

در تجربه‌ی من، Named Parameters تقریبا همیشه انتخاب بهتری هستند، مگر در کوئری‌های بسیار ساده با یک یا دو پارامتر. در کوئری‌هایی با پنج پارامتر یا بیشتر، Named Parameters تفاوت چشمگیری در خوانایی و کاهش خطا می‌سازند:

// با Positional، باید ترتیب را دقیق رعایت کنید
$stmt = $pdo->prepare( 'INSERT INTO orders (product_id, qty, price, status) VALUES (?, ?, ?, ?)' );
$stmt->execute( [ 5, 2, 1250000, 'pending' ] );

// با Named، ترتیب مهم نیست و کد خودش را توضیح می‌دهد
$stmt = $pdo->prepare(
    'INSERT INTO orders (product_id, qty, price, status) VALUES (:pid, :qty, :price, :status)'
);
$stmt->execute( [
    ':status' => 'pending',
    ':qty'    => 2,
    ':pid'    => 5,
    ':price'  => 1250000,
] );

نکته‌ی ظریف: هرگز در یک کوئری، هر دو سبک را با هم قاطی نکنید. PDO اجازه نمی‌دهد که در یک کوئری هم از ? و هم از :name استفاده کنید. انتخاب کنید و به آن پایبند بمانید.

مدیریت خطا در PDO

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

سطح اول — حالت سکوت (پیش‌فرض قدیمی): اگر ATTR_ERRMODE را تنظیم نکنید، PDO در صورت خطا ساکت می‌ماند و فقط با متد errorInfo() می‌توانید خطا را ببینید. این حالت، خطرناک‌ترین حالت است و در پروژه‌های واقعی چندین بار منجر به از دست رفتن داده‌های مهم شده است.

سطح دوم — حالت هشدار: با ERRMODE_WARNING، PDO یک هشدار PHP صادر می‌کند اما اجرای کد را متوقف نمی‌کند. این حالت هم قابل اعتماد نیست.

سطح سوم — حالت استثنا (توصیه‌ی من): با ERRMODE_EXCEPTION، PDO در صورت خطا یک استثنا پرتاب می‌کند که کد شما می‌تواند آن را در بلوک try/catch مدیریت کند:

try {
    $stmt = $pdo->prepare( 'INSERT INTO products (name, price) VALUES (:name, :price)' );
    $stmt->execute( [ ':name' => 'هدفون', ':price' => 1250000 ] );
} catch ( PDOException $e ) {
    error_log( 'Insert Error: ' . $e->getMessage() );
    // در محیط تولید، پیام عمومی
    echo 'خطا در ثبت داده. لطفا بعدا تلاش کنید.';
}

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

یک نکته‌ی حرفه‌ای دیگر: در محیط توسعه، خطاها را با error_log در یک فایل جداگانه ذخیره کنید. در محیط تولید، از یک سیستم لاگ متمرکز مثل Sentry یا Monolog استفاده کنید. این لایه، در بلندمدت بسیار ارزشمند است.

تراکنش‌ها: عملیات اتمیک

یکی از ویژگی‌های قدرتمند PDO، پشتیبانی از تراکنش‌ها (Transactions) است. تراکنش یعنی چند عملیات دیتابیس که به‌صورت «همه یا هیچ» انجام می‌شوند. اگر یکی از آن‌ها شکست بخورد، بقیه هم لغو می‌شوند. مثال کلاسیک: انتقال پول بین دو حساب. سه عملیات باید همزمان انجام شوند: کم کردن از حساب مبدا، اضافه کردن به حساب مقصد، و ثبت تراکنش. اگر یکی از این‌ها شکست بخورد، بقیه هم باید لغو شوند. الگوی پیاده‌سازی:

try {
    $pdo->beginTransaction();

    // مرحله‌ی اول
    $stmt = $pdo->prepare( 'UPDATE products SET stock = stock - :qty WHERE id = :id AND stock >= :qty' );
    $stmt->execute( [ ':qty' => 2, ':id' => 5 ] );

    if ( $stmt->rowCount() === 0 ) {
        throw new Exception( 'موجودی کافی نیست' );
    }

    // مرحله‌ی دوم
    $stmt = $pdo->prepare( 'INSERT INTO orders (product_id, qty) VALUES (:pid, :qty)' );
    $stmt->execute( [ ':pid' => 5, ':qty' => 2 ] );

    $pdo->commit();
} catch ( Exception $e ) {
    $pdo->rollBack();
    error_log( 'Transaction failed: ' . $e->getMessage() );
    echo 'خطا در ثبت سفارش.';
}

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

  • beginTransaction(): شروع تراکنش. از این لحظه، تمام تغییرات «موقتی» هستند.
  • commit(): تأیید تراکنش. تغییرات برای همیشه ثبت می‌شوند.
  • rollBack(): لغو تراکنش. تمام تغییرات از زمان beginTransaction لغو می‌شوند.

نکته‌ی حیاتی: تراکنش‌ها فقط با موتور InnoDB کار می‌کنند، نه MyISAM. اگر جدول شما موتور MyISAM دارد، تراکنش‌ها هیچ اثری ندارند و شما با یک توهم امنیت کار می‌کنید. تجربه‌ی من: در پروژه‌های فروشگاهی و مالی، همیشه از InnoDB استفاده کنید، حتی اگر به تراکنش نیاز ندارید. برای درک تفاوت این دو موتور، تفاوت InnoDB و MyISAM را ببینید.

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

PDO در قالب شی‌گرا

در پروژه‌های واقعی، اتصال PDO معمولا در قالب یک کلاس اختصاصی مدیریت می‌شود. مزایای این رویکرد: متمرکزسازی تنظیمات، استفاده‌ی مجدد، و جداسازی منطق از نمایش. یک الگوی ساده و کارآمد که در پروژه‌های خودم زیاد استفاده می‌کنم:

class Database {
    private static ?PDO $instance = null;

    public static function getConnection(): PDO {
        if ( self::$instance === null ) {
            $dsn = 'mysql:host=' . DB_HOST . ';dbname=' . DB_NAME . ';charset=utf8mb4';

            try {
                self::$instance = new PDO( $dsn, DB_USER, DB_PASS );
                self::$instance->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
                self::$instance->setAttribute( PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC );
                self::$instance->setAttribute( PDO::ATTR_EMULATE_PREPARES, false );
            } catch ( PDOException $e ) {
                error_log( 'DB Error: ' . $e->getMessage() );
                throw new RuntimeException( 'اتصال به دیتابیس برقرار نشد.' );
            }
        }

        return self::$instance;
    }
}

این الگو، Singleton نام دارد: اتصال فقط یک بار ساخته می‌شود و در طول اجرای اسکریپت استفاده می‌شود. در PHP، که هر درخواست در یک پروسه‌ی مستقل اجرا می‌شود، این الگو به کاهش سربار کمک می‌کند — چون ساخت اتصال جدید در هر تابع، هزینه‌ی زیادی دارد. اگر با مفهوم Singleton و سایر الگوهای طراحی آشنا نیستید، آموزش شی‌گرایی در PHP مسیر کاملی از الگوهای طراحی را نشان می‌دهد.

در پروژه‌های بزرگ، معمولا از این لایه یک قدم جلوتر می‌روند و از یک Repository Pattern استفاده می‌کنند. یعنی برای هر جدول، یک کلاس اختصاصی ساخته می‌شود که تمام کوئری‌های مربوط به آن جدول را در خود جمع می‌کند:

class ProductRepository {
    public function __construct( private PDO $pdo ) {}

    public function find( int $id ): ?array {
        $stmt = $this->pdo->prepare( 'SELECT * FROM products WHERE id = :id' );
        $stmt->execute( [ ':id' => $id ] );
        $result = $stmt->fetch();
        return $result ?: null;
    }

    public function all(): array {
        return $this->pdo->query( 'SELECT * FROM products' )->fetchAll();
    }
}

$repo = new ProductRepository( Database::getConnection() );
$product = $repo->find( 5 );

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

PDO در بستر وردپرس

اگر در بستر وردپرس کار می‌کنید، یک نکته‌ی مهم را باید بدانید: در وردپرس، به‌طور پیش‌فرض نباید از PDO استفاده کنید. وردپرس یک لایه‌ی انتزاعی به نام wpdb دارد که تمام کارهای مربوط به دیتابیس را انجام می‌دهد و به‌طور خودکار با ساختار وردپرس هم‌خوان است. دلایل این توصیه:

  • مدیریت خودکار پیشوند جدول‌ها: در وردپرس، جدول‌ها پیشوند دارند (مثل wp_posts) و این پیشوند ممکن است در نصب‌های مختلف متفاوت باشد. wpdb این را مدیریت می‌کند، PDO نمی‌کند.
  • هم‌خوانی با کش داخلی: وردپرس یک لایه‌ی کش داخلی دارد که wpdb از آن استفاده می‌کند. استفاده از PDO این لایه را دور می‌زند.
  • هم‌خوانی با افزونه‌ها: بسیاری از افزونه‌ها به wpdb دسترسی دارند و ممکن است انتظار داشته باشند که از این لایه استفاده کنید.

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

// اتصال به یک دیتابیس خارجی، جدا از دیتابیس وردپرس
function get_external_connection(): PDO {
    $dsn = 'mysql:host=' . EXTERNAL_DB_HOST . ';dbname=' . EXTERNAL_DB_NAME . ';charset=utf8mb4';
    return new PDO( $dsn, EXTERNAL_DB_USER, EXTERNAL_DB_PASS, [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        PDO::ATTR_EMULATE_PREPARES => false,
    ] );
}

موضوع استفاده از wpdb در وردپرس را در کدنویسی وردپرس چیست و از کجا شروع کنیم به تفصیل باز کرده‌ام. و اگر می‌خواهید یک جدول اختصاصی در وردپرس بسازید (که گاهی لازم است)، روند کاملش در آموزش مدیریت دیتابیس وردپرس آمده است. اگر هم می‌خواهید از دیتابیس وردپرس در یک اپلیکیشن بیرونی استفاده کنید، مسیر API مسیر مدرن‌تری است — موضوعی که در API چیست و چه کاربردی دارد و آموزش REST API باز شده است.

بهینه‌سازی و اتصال پایدار

در پروژه‌های با ترافیک بالا، دو مفهوم مهم درباره‌ی بهینه‌سازی اتصال PDO وجود دارد: اتصال پایدار (Persistent Connection) و Pool اتصال.

اتصال پایدار: در PHP، به‌طور پیش‌فرض، اتصال دیتابیس در انتهای هر درخواست بسته می‌شود. با اتصال پایدار، اتصال بین درخواست‌ها باقی می‌ماند و در درخواست بعدی استفاده می‌شود. مزیت: کاهش زمان ساخت اتصال. معایب: مصرف حافظه‌ی بیشتر روی سرور، و پتانسیل مشکلات در محیط‌های multi-tenant. تجربه‌ی من: در سایت‌های معمولی، فعال کردن اتصال پایدار تفاوت محسوسی نمی‌سازد؛ در سایت‌های بسیار پربازدید، می‌تواند تفاوت مهمی بسازد. فعال‌سازی:

$options = [
    PDO::ATTR_PERSISTENT => true,
    PDO::ATTR_ERRMODE    => PDO::ERRMODE_EXCEPTION,
];

$pdo = new PDO( $dsn, $user, $pass, $options );

Pool اتصال: در معماری‌های پیشرفته، Pool اتصال (اتصال از یک مجموعه‌ی آماده) جایگزین اتصال مستقیم می‌شود. ابزارهایی مثل ProxySQL یا MySQL Router این لایه را مدیریت می‌کنند. این رویکرد، در سایت‌های بسیار پربازدید و محیط‌های میکروسرویس رایج است.

سه نکته‌ی مکمل در بهینه‌سازی PDO:

  1. استفاده از LIMIT در کوئری‌ها: حتی اگر لایه‌ی برنامه محدودیت دارد، همیشه یک LIMIT منطقی در کوئری هم بگذارید. این عادت، جلوی خطای Memory Limit را می‌گیرد. موضوع مرتبط در خطای Memory Limit در وردپرس به تفصیل باز شده است.
  2. بستن صریح Statement ها: بعد از پایان کار با یک PDOStatement، می‌توانید آن را با closeCursor() آزاد کنید. این کار در پروژه‌های با کوئری‌های زیاد، فشار حافظه را کم می‌کند.
  3. کاهش تعداد کوئری‌ها: به جای اجرای یک کوئری در حلقه، سعی کنید همه‌ی داده‌ها را با یک کوئری بگیرید. بهینه‌سازی کوئری‌های MySQL راهنمای دقیقی از این الگوها را نشان می‌دهد.

و یک نکته‌ی مهم: بهینه‌سازی PDO بدون بهینه‌سازی کوئری، بی‌فایده است. اتصال سریع، اگر کوئری کندی داشته باشد، نتیجه‌ای نمی‌دهد. برای درک جامع‌تر، تاثیر دیتابیس بر سرعت سایت تصویر کاملی از لایه‌ی دیتابیس ارائه می‌دهد.

اشتباهات رایج در استفاده از PDO

در سال‌ها کار با PDO، الگوهای مشخصی از اشتباهات را دیده‌ام. جدول زیر، این اشتباهات را با پیامد و راه‌حل نشان می‌دهد:

اشتباهپیامدروش درست
استفاده از query() برای ورودی کاربرSQL Injection و افشای دادههمیشه prepare()
تنظیم نکردن ATTR_ERRMODEخطاهای پنهان، دیباگ دشوارهمیشه ERRMODE_EXCEPTION
فعال گذاشتن ATTR_EMULATE_PREPARESکاهش امنیت، خطاهای مبهمتنظیم به false
استفاده از utf8 به‌جای utf8mb4مشکل با کاراکترهای خاصهمیشه utf8mb4
نمایش خطای خام به کاربرافشای اطلاعات حساسلاگ خطا و پیام عمومی
نبود WHERE در UPDATE یا DELETEتغییر یا حذف تمام داده‌هاهمیشه WHERE صریح
اتصال در هر تابع به‌صورت مستقلهزینه‌ی زیاد، مصرف منابعالگوی Singleton
نگذاشتن LIMIT در SELECTهای بزرگخطای Memory Limitهمیشه LIMIT منطقی
قاطی کردن ? و :name در یک کوئریخطای اجراانتخاب یک سبک و پایبندی به آن
قرار دادن رمز در کد publicافشای دیتابیسمتغیر محیطی یا فایل جداگانه

یک اشتباه ظریف که در دیدگاه‌ها زیاد می‌بینم: کاربران، یک کوئری را در یک حلقه اجرا می‌کنند بدون اینکه بفهمند که این کار می‌تواند صدها کوئری به دیتابیس بفرستد. مثلا در یک حلقه‌ی foreach برای هزار محصول، هزار کوئری UPDATE اجرا می‌شود. راه‌حل: همیشه یک کوئری ترکیبی یا Bulk Update بنویسید. این عادت، در پروژه‌های واقعی تفاوت بین یک سایت سریع و یک سایت کند را می‌سازد.

در کار با PDO، پیش‌فرض‌ها همیشه امن نیستند. سه تنظیمات کوچک، یک متد درست، و یک عادت صریح — همین‌ها تفاوت بین کد آسیب‌پذیر و کد حرفه‌ای را می‌سازند.

نقشه‌ی ادامه‌ی مسیر

بعد از تسلط بر مبانی PDO، سه مسیر اصلی برای ادامه وجود دارد:

مسیر اول — لایه‌ی انتزاعی (ORM): در پروژه‌های بزرگ، معمولا از یک لایه‌ی انتزاعی روی PDO استفاده می‌شود. Doctrine ORM و Eloquent در لاراول، دو نمونه‌ی رایج هستند. این ابزارها، کار با دیتابیس را به سطح مدل‌ها و آبجکت‌ها می‌برند و کد را بسیار تمیزتر می‌کنند. شروع این مسیر نیازمند تسلط بر OOP است — همان چیزی که در آموزش شی‌گرایی در PHP باز کرده‌ام.

مسیر دوم — بهینه‌سازی کوئری: حتی اگر لایه‌ی انتزاعی استفاده کنید، درک عمیق کوئری‌های MySQL ضروری است. موضوعات مهم: Index گذاری، Join بهینه، و تحلیل کوئری‌های کند. Index گذاری در MySQL و بهینه‌سازی کوئری‌های MySQL دو منبع کلیدی این مسیر هستند.

مسیر سوم — کار با API: در معماری‌های مدرن، اپلیکیشن‌ها به‌جای اتصال مستقیم به دیتابیس، از API استفاده می‌کنند. این رویکرد، جداسازی منطق را ممکن می‌کند و امنیت را بالا می‌برد. مسیر کامل در API چیست و چه کاربردی دارد و آموزش REST API آمده است. اگر با زبان دیگری هم کار می‌کنید، مقایسه‌ی اتصال PHP با اتصال Python به MySQL می‌تواند دید جالبی بدهد.

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

و حالا یک تمرین عملی که می‌تواند در همین امشب، مسیر یادگیری‌تان را روشن کند: یک فایل PHP بسازید که یک کلاس Repository کوچک برای یک جدول دلخواه داشته باشد — با چهار متد find، all، create و delete. سپس این کلاس را طوری بنویسید که تمام کوئری‌هایش از prepare() استفاده کنند و خطاها با try/catch گرفته شوند. اگر این را انجام دهید، سه چیز را با هم تمرین کرده‌اید: الگوی Repository، Prepared Statements، و مدیریت خطا — سه ستون اول یک لایه‌ی داده‌ی حرفه‌ای. سه ماه بعد که به این لحظه برگشتید، دو حالت وجود دارد: یا این کلاس کوچک، اولین قدمِ یک معماری بلندمدت بوده، یا هنوز در همان جای اول مانده‌اید و آن‌وقت سوال درست این نیست «چرا پیشرفت نکردم؟» — سوال درست این است: «آیا واقعا شروع کردم؟»

و اگر در مسیر، به یک رفتار غیرمنتظره رسیدید — مثلا اینکه چرا rowCount() در UPDATE گاهی صفر برمی‌گرداند در حالی که تغییرات اعمال شده، یا چرا یک کوئری با LIMIT بزرگ سرور را کند می‌کند — همان مشاهدات را در دیدگاه‌ها بنویسید. PDO، از آن دسته موضوعاتی است که هر مشکل عملی، یک درس عمیق در خودش دارد؛ و آن درس وقتی با چند نفر به اشتراک گذاشته شود، چند برابر می‌شود. اگر هم در حین کار با خطای اتصال یا خطای کوئری روبه‌رو شدید، خطاهای رایج MySQL نقطه‌ی شروع دقیقی است — بسیاری از آن خطاها، ریشه‌شان در همان جزئیاتی است که در این مقاله باز کرده‌ام. 🚀