Emulation Mode در PDO

25 بهمن 1397
درسنامه درس 14 از سری آموزش PDO
PDO-emulation-mode

قبل از توضیح مبحث emulation mode باید به مبحثی در رابطه با اجرای چندگانه ی کوئری ها اشاره کنم:

اگر هنگام استفاده از PDO در حالت emulation mode باشید، PDO می تواند چندین کوئری را در یک statement اجرا کند؛ چه از طریق دستور ()query (به طور مستقیم) و چه از طریق دستورات ()prepare و ()execute (به صورت غیر مستقیم). برای دسترسی به نتایج کوئری های چند تایی باید از دستور ()PDOStatement::nextRowset استفاده کنید:

$stmt = $pdo->prepare("SELECT ?;SELECT ?");
$stmt->execute([1,2]);
do {
    $data = $stmt->fetchAll();
    var_dump($data);
} while ($stmt->nextRowset());

با چنین حلقه ای می توانید تمام اطلاعات مربوطه را از هر کوئری به دست بیاورید؛ مانند ردیف های تحت تاثیر قرار گرفته، id های تولید شده یا خطاهای اتفاق افتاده.

باید توجه داشته باشید که در قسمت دستور ()execute، تنها خطاهای کوئری اول نمایش داده خواهند شد. اگر در کوئری های بعد خطایی پیش بیاید و بخواهید ببینید این خطا چه بوده است باید در نتیجه ها iterate (گردش) کنید.

البته این موضوع نقطه ضعف نیست و در واقع PDO نباید تمام خطاها را یک جا نمایش دهد چرا که خیلی از برنامه نویسان نمی توانند کل مشکل را یک جا هضم کنند و نمی دانند که خطای مورد نظر تنها خروجی کوئری نیست، بلکه ممکن است مجموعه داده هایی برگردانده شوند، ممکن است متادیتاهایی مانند insert id برگردانده شوند و ... .

برای دریافت این موارد شما باید در نتایج iterate (گردش) کنید. اما اگر قرار بود که PDO تمام خطاها را یکجا نمایش بدهد، مجبور بود خودش به صورت خودکار گردش کند که در نتیجه ی آن بعضی از نتایج دور ریخته می شد.

نکته: بر خلاف ()mysqli_multi_query و چیزی که در mysql شاهد آن بودیم، PDO درخواست های غیرهمگام (asynchronous) ارسال نمی کند بنابراین نمی توانید به صورت "fire and forget" عمل کنید.

ممکن است این جمله برایتان گُنگ باشد بنابراین می خواهم توضیح بیشتری در موردش بدهم:

  • درخواست غیر همگام (asynchronous)، یعنی درخواستی که می توانید کنار درخواست های دیگر ارسال شود و برای اینکه ارسال شود نیازی ندارد تا صبر کند درخواست های قبلی تمام شوند. مثلا صف نانوایی را در نظر بگیرید؛ درخواست های شما در صف نانوایی غیرهمگام نیست چرا که تا نفر جلوتر نان خود را نگیرد شما نمی توانید نانی دریافت کنید. حال اگر تعداد نانواها زیاد باشد و نانوا همزمان از دو نفر یا بیشتر درخواست نان بگیرد و برایشان همزمان نان بپزد، این درخواست غیرهمگام می شد.
  • متد fire and forget که بین برنامه نویسان معروف است اشاره به وقتی می کند که شما چندین کوئری را به mysql می فرستید و سپس اتصال را قطع می کنید؛ در این حالت PHP صبر می کند تا تمام کوئری ها اجرا شوند و سپس اتصال را قطع می کند. این در حالی است که PDO چنین کاری انجام نمی دهد و باید مراقب کد هایتان باشید.

emulation mode و دستور PDO::ATTR_EMULATE_PREPARES

یکی از بحث برانگیز ترین تنظیمات در PDO دستور PDO::ATTR_EMULATE_PREPARES است. برای اینکه بفهمید این دستور چکار می کند باید ابتدا با نحوه ی اجرای کوئری ها در PDO آشنا شوید. PDO می تواند کوئری های شما را به دو شکل اجرا می کند:

  1. استفاده از prepared statement های طبیعی و واقعی:
    در این حالت زمانی که()prepare فراخوانی می شود، کوئری شما به همراه placeholder ها و به همان شکلی که هست به mysql ارسال می شود؛ بنابراین تمامی علامت های سوال نیز در آن هستند (حتی اگر از placeholder های اسمی نیز استفاده کنید، به علامت سوال تبدیل می شوند). این در حالی است که داده های حقیقی بعدا ارسال می شوند؛ یعنی زمانی که ()execute صدا زده شود.
  2. استفاده از prepared statement های شبیه سازی شده (emulated):
    در این حالت زمانی که کوئری شما به mysql ارسال می شود، به عنوان SQL صحیح، با فرمت بندی مناسب و با تمامی داده های حقیقی ارسال می شود. بنابراین با دستور ()execute تنها یک رفت و برگشت به پایگاه داده اتفاق می افتد. برای برخی از driver ها مانند mysql، حالت emulation mode به طور پیش فرض فعال است.

هر دوی این روش ها مزایا و معایب خودشان را دارند اما هر دوی آن ها، در صورت استفاده ی صحیح، امن هستند. بله متاسفانه ممکن است شنیده باشید که یکی از این روش ها از دیگر نا امن تر است اما باید بگویم این حرف ها صحیح و دقیق نیست.

تنها کاری که باید برای emulation mode انجام دهید این است که encoding مربوط به DSN را به درستی انجام دهید.

در این صورت prepared statement های شبیه سازی شده (emulated) شما کاملا امن خواهند بود. در رابطه با DSN توضیح کوتاهی عرض می کنم:

PDO برای اتصال به پایگاه داده (ساخت connection) از شما می خواهد سه نوع داده ی مختلف را به آن بدهید:

  • database driver و host و db (schema) name و charset باید داخل خود رشته ی DSN قرار بگیرند. موارد دلخواه دیگری مانند port و unix_socket نیز در این قسمت قرار خواهند گرفت.
  • username و password تحویل constructor داده می شوند.
  • موارد دلخواه دیگر داخل آرایه ی آپشن ها قرار می گیرند.

مثالی از یک اتصال کامل را در قسمت زیر می بینید:

$host = '127.0.0.1';
$db   = 'test';
$user = 'root';
$pass = '';
$charset = 'utf8mb4';

$dsn = "mysql:host=$host;dbname=$db;charset=$charset";
$options = [
    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::ATTR_EMULATE_PREPARES   => false,
];
try {
     $pdo = new PDO($dsn, $user, $pass, $options);
} catch (\PDOException $e) {
     throw new \PDOException($e->getMessage(), (int)$e->getCode());
}

ما این مثال را در مقاله ی اتصال به پایگاه داده PDO – Credentials در PHP توضیح داده ایم. اگر به یادآوری و توضیحات اضافی نیاز دارید به این مقاله سری بزنید.

هنگامی که emulation mode فعال است

در این حالت شما می توانید از قابلیتی به نام prepared statement های اسمی استفاده کنید؛ این قابلیت می گوید، یک placeholder با نامی یکسان می تواند هر چند بار که بخواهد در یک کوئری یکسان استفاده شود و متغیر های مربوطه نیز تنها یک بار bind1 می شوند.

1- binding: فعل bind در فارسی معنیِ “جفت کردن” یا “به هم چسباندن” می دهد. منظور از binding در برنامه نویسی، چسباندن یک مقدار به متغیر آن است. اگر یادتان باشد در مقالات قبلی توضیح دادیم که هنگام اجرای کوئری با ()execute یک سری آرگومان را به آن پاس می دهیم.

این آرگومان ها همان مقادیری بودند که در کوئری باید به جای placeholder قرار می گرفتند یا به عبارت دیگر مقادیری بودند که باید به placeholder خودشان bind می شدند یا می چسبیدند. به این عمل binding می گفتیم.

مشکل اینجاست که اگر emulation mode غیر فعال باشد به دلایل نا معلومی این قابلیت نیز غیر فعال می شود:

$stmt = $pdo->prepare("SELECT * FROM t WHERE foo LIKE :search OR bar LIKE :search");
$stmt->execute(['search'] => "%$search%");

از طرفی اگر emulation mode فعال باشد PDO می تواند چندین کوئری را در یک statement اجرا کند (رجوع کنید به جلسات قبل). مثال:

$stmt = $pdo->prepare("SELECT ?;SELECT ?");
$stmt->execute([1,2]);
do {
    $data = $stmt->fetchAll();
    var_dump($data);
} while ($stmt->nextRowset());

همچنین از آن جایی که prepared statement ها در حالت عادی تنها برخی از انواع کوئری را پشتیبانی می کنند، می توان گفت برخی از کوئری ها تنها با emulation mode در حالت فعال اجرا می شوند. به طور مثال اگر emulation mode فعال باشد، کد زیر اسامی جدول ها را بر می گرداند اما اگر غیر فعال باشد به خطا برمیخوریم:

$stmt = $pdo->prepare("SHOW TABLES LIKE ?");
$stmt->execute(["%$name%"]);
var_dump($stmt->fetchAll());

هنگامی که emulation mode غیر فعال است

در این حالت شما می توانید نوع پارامتر ها (parameter type) را نادیده بگیرید چرا که خود MySQL تمام آن ها را مرتب می کند. بنابراین حتی رشته ها هم می توانند به LIMIT متصل (bind) شوند. اگر یادتان باشد در قسمت های قبلی توضیح دادیم که:

اگر در حالت emulation mode باشید (در حالت پیش فرض emulation mode فعال است)، PDO به جای آن که داده ها را به placeholder ها بفرستد، placeholder ها را مستقیما با داده ها تعویض می کند. از طرفی PDO در هنگام استفاده از آرایه ها در ()execute تمام پارامتر ها را رشته فرض می کند. نتیجه ی این دو مسئله این است که دستور ?,? LIMIT تبدیل به دستور '10', '10' LIMIT می شود و از طرفی همه می دانیم که این ساختار برای یک کوئری اشتباه است و نتیجتا کوئری اجرا نخواهد شد.

دو راه حل برای این مشکل وجود دارد:

  • اولین راه حل غیر فعال کردن emulation mode است. این کار مشکلی ایجاد نمی کند چرا که MySQL می تواند به تنهایی placeholder ها را مدیریت کند.
  • دومین راه حل این است که باید پارامتر ها را به صورت دستی و صریح bind کرده و نوع آن ها را نیز تعیین کنید.

مثال از غیر فعال کردن emulation mode:

$conn->setAttribute( PDO::ATTR_EMULATE_PREPARES, false );

مثال از bind کردن دستی و صریح:

$stmt = $pdo->prepare('SELECT * FROM table LIMIT ?, ?');
$stmt->bindParam(1, $offset,PDO::PARAM_INT);
$stmt->bindParam(2, $limit,PDO::PARAM_INT);
$stmt->execute();
$data = $stmt->fetchAll();

همچنین با غیر فعال بودن emulation mode می توانید از مزیت "اجرای چند باره ی prepared statement ها" استفاده کنید؛ برخی اوقات می توانید به جای اجرای چند باره ی یک کوئری، از prepared statement ها برای اجرای چند باره ی آن ها استفاده کنید. بدین صورت سرعت اجرای کار را تا حدی بهبود می بخشید و همچنین کوئری ما یک بار بیشتر تجزیه (parse) نمی شود. مثال:

$data = [
    1 => 1000,
    5 =>  300,
    9 =>  200,
];
$stmt = $pdo->prepare('UPDATE users SET bonus = bonus + ? WHERE id = ?');
foreach ($data as $id => $bonus)
{
    $stmt->execute([$bonus, $id]);
}

البته باید بگویم که این قابلیت آن چنان رایج نیست، آن هم به دو دلیل عمده؛ اول آن که نیاز عمیقی به آن حس نمی شود. چند موقعیت را می توانید نام ببرید که این قابلیت در آن حیاتی باشد؟ دوما در حال حاضر، در عصر تکنولوژی، تجزیه ی (parse) کوئری ها بسیار سریع تر از گذشته انجام می شود و تفاوت سرعت در این دو حالت آنقدر کم شده است که به سختی حس می شود.

دو راهی پایانی

آیا emulation mode را غیر فعال کنیم و یا اجازه بدهیم فعال باقی بماند؟ پاسخ این سوال بسیار سخت است و به شما بستگی دارد. من مزایا و معایب آن را برای شما ذکر کردم و شما باید با توجه به پایگاه داده و پروژه ی خود تصمیم بگیرید که emulation mode روشن باشد و یا خاموش.

اگر نظر شخصی من را بخواهید، می گویم برای من کاربرد دستورات اهمیت زیادی دارد و از آن جایی که خودم به این روش عادت کرده ام emulation mode را غیر فعال می کنم تا به مشکلاتی که قبلا توضیح دادم (مشکل کار با دستور LIMIT) برخورد نکنم اما غیر از آن دیگر مشکلات آن چنان جدی نیستند و می توانید با روش هایی که به شما گفته شد آن ها را دور بزنید.

خلاصه ی مقاله

در این قسمت با emulation mode و مزایا و معایب آن آشنا شدیم و دیدیم در هر حالت به چه قابلیت هایی دسترسی داشته و از چه قابلیت هایی محروم هستیم. در نهایت انتخاب با شماست ه حالت پیش فرض PDO (یعنی روشن بودن emulation mode) را رها و یا غیر فعالش کنید.

تمام فصل‌های سری ترتیبی که روکسو برای مطالعه‌ی دروس سری آموزش PDO توصیه می‌کند:
نویسنده شوید
دیدگاه‌های شما

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.