PHP Profi

Типы данных в PHP и MySQL Перевод

MySQL   PHP

С тех пор как был выпущен PHP 7.0, большое внимание уделяется скалярным типам. Сохранять типы данных внутри вашего приложения теперь довольно просто. Но когда речь идёт о внешних системах, таких как база данных, то всё не так всё гладко, как казалось изначально.

В случае с MySQL типы, с которыми мы работаем, в первом приближении, определены сетевым протоколом. Сетевой протокол MySQL по умолчанию преобразует все данные в строки. Так что если мы возьмём целое число из базы данных и будем использовать типизацию в PHP 7, мы получим ошибку:

declare(strict_types=1);

function getInteger() : int {
  $mysqli = new mysqli(...);
  return $mysqli->query("SELECT 1")->fetch_row()[0];
}

var_dump(getInteger());


Fatal error: Uncaught TypeError: Return value of getInteger() must be of the type integer, string returned in t.php:6

Конечно, есть простое решение: либо преобразовать самим, либо отключить строгий режим и PHP преобразует за нас.

Теперь давайте взглянем на другой случай. Предположим у нас есть приложение, где мы получаем целочисленный ID из базы данных. Мы знаем, что MySQL отправит нам строку, и мы относимся к ID как к непрозрачным данным в любом случае, таким образом у нас есть проверка типа на строку. Сейчас мы немного отрефакторим код и будем использовать подготовленные запросы (prepared statements). Какой будет результат?

declare(strict_types=1);

function getId() : string {
  $mysqli = new mysqli(...);
  $stmt = $mysqli->prepare("SELECT 1");
  $stmt->execute();
  return $stmt->get_result()->fetch_row()[0];
}

var_dump(getId());


Fatal error: Uncaught TypeError: Return value of getId() must be of the type string, integer returned in t.php:8

Постойте! Что там случилось?! Разве я только что не говорил, что протокол MySQL всегда будет отправлять строку, таким образом, мы получаем строку в PHP?! Да говорил, и это верно для "прямых запросов". Но это не так для подготовленных запросов. С готовыми запросами протокол MySQL использует двоичное кодирование данных и поэтому mysqlnd и mysqli будут пытаться найти соответствующий тип PHP. Это не всегда возможно, особенно если мы наталкиваемся на диапазон больших значений. Итак, давайте сделаем запрос для PHP_INT_MAX и PHP_INT_MAX+1 и посмотрим на типы:

$mysqli = new mysqli(...);
$stmt = $mysqli->prepare("SELECT 9223372036854775807, 9223372036854775808");
$stmt->execute();
var_dump($stmt->get_result()->fetch_row());


array(2) {
  [0]=>
  int(9223372036854775807)
  [1]=>
  string(19) "9223372036854775808"
}

Здесь 9223372036854775807 - наибольшее значение целого числа, которое можно представить в PHP, и, таким образом, оно целое число. Однако 9223372036854775808 является слишком большим и не помещается в 64-битное целое, таким образом, оно преобразуется в строку, как она сохранит всю информацию и с ней можно работать, по крайней мере, до некоторой степени.

Подобные вещи происходят и в других типах, которые не могут быть правильно представлены в PHP:

$mysqli = new mysqli(...);
$stmt = $mysqli->prepare("SELECT 1.23");
$stmt->execute();
var_dump($stmt->get_result()->fetch_row());


array(2) {
  [0]=>
  string(4) "1.23"
}

Yay - yet another wtf! Что произошло на этот раз? Ну, такая запись в SQL интерпретируется как DECIMAL (десятичное). Десятичное поле предполагается быть точным. Если бы оно было преобразовано в РНР-шный float (то же самое, что и double в PHP) мы, вероятно, потеряли бы точность. Таким образом, не преобразовывая, а рассматривая его как строку, опять-таки даёт нам уверенность в том, что не мы потеряем информацию. Если бы у нас было поле типа float или double, то это могло благополучно быть преобразовано во float в PHP:

$mysqli = new mysqli(...);
$stmt = $mysqli->prepare("SELECT RAND()");
$stmt->execute();
var_dump($stmt->get_result()->fetch_row());


array(2) {
  [0]=>
  float(0.16519711461402206)
}

Итак, подведём итог:

  • Для прямых запросов: MySQL-сервер отправляет строки, PHP возвращает все данные в виде строки
  • Для подготовленных запросов: MySQL отправляет данные в двоичной форме и PHP будет использовать соответствующий тип
  • Если значение может быть представлено только с возможной потерей данных в PHP, оно преобразуется в строку, даже для подготовленных запросов

Теперь мы можем ожидать того же при использовании PDO. Давайте проверим:

$pdo = new PDO("mysql:host=localhost", "...", "...");
$stmt = $pdo->prepare("SELECT 9223372036854775808, RAND()");
$stmt->execute();
var_dump($stmt->fetch(PDO::FETCH_NUM));


array(2) {
  [0]=>
  string(1) "1"
  [1]=>
  string(18) "0.3217373297752229"
}

Этот пример использует подготовленные запросы, но возвращает строки?! Причина в том, что PDO по умолчанию не использует подготовленные запросы на сетевом уровне, а эмулирует на уровне PHP. Это означает, что PHP заменяет плейсхолдеры в шаблоне запроса, а затем выполняется прямой запрос. Как упоминалось выше, для прямых запросов MySQL-сервер отправляет строки, так что PHP воспринимает все данные как строки. Однако мы можем легко попросить PDO отключить эмуляцию:

$pdo = new PDO("mysql:host=localhost", "...", "...");
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$stmt = $pdo->prepare("SELECT 1, RAND()");
$stmt->execute();
var_dump($stmt->fetch(PDO::FETCH_NUM));


array(2) {
  [0]=>
  int(1)
  [1]=>
  float(0.24252333421495)
}

Но остаётся вопрос, нужно ли отключать эмуляцию для того, чтобы получить правильные типы? Т.к. это влияет на производительность. С родной подготовкой запросов клиент-серверный обмен данными будет происходить во время подготовки и еще один обмен — при выполнении запроса. С эмуляцией — только во время выполнения. Родная подготовка запросов также требует некоторые ресурсы сервера для сохранения данных. Однако если один запрос выполняется несколько раз, возможна некоторая экономия. Также представление типов означает, что происходят различные преобразования типов и различное количество и точность данных передается. В большинстве случаев это не должно иметь существенного влияния, но в итоге покажет только тест.

Надеюсь, что это поможет дать лучшее понимание, а может ещё больше запутает.

2017-03-28 оригинал

Последние посты

Комментарии

авторизуйтесь или зарегистрируйтесь, чтобы оставить комментарий