RE:NODE

mysqli vs PDO PHP-ში: განსხვავებები, შეცდომები და რომელი აირჩიო

ავტორი RE:NODE-ის გუნდი
15 წუთის საკითხავი


აირჩიე PDO, თუ არ გაქვს კონკრეტული მიზეზი, რომ არ აირჩიო. ორივე extension მხარდაჭერილია, ორივე MySQL-თან ერთსა და იმავე mysqlnd ბიბლიოთეკით მუშაობს, ორივე უსაფრთხოა prepared statement-ებთან და სიჩქარეში განსხვავება რეალურ აპლიკაციაში იმდენად მცირეა, რომ გაზომვას არ ექვემდებარება. PDO იგებს იმაში, რასაც ყოველდღე ეხები: named placeholder-ები, ერთი API MySQL-ის, PostgreSQL-ისა და SQLite-ისთვის, fetch mode-ები, რომლებიც მწკრივებს ციკლის გარეშე აქცევს მასივებად ან ობიექტებად, და PHP 8.0-დან ჩართული გამონაკლისები (exceptions) ნაგულისხმევად. mysqli იგებს მხოლოდ მაშინ, როცა გჭირდება რაიმე MySQL-სპეციფიკური, რასაც PDO არ გვთავაზობს - ასინქრონული მოთხოვნები, multi_query() თავისი ცალკე შედეგებით, ან უკვე დაწერილი და მუშა კოდი. ამ პოსტის დანარჩენი ნაწილი ამ დასკვნის დეტალებია: როგორ განსხვავდება ორი API ერთმანეთისგან, prepared statement-ები გვერდიგვერდ, PHP 8.1-ში შეცდომების დამუშავების ცვლილება, რომელმაც ბევრი ძველი mysqli კოდი გააფუჭა, რას ნიშნავს აქ "სიჩქარე" და როგორ გადაიტანო ძველი mysql_* კოდი ორივეზე.

მოკლე ვერსია, ცხრილის სახით#

კითხვაmysqliPDO
მონაცემთა ბაზებიმხოლოდ MySQL და MariaDBათიოდე driver, ერთი API
Placeholder-ებიმხოლოდ ?? და :named
სტილიობიექტური ან პროცედურულიმხოლოდ ობიექტური
Prepare-ებიყოველთვის server-sideMySQL-ისთვის ნაგულისხმევად ემულირებული
შეცდომები ნაგულისხმევადგამონაკლისები PHP 8.1-დანგამონაკლისები PHP 8.0-დან
ასინქრონული მოთხოვნებიდიახ, MYSQLI_ASYNCარა
Fetch კლასშიfetch_object()FETCH_CLASS, fetchObject()

ამ სტრიქონებიდან არცერთი არ არის მიზეზი, რომ მუშა კოდი თავიდან დაწერო. ისინი მიზეზებია, რათა ახალი კოდისთვის ერთ-ერთი აირჩიო, და ახალი კოდისთვის არჩევანი თითქმის ყოველთვის PDO-ა. თუ უკვე PDO-ს იყენებ და გინდა DSN-ის, პარამეტრების, ტრანზაქციებისა და შეცდომის კოდების სრული განხილვა, PHP PDO და MySQL ამას ფარავს; ეს პოსტი შედარებაზე რჩება.

რა არის ეს ორი extension#

PHP-ში MySQL-ისთვის სამი extension არსებობდა და სახელები ისტორიას ასახავს.

თავდაპირველი mysql extension - mysql_connect(), mysql_query(), mysql_fetch_assoc() - PHP 5.5-ში deprecated გახდა და PHP 7.0-ში მთლიანად ამოიღეს. მას prepared statement-ები არ ჰქონდა, გლობალურ მდგომარეობაში "ბოლო კავშირს" ინახავდა და დეველოპერების მთელი თაობა SQL-ის სტრიქონების შეერთებით აწყობისკენ უბიძგა. თუ რომელიმე კოდბაზა ჯერ კიდევ იძახებს mysql_query()-ს, ის PHP 5.6-ზე ან უფრო ძველზე მუშაობს, ან საერთოდ არ მუშაობს.

mysqli ("i" ნიშნავს improved-ს) PHP 5-ში გაჩნდა, როგორც შემცვლელი. ეს საკმაოდ თხელი შრეა MySQL-ის საკუთარ client API-ზე: ის MySQL-ის ცნებებს პირდაპირ აჩვენებს, მხარს უჭერს prepared statement-ებს, ტრანზაქციებს, მრავალ ბრძანებას და ასინქრონულ მოთხოვნებს და გთავაზობს როგორც ობიექტურ ინტერფეისს ($mysqli->query()), ასევე პროცედურულს (mysqli_query($link, ...)). პროცედურული ინტერფეისი ძირითადად იმისთვის არსებობს, რომ ძველი mysql_* კოდის გადატანა გაადვილდეს.

PDO (PHP Data Objects) დაახლოებით ამავე დროს გამოჩნდა განსხვავებული მიზნით: მონაცემთა ბაზასთან წვდომის ერთი API, რომლის ქვეშაც თითოეული ბაზის driver ერთვება. pdo_mysql MySQL-ის driver-ია. PDO არ ცდილობს ყველაფრის გამოჩენას, რასაც MySQL ახერხებს. ის აჩვენებს იმას, რაც აპლიკაციების უმეტესობას სჭირდება, ერთგვაროვნად, სხვადასხვა ბაზაზე.

ორივე extension იყენებს mysqlnd-ს, PHP-სთვის დაწერილ MySQL-ის native driver-ს, ყველა გავრცელებულ build-ში. ეს მნიშვნელოვანია სიჩქარის საკითხისთვის, რომელსაც ქვემოთ განვიხილავთ: ქსელური პროტოკოლი, buffering-ი და შედეგების დამუშავება ორივე შემთხვევაში ერთი და იგივე კოდია.

Driver-ები: ერთი ბაზა თუ ბევრი#

PDO-ს driver-ების სია ყველაზე მკაფიო ფუნქციური განსხვავებაა. გავრცელებულები:

Driverმონაცემთა ბაზა
pdo_mysqlMySQL, MariaDB
pdo_pgsqlPostgreSQL
pdo_sqliteSQLite
pdo_sqlsrvSQL Server (Microsoft-ის extension, ცალკე ინსტალდება)
pdo_odbcნებისმიერი ბაზა ODBC driver-ით

რა აქვს PHP-ის ინსტალაციას, შეგიძლია შეამოწმო php -m-ით ან php -r 'print_r(PDO::getAvailableDrivers());'-ით.

გადატანადობის არგუმენტის გულწრფელი ვერსია: PDO SQL-ს გადატანადს არ ხდის. LIMIT და TOP, AUTO_INCREMENT და identity სვეტები, backtick-ები და ორმაგი ბრჭყალები - ყველაფერი ეს შენს საქმედ რჩება. PDO გადატანადს ხდის SQL-ის გარშემო არსებულ კოდს: დაკავშირებას, binding-ს, fetch-ს, ტრანზაქციებსა და შეცდომების დამუშავებას. პრაქტიკაში ეს აპლიკაციის ბაზასთან მუშაობის კოდის უმეტესი ნაწილია და ამიტომაა, რომ ყველა მსხვილი PHP framework და ORM - Laravel-ის Eloquent, Doctrine DBAL, CakePHP-ის ORM - PDO-ზეა აგებული. ამიტომაც არის ტესტების SQLite-ზე გაშვება, სანამ production MySQL-ზე მუშაობს, PDO-თან რეალისტური ვარიანტი და mysqli-თან - შეუძლებელი.

თუ აპლიკაცია მხოლოდ MySQL-თან იმუშავებს, გადატანადობას თავისთავად დიდი ფასი არ აქვს. მაშინ PDO-ს სასარგებლო არგუმენტი ის არის, რომ API უფრო სასიამოვნოა, და ეს მართლაც ასეა, ძირითადად placeholder-ებისა და fetch mode-ების გამო.

Prepared statement-ები ორივეში#

ერთი და იგივე მოთხოვნა - აქტიური მომხმარებლები, რომლებიც თარიღის შემდეგ შეიქმნა - სამი გზით დაწერილი.

mysqli, კლასიკური ფორმა. პარამეტრები reference-ით ებმება ტიპების სტრიქონით: i მთელი რიცხვი, d double, s სტრიქონი, b blob, რომელიც პაკეტებად იგზავნება.

php
$stmt = $mysqli->prepare(    'SELECT id, email FROM users WHERE status = ? AND created_at > ?');$stmt->bind_param('ss', $status, $since);$stmt->execute();$result = $stmt->get_result();$users = $result->fetch_all(MYSQLI_ASSOC);

PHP 8.1-ში და შემდეგ mysqli-ს შეუძლია bind_param()-ის გამოტოვება და execute()-ისთვის მასივის გადაცემა; ამ დროს ყველა მნიშვნელობა სტრიქონად ებმება, რომელსაც MySQL საჭიროებისამებრ გარდაქმნის. PHP 8.2-მა დაამატა execute_query(), რომელიც ერთი გამოძახებით prepare-ს, binding-სა და execute-ს აკეთებს და შედეგს აბრუნებს:

php
// PHP 8.1+$stmt = $mysqli->prepare('SELECT id, email FROM users WHERE status = ? AND created_at > ?');$stmt->execute([$status, $since]);// PHP 8.2+$users = $mysqli->execute_query(    'SELECT id, email FROM users WHERE status = ? AND created_at > ?',    [$status, $since])->fetch_all(MYSQLI_ASSOC);

PDO, named placeholder-ებით:

php
$stmt = $pdo->prepare(    'SELECT id, email FROM users WHERE status = :status AND created_at > :since');$stmt->execute(['status' => $status, 'since' => $since]);$users = $stmt->fetchAll(PDO::FETCH_ASSOC);

განსხვავებები, რომლებიც პრაქტიკაში გაწუხებს:

  • Named placeholder-ები. PDO-ს აქვს, mysqli-ს არა. რვა პარამეტრიან მოთხოვნაში :email იკითხება, ტიპების სტრიქონი ssisdssi კი ბაგია, რომელიც ელოდება, როდის დაამატებს ვინმე სვეტს შუაში.
  • Reference-ით binding. bind_param() ცვლადებს აკავშირებს და არა მნიშვნელობებს და მათ execute()-ის დროს კითხულობს. ეს ციკლში გამოსადეგია (ერთხელ აკავშირებ, ცვლადებს ცვლი, არაერთხელ ასრულებ), სხვა ყველგან კი დამაბნეველია, რადგან პირდაპირ ლიტერალს ან ფუნქციის შედეგს ვერ გადასცემ. PDO-ს აქვს როგორც bindParam() (reference-ით), ასევე bindValue() (მნიშვნელობით), და execute($array) უმეტეს შემთხვევას ფარავს.
  • `get_result()` და `bind_result()`. get_result() ჩვეულებრივ შედეგის ობიექტს აბრუნებს, საიდანაც მასივებს იღებ. მას mysqlnd სჭირდება, ამიტომ ძველი tutorial-ები, რომლებიც ამის გარეშე build-ებისთვისაა დაწერილი, bind_result()-ს იყენებენ თითოეული გამოსავალი სვეტის ცვლადზე მისაბმელად. ნებისმიერ მიმდინარე PHP-ზე შეგიძლია get_result() გამოიყენო და bind_result() დაივიწყო.
  • Server-side თუ ემულირებული. mysqli-ს prepare-ები ყოველთვის ნამდვილი server-side prepared statement-ებია. PDO MySQL-თან მათ ნაგულისხმევად ემულირებს - მნიშვნელობებს escape-ს უკეთებს და ერთ მზა მოთხოვნას აგზავნის - სანამ PDO::ATTR_EMULATE_PREPARES-ს false-ზე არ დააყენებ. ორივე უსაფრთხოა, როცა კავშირის სიმბოლოთა ნაკრები სწორადაა დაყენებული. PDO-ს პოსტში აღწერილია, როდის აქვს ემულაციას მნიშვნელობა.

არცერთი API არ გაძლევს საშუალებას, placeholder ცხრილის სახელს, სვეტის სახელს ან ASC/DESC-ს ჩაანაცვლებდეს. ეს მნიშვნელობები შენს კოდში ფიქსირებული სიიდან უნდა მოდიოდეს, რომელ extension-საც არ უნდა იყენებდე.

შეცდომების დამუშავება: რა შეიცვალა PHP 8.0-სა და 8.1-ში#

ეს ის განსხვავებაა, რომელიც განახლებას ყველაზე ხშირად აფუჭებს, და ორივე extension-ში ერთი წლის განმავლობაში შეიცვალა.

PDO PHP 8.0-მდე PDO::ERRMODE_SILENT-ზე იყო. ჩავარდნილი მოთხოვნა false-ს აბრუნებდა, შეცდომის კოდს აყენებდა, რომელიც შენ უნდა გეკითხა, და სკრიპტი გრძელდებოდა. PHP 8.0-მა ნაგულისხმევი PDO::ERRMODE_EXCEPTION გახადა, ამიტომ ჩავარდნილი მოთხოვნა PDOException-ს აგდებს.

mysqli PHP 8.1-ში გაჰყვა. 8.1-მდე ნაგულისხმევი report mode იყო MYSQLI_REPORT_OFF: query() აბრუნებდა false-ს და $mysqli->error-ს ან $mysqli->errno-ს თავად ამოწმებდი - ან, როგორც კოდის უმეტესობა იქცეოდა, საერთოდ არ ამოწმებდი. PHP 8.1-დან ნაგულისხმევია MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT, რომელიც ნებისმიერ შეცდომაზე, წარუმატებელ დაკავშირებასაც ჩათვლით, mysqli_sql_exception-ს აგდებს.

ძველი mysqli კოდი ჩვეულებრივ ასე გამოიყურება:

php
$link = mysqli_connect($host, $user, $pass, $db);if (!$link) {    die('Connect failed: ' . mysqli_connect_error());}$result = mysqli_query($link, $sql);if ($result === false) {    error_log(mysqli_error($link));    // fall back to something}

PHP 8.1-ზე if ბლოკები არასდროს სრულდება: გამონაკლისი ადრე ისროლება, და თუ არავინ დაიჭერს, მოთხოვნა დაუჭერელი mysqli_sql_exception-ით ეღუპება. ჩართული display_errors-ის დროს - რაც production-ში არ უნდა იყოს - ამ შეტყობინებაში host-ის სახელი და მომხმარებელი შეიძლება გამოჩნდეს. კოდი, რომელიც იმაზე იყო დაფუძნებული, რომ ჩავარდნილი INSERT false-ს დააბრუნებდა და "ეს სახელი დაკავებულია" გამოჩნდებოდა, ახლა უბრალოდ ინგრევა.

წინ ორი გზა გაქვს. სწორი ისაა, რომ გამონაკლისები დატოვო და მოსალოდნელი შეცდომები დაიჭირო:

php
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT); // explicit, same as 8.1 defaulttry {    $mysqli->execute_query('INSERT INTO users (email) VALUES (?)', [$email]);} catch (mysqli_sql_exception $e) {    if ($e->getCode() === 1062) {   // duplicate entry        return 'That email is already registered.';    }    throw $e;}

დროებითი გამოსავალია ძველი ქცევის დაბრუნება mysqli_report(MYSQLI_REPORT_OFF);-ით გაშვების შემდეგ მაშინვე. დიდ ძველ კოდბაზაზე ის დროს იგებს, მაგრამ ჩუმ ჩავარდნებსაც აბრუნებს, ამიტომ ის დროებით პარამეტრად მიიჩნიე, რომელსაც task აქვს მიბმული.

ერთი პატარა განსხვავება იმაში, რის მიხედვითაც პირობას წერ: mysqli_sql_exception::getCode() MySQL-ის შეცდომის ნომერს აბრუნებს (1062, 1045, 2002), ხოლო getSqlState() - SQLSTATE-ს. PDOException::getCode() SQLSTATE სტრიქონს აბრუნებს (23000), MySQL-ის ნომერი კი $e->errorInfo[1]-შია. ორივეში MySQL-ის ნომრის მიხედვით შეამოწმე; ერთი SQLSTATE ბევრ დაუკავშირებელ შეცდომას ფარავს. MySQL-ის მომხმარებლები და უფლებები განმარტავს გავრცელებულ 1045 Access denied შემთხვევებს.

ტიპები, fetch და ფუნქციები, რომლებიც მხოლოდ ერთს აქვს#

სტრიქონებად დაბრუნებული მთელი რიცხვები

mysqli-ში უბრალო query() MySQL-ის ტექსტურ პროტოკოლს იყენებს და ყველა სვეტს სტრიქონად აბრუნებს, INT სვეტებსაც კი. Prepared statement-ები ბინარულ პროტოკოლს იყენებენ და native მთელ და წილად რიცხვებს აბრუნებენ. უბრალო მოთხოვნებსაც შეგიძლია native ტიპები დააბრუნებინო:

php
$mysqli = mysqli_init();$mysqli->options(MYSQLI_OPT_INT_AND_FLOAT_NATIVE, true);$mysqli->real_connect($host, $user, $pass, $db, $port);

PDO native prepare-ებით მთელ და წილად რიცხვებს აბრუნებს; ემულირებულ prepare-ებთან PHP 8.1-მდე სტრიქონებს აბრუნებდა, რომელმაც ეს native ტიპებზე შეცვალა. ორივე extension-ში DECIMAL განზრახ სტრიქონად მოდის, რათა ფულს მცურავწერტილიანი დამრგვალება არ შეეყაროს. როცა განახლების შემდეგ მკაცრი შედარება, როგორიცაა $row['id'] === '5', უცებ ირღვევა, ჩვეულებრივ ეს არის მიზეზი.

Fetch

PDO-ს fetch mode-ები ციკლებს გიზოგავს: FETCH_COLUMN ბრტყელი სიისთვის, FETCH_KEY_PAIR id-იდან სახელამდე რუკისთვის, FETCH_GROUP პირველი სვეტით დაჯგუფებული მწკრივებისთვის, FETCH_CLASS ობიექტების შესავსებად. mysqli-ს აქვს fetch_assoc(), fetch_row(), fetch_object(), fetch_all() და PHP 8.1-დან fetch_column(). ყველაფერი ამის მიღმა ციკლია, რომელსაც თავად წერ.

mysqli_result იტერირებადია, ამიტომ foreach ($result as $row) ასოციაციურ მასივებს გაძლევს while ციკლში fetch_assoc()-ის გამოძახების გარეშე. PDO statement ანალოგიურად იტერირდება, იმ fetch mode-ით, რაც დაყენებულია.

რას აკეთებს მხოლოდ mysqli

  • ასინქრონული მოთხოვნები. $mysqli->query($sql, MYSQLI_ASYNC) მაშინვე ბრუნდება; mysqli_poll() რამდენიმე კავშირს ელოდება და reap_async_query() შედეგს იღებს. ეს ერთ PHP პროცესს რამდენიმე ნელი მოთხოვნის პარალელურად გაშვების საშუალებას აძლევს ცალკე კავშირებზე. იშვიათად გამოიყენება და როცა გამოიყენება, ჩვეულებრივ ანგარიშებისთვისაა, რომლებიც რამდენიმე ბაზაზე ნაწილდება.
  • `multi_query()`. ერთი გამოძახებით რამდენიმე წერტილ-მძიმით გამოყოფილ ბრძანებას ასრულებს და შედეგებს next_result()-ით გადაუყვება. მიგრაციის ფაილის გასაშვებად გამოსადეგია. ამასვე ელოდება SQL injection, ამიტომ მას მომხმარებლის შეყვანა არასდროს უნდა შეხვდეს. PDO-ს ემულირებული prepare-ებით ერთ exec()-ში რამდენიმე ბრძანების გაშვება შეუძლია, მაგრამ მათი შედეგების დამუშავება უხერხულია.
  • MySQL-სპეციფიკური გამოძახებები, როგორიცაა $mysqli->info (UPDATE-ის შემდეგ დამთხვეული და შეცვლილი მწკრივები), stat(), thread_id და kill().

რას აკეთებს მხოლოდ PDO

Named placeholder-ები, ზემოთ აღწერილი fetch mode-ები, FETCH_CLASS კონსტრუქტორის არგუმენტებით და ერთი და იგივე კოდი რამდენიმე ბაზაზე. PHP 8.4-დან ასევე არის driver-ის ქვეკლასები: PDO::connect() MySQL DSN-ისთვის Pdo\Mysql ობიექტს აბრუნებს, რომელზეც MySQL-სპეციფიკური კონსტანტებია.

სიჩქარე: რა განსხვავდება სინამდვილეში#

mysqli-სა და PDO-ს benchmark-ები ჩვეულებრივ აჩვენებს, რომ mysqli რამდენიმე პროცენტით უფრო სწრაფია უმნიშვნელო მოთხოვნების მჭიდრო ციკლებში. ეს რიცხვი რეალურია და უმნიშვნელო. ორივე extension სამუშაოს mysqlnd-ს გადასცემს; განსხვავება თითო გამოძახებაზე PHP-ის მხარეს არსებული თხელი შრეა, რომელიც მიკროწამებში იზომება. ერთი მოთხოვნა, რომელიც buffer pool-ის ნაცვლად დისკიდან კითხულობს, უფრო ძვირია, ვიდრე ეს სხვაობა ათასობით გამოძახებაზე.

რაც ბაზასთან მუშაობის კოდის სიჩქარეს ნამდვილად ცვლის:

  • Round trip-ები. Server-side prepared statement არის prepare მოთხოვნა და შემდეგ execute მოთხოვნა: ორი ვიზიტი სერვერთან, სანამ შედეგები დაბრუნდება. mysqli ამას ყოველთვის აკეთებს prepare()-სა და execute_query()-ზე. PDO ემულირებული prepare-ებით ერთ მოთხოვნას აგზავნის. ლოკალურ socket-ზე ამას ვერავინ ამჩნევს; რამდენიმე მილიწამიან ქსელურ გზაზე კი გვერდი, რომელიც 200 ერთჯერად prepared მოთხოვნას ასრულებს, რეალურ დროს ხარჯავს ლოდინში. ან გვერდი გაასწორე, რომ 200 მოთხოვნა არ გაუშვას, ან ერთხელ გააკეთე prepare და ციკლში შეასრულე - ორივე extension ამას კარგად ართმევს თავს.
  • Buffered და unbuffered შედეგები. ორივე ნაგულისხმევად მთელ შედეგს PHP-ის მეხსიერებაში ინახავს. დიდი ექსპორტისთვის mysqli-ს MYSQLI_USE_RESULT (ან real_query() პლუს use_result()) და PDO-ს MYSQL_ATTR_USE_BUFFERED_QUERY => false მწკრივებს ნაკადით გადმოსცემს. ეს მეხსიერების გამოყენებას რიგებით ცვლის; extension-ის არჩევანი - არა.
  • კავშირები. კავშირის გახსნა თითოეულ დამხმარე ფუნქციაში მოთხოვნაზე ერთხელ გახსნის ნაცვლად handshake-ებს ამრავლებს. ეს ორივეში ერთნაირად ჯდება. MySQL-ის კავშირების ლიმიტები და pooling განმარტავს, რატომ უნდა ეთანხმებოდეს PHP-FPM worker-ები და max_connections ერთმანეთს.
  • ინდექსები. თითქმის ყველა "PHP ნელია MySQL-თან" საჩივარი ინდექსის არქონაა. MySQL-ის ინდექსები და EXPLAIN საწყისი წერტილია, ხოლო slow query log გეუბნება, რომელი მოთხოვნით დაიწყო.

თუ ორ extension-ს შორის სიჩქარის გამო ირჩევ, არასწორ შრეს ოპტიმიზებ.

ძველი mysql_* კოდის მიგრაცია#

mysql_* ფუნქციებზე აგებული კოდი უნდა გადავიდეს, სანამ რომელიმე მხარდაჭერილ PHP-ზე იმუშავებს. მექანიკური გზა პროცედურული mysqli API-ა, რადგან ფუნქციების სახელები ემთხვევა; უკეთესი გზა, თუ ისედაც ყველა მოთხოვნას ეხები, PDO-ა prepared statement-ებით. ნებისმიერ შემთხვევაში სამუშაო ერთი და იგივეა, სამი ნაბიჯი: გამოძახებების ჩანაცვლება, კავშირის აშკარად გადაცემა და სტრიქონებით აწყობილი SQL-ის placeholder-ებით ჩანაცვლება.

ხშირი გამოძახებების შესაბამისობა:

mysql_*mysqli (პროცედურული)PDO
mysql_connect() + mysql_select_db()mysqli_connect($h, $u, $p, $db)new PDO($dsn, $u, $p)
mysql_query($sql)mysqli_query($link, $sql)$pdo->query($sql)
mysql_fetch_assoc($r)mysqli_fetch_assoc($r)$stmt->fetch()
mysql_num_rows($r)mysqli_num_rows($r)count($stmt->fetchAll())
mysql_insert_id()mysqli_insert_id($link)$pdo->lastInsertId()
mysql_real_escape_string($s)prepared statementprepared statement
mysql_error()გამონაკლისი (8.1+)გამონაკლისი

საფრთხეები პირდაპირ find-and-replace-ში:

  1. არგუმენტების თანმიმდევრობა. mysql_query($sql, $link) კავშირს მეორე და არასავალდებულო არგუმენტად იღებდა. mysqli_query($link, $sql) მას პირველად და ყოველთვის იღებს. Regex, რომელიც ფუნქციას არგუმენტების გადანაცვლების გარეშე ანაცვლებს, კოდს აწარმოებს, რომელიც ყოველ გამოძახებაზე ვარდება.
  2. იმპლიციტური კავშირი. mysql_* ფუნქციები ბოლოს გახსნილ კავშირს იყენებდნენ, თუ არაფერს გადასცემდი, ამიტომ ძველი კოდი mysql_query()-ს იძახებს ფუნქციების სიღრმეში, რომლებსაც კავშირის ცვლადი არასდროს უნახავთ. mysqli-სა და PDO-ს ის გადაცემული უნდა ჰქონდეთ. სწრაფი გამოსავალია ფუნქცია, რომელიც საერთო კავშირს აბრუნებს (db()->query(...)); გლობალური ცვლადიც მუშაობს და უარესია.
  3. ფუნქციები ექვივალენტის გარეშე. mysql_result()-ს (ერთი უჯრის აღება მწკრივითა და ველით) mysqli-ში ტყუპისცალი არ ჰყავს; ჩაანაცვლე fetch-ით და მასივის ინდექსით, ან PDO-ს fetchColumn()-ით. mysql_list_dbs() და მისი მსგავსები SHOW DATABASES მოთხოვნებად იქცევა.
  4. Escape სიმბოლოთა ნაკრების გარეშე. თუ ძველი კოდი mysql_real_escape_string()-ს იძახებს და მას mysqli_real_escape_string()-ით ცვლი, ჯერ დააყენე სიმბოლოთა ნაკრები mysqli_set_charset($link, 'utf8mb4')-ით და არასდროს SET NAMES მოთხოვნით - escape მხოლოდ იმ სიმბოლოთა ნაკრებს იცნობს, რომელიც API-ით გადაეცა. უკეთესი ისაა, რომ escape საერთოდ წაშალო და placeholder-ები გამოიყენო.
  5. შეცდომების დამუშავება უკუღმა ტრიალდება. ძველი or die(mysql_error()) შაბლონები PHP 8.1-ის გამონაკლისებთან მკვდარ კოდად იქცევა. შეცდომების ერთი სტრატეგია პორტირების დასაწყისში გადაწყვიტე და არა მოთხოვნა-მოთხოვნა აღმოაჩინო.

არსებობს shim ბიბლიოთეკები, რომლებიც mysql_* ფუნქციებს mysqli-ზე ახლიდან ახორციელებენ, რათა ძველი კოდი უცვლელად იმუშაოს PHP 7-ზე და 8-ზე. ისინი გზაა, რომ მიტოვებული აპლიკაცია მხარდაჭერილ PHP ვერსიაზე გადაიყვანო, სანამ რეალურ მიგრაციას გეგმავ, და არა დანიშნულების ადგილი: კოდი მაინც სტრიქონებისგან აწყობს SQL-ს.

ძველ კოდს ჩვეულებრივ ძველი ბაზა ახლავს. MySQL 8.4 ნაგულისხმევად აღარ რთავს mysql_native_password plugin-ს და ძალიან ძველ PHP build-ებს არ შეუძლიათ მისი შემცვლელით, caching_sha2_password-ით, ავტორიზაცია, ამიტომ ახალ ბაზაზე გადატანილ ძველ აპლიკაციას დაკავშირებისას ავტორიზაციის შეცდომა შეიძლება მოუვიდეს. MySQL 8.4 LTS: რა შეიცვალა გამოსწორებას აღწერს, ხოლო MySQL-ის ახალ host-ზე გადატანა მონაცემების გადატანას ფარავს.

ჰოსტინგის ბაზასთან დაკავშირება ორივედან#

დაკავშირების წესები ორივე extension-ში ერთნაირია, რადგან ქვემოთ client ბიბლიოთეკა ერთია.

php
// mysqli$mysqli = new mysqli($host, $user, $pass, $db, (int) $port);$mysqli->set_charset('utf8mb4');// PDO$pdo = new PDO(    "mysql:host=$host;port=$port;dbname=$db;charset=utf8mb4",    $user,    $pass,    [PDO::ATTR_EMULATE_PREPARES => false]);

სამი რამ ორივეში მართებულია. utf8mb4 API-ით დააყენე - set_charset()-ით ან DSN-ით - რომ escape-იც და emoji-ც იმუშაოს (MySQL utf8mb4 და collation-ები ცხრილებს ფარავს). სხვა მანქანაზე არსებული ბაზისთვის localhost არ დაწერო, რადგან client localhost-ს ლოკალური Unix socket-ის მოთხოვნად აღიქვამს და port-ს უგულებელყოფს. და credentials-ები გარემოს ცვლადებში შეინახე და არა web root-ში მდებარე PHP ფაილში; გარემოს ცვლადები და საიდუმლოებები განმარტავს, სად ეკუთვნის მათ ადგილი.

პერსისტენტული კავშირები ორივეში არსებობს - p: პრეფიქსი mysqli-ს host-ზე, PDO::ATTR_PERSISTENT PDO-ში - და იგივე სიფრთხილე მოქმედებს: ისინი სესიის მდგომარეობას ერთი მოთხოვნიდან იმავე worker-ზე შემდეგ მოთხოვნაზე გადააქვთ და იშვიათად ზოგავენ იმდენს, რომ ღირდეს.

RE:NODE-ზე MySQL 8.4 LTS ბაზა იქმნება root პაროლით, უკვე გენერირებული აპლიკაციის ბაზითა და აპლიკაციის მომხმარებლით და ის მიიღწევა გეგმის host-ზე და port-ზე - ამიტომ ზემოთ მოცემული ორივე კავშირი სწორედ ამ ოთხი მნიშვნელობის ჩასმაა. Laravel / PHP აპლიკაციის ხაზი PHP 8.3-ს, 8.4-ს ან 8.5-ს უშვებს, რაც ნიშნავს, რომ ამ პოსტში აღწერილი ყველა ნაგულისხმევი ქცევა (გამონაკლისები ორივე extension-ში, execute_query(), native ტიპები ემულირებული PDO prepare-ებიდან) ის ქცევაა, რასაც იქ ნახავ.

FAQ#

mysqli deprecated-ია?

არა. mysqli მხარდაჭერილია და ახალ შესაძლებლობებს იღებს - execute_query() PHP 8.2-ში გაჩნდა. რაც deprecated გახდა და ამოიღეს, არის ძველი mysql extension "i"-ს გარეშე. ახალი კოდისთვის PDO-ს არჩევა უპირატესობაა და არა მიგრაცია, რომელსაც გაიძულებენ.

mysqli ნაკლებად უსაფრთხოა, ვიდრე PDO?

არა. ორივე თანაბრად უსაფრთხოა, როცა ყველა გარე მნიშვნელობა placeholder-ში გადის, და თანაბრად სახიფათოა, როცა SQL შეერთებით აიწყობა. mysqli-ს prepare-ები ყოველთვის server-side-ია, ამიტომ დასაკონფიგურირებელი ემულაციის შრე არ არსებობს. რომელიმეს უსაფრთხოება მის გარშემო არსებულ ჩვევებზეა დამოკიდებული და არა extension-ზე.

შეიძლება mysqli-ისა და PDO-ს ერთ აპლიკაციაში გამოყენება?

დიახ. ისინი ცალკე კავშირებია, ამიტომ ტრანზაქციებს, lastInsertId()-ს და სესიის ცვლადებს არ ინაწილებენ, მაგრამ ორივეს შეუძლია ერთი და იმავე ბაზის წაკითხვა და ჩაწერა. ერთიდან მეორეზე თანდათანობითი გადასვლა ჩვეულებრივ ასე კეთდება.

PDO MariaDB-სთან მუშაობს?

დიახ. pdo_mysql და mysqli ორივე MySQL-ის პროტოკოლზე საუბრობს, რომელსაც MariaDB ახორციელებს. განსხვავებები, რომლებსაც შეხვდები, SQL-ის ფუნქციებსა და სერვერის ნაგულისხმევ პარამეტრებშია და არა PHP extension-ში.

რატომ აგდებს ჩემი mysqli კოდი გამონაკლისებს PHP-ის განახლების შემდეგ?

PHP 8.1-მა mysqli-ს ნაგულისხმევი report mode გამორთულიდან MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT-ზე შეცვალა. მოთხოვნები, რომლებიც ადრე ჩუმად false-ს აბრუნებდა, ახლა mysqli_sql_exception-ს აგდებს. დაიჭირე მოსალოდნელი შეცდომები, ან დროებით გამოსავლად გამოიძახე mysqli_report(MYSQLI_REPORT_OFF).

რომელს იყენებენ framework-ები?

PDO-ს. Laravel, Symfony Doctrine DBAL-ის გავლით, CakePHP და სხვა framework-ების უმეტესობა ბაზის შრეს მასზე აგებს, რაც კიდევ ერთი მიზეზია, რატომ უნდა დაიწყოს მისით ახალი კოდი, რომელიც framework-ში შეიძლება აღმოჩნდეს.