قبل از توضیح مبحث 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" عمل کنید.
ممکن است این جمله برایتان گُنگ باشد بنابراین می خواهم توضیح بیشتری در موردش بدهم:
یکی از بحث برانگیز ترین تنظیمات در PDO دستور PDO::ATTR_EMULATE_PREPARES
است. برای اینکه بفهمید این دستور چکار می کند باید ابتدا با نحوه ی اجرای کوئری ها در PDO آشنا شوید. PDO می تواند کوئری های شما را به دو شکل اجرا می کند:
()prepare
فراخوانی می شود، کوئری شما به همراه placeholder ها و به همان شکلی که هست به mysql ارسال می شود؛ بنابراین تمامی علامت های سوال نیز در آن هستند (حتی اگر از placeholder های اسمی نیز استفاده کنید، به علامت سوال تبدیل می شوند). این در حالی است که داده های حقیقی بعدا ارسال می شوند؛ یعنی زمانی که ()execute
صدا زده شود.()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 توضیح داده ایم. اگر به یادآوری و توضیحات اضافی نیاز دارید به این مقاله سری بزنید.
در این حالت شما می توانید از قابلیتی به نام 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());
در این حالت شما می توانید نوع پارامتر ها (parameter type) را نادیده بگیرید چرا که خود MySQL تمام آن ها را مرتب می کند. بنابراین حتی رشته ها هم می توانند به LIMIT متصل (bind) شوند. اگر یادتان باشد در قسمت های قبلی توضیح دادیم که:
اگر در حالت emulation mode باشید (در حالت پیش فرض emulation mode فعال است)، PDO به جای آن که داده ها را به placeholder ها بفرستد، placeholder ها را مستقیما با داده ها تعویض می کند. از طرفی PDO در هنگام استفاده از آرایه ها در ()execute تمام پارامتر ها را رشته فرض می کند. نتیجه ی این دو مسئله این است که دستور ?,? LIMIT
تبدیل به دستور '10', '10' LIMIT
می شود و از طرفی همه می دانیم که این ساختار برای یک کوئری اشتباه است و نتیجتا کوئری اجرا نخواهد شد.
دو راه حل برای این مشکل وجود دارد:
مثال از غیر فعال کردن 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) را رها و یا غیر فعالش کنید.
در این قسمت، به پرسشهای تخصصی شما دربارهی محتوای مقاله پاسخ داده نمیشود. سوالات خود را اینجا بپرسید.