PDO MySQL: использовать PDO :: ATTR_EMULATE_PREPARES или нет?

117

Вот о чем я читал до сих пор PDO::ATTR_EMULATE_PREPARES:

  1. Эмуляция подготовки PDO лучше с точки зрения производительности, поскольку собственная подготовка MySQL обходит кеш запросов .
  2. Собственная подготовка MySQL лучше для безопасности (предотвращение SQL-инъекций) .
  3. Собственная подготовка MySQL лучше подходит для отчетов об ошибках .

Я больше не знаю, насколько правдивы эти утверждения. При выборе интерфейса MySQL меня больше всего беспокоит предотвращение внедрения SQL-кода. Вторая проблема - производительность.

В настоящее время мое приложение использует процедурный MySQLi (без подготовленных операторов) и довольно часто использует кеш запросов. Он редко повторно использует подготовленные операторы в одном запросе. Я начал переход к PDO для названных параметров и безопасности подготовленных операторов.

Я использую MySQL 5.1.61иPHP 5.3.2

Оставить PDO::ATTR_EMULATE_PREPARESвключенным или нет? Есть ли способ обеспечить как производительность кеша запросов, так и безопасность подготовленных операторов?

Эндрю Энсли
источник
3
Честно? Просто продолжайте использовать MySQLi. Если он уже работает с использованием подготовленных операторов под этим, PDO в основном представляет собой бессмысленный уровень абстракции. РЕДАКТИРОВАТЬ : PDO действительно полезен для приложений с нуля, где вы не уверены, какая база данных входит в серверную часть.
jmkeyes
1
Извините, мой вопрос раньше был непонятен. Я его отредактировал. В настоящий момент приложение не использует подготовленные операторы MySQLi; просто mysqli_run_query (). Из того, что я читал, подготовленные операторы MySQLi также обходят кеш запросов.
Эндрю Энсли

Ответы:

108

Чтобы ответить на ваши вопросы:

  1. MySQL> = 5.1.17 (или> = 5.1.21 для PREPAREи EXECUTEзаявления) могут использовать подготовленные заявления в кэше запросов . Таким образом, ваша версия MySQL + PHP может использовать подготовленные операторы с кешем запросов. Однако обратите внимание на предостережения относительно кеширования результатов запросов в документации MySQL. Есть много видов запросов, которые нельзя кэшировать или которые бесполезны, даже если они кэшированы. По моему опыту, кеш запросов в любом случае не очень важен. Запросы и схемы требуют специальной конструкции, чтобы максимально использовать кеш. Часто кеширование на уровне приложения в конечном итоге все равно необходимо.

  2. Собственная подготовка не имеет никакого значения для безопасности. Псевдо-подготовленные операторы по-прежнему будут экранировать значения параметров запроса, это будет просто выполняться в библиотеке PDO со строками, а не на сервере MySQL с использованием двоичного протокола. Другими словами, один и тот же код PDO будет одинаково уязвим (или не уязвим) для атак путем внедрения, независимо от ваших EMULATE_PREPARESнастроек. Единственная разница в том, где происходит замена параметра - с EMULATE_PREPARES, это происходит в библиотеке PDO; без EMULATE_PREPARES, это происходит на сервере MySQL.

  3. Без этого EMULATE_PREPARESвы можете получить синтаксические ошибки во время подготовки, а не во время выполнения; with EMULATE_PREPARESвы получите синтаксические ошибки только во время выполнения, потому что PDO не имеет запроса для передачи MySQL до времени выполнения. Обратите внимание, что это влияет на код, который вы напишете ! Особенно если пользуетесь PDO::ERRMODE_EXCEPTION!

Дополнительное соображение:

  • Существует фиксированная стоимость для prepare()(с использованием собственных подготовленных операторов), поэтому prepare();execute()запрос с собственными подготовленными операторами может быть немного медленнее, чем выдача простого текстового запроса с использованием эмулированных подготовленных операторов. Во многих системах баз данных план запроса для a также prepare()кэшируется и может использоваться несколькими соединениями, но я не думаю, что MySQL делает это. Поэтому, если вы не используете повторно подготовленный объект оператора для нескольких запросов, ваше общее выполнение может быть медленнее.

В качестве последней рекомендации я думаю, что для более старых версий MySQL + PHP вы должны эмулировать подготовленные операторы, но в ваших самых последних версиях вы должны отключить эмуляцию.

После написания нескольких приложений, использующих PDO, я создал функцию подключения PDO, которая, по моему мнению, имеет лучшие настройки. Вероятно, вам следует использовать что-то подобное или настроить свои предпочтительные настройки:

/**
 * Return PDO handle for a MySQL connection using supplied settings
 *
 * Tries to do the right thing with different php and mysql versions.
 *
 * @param array $settings with keys: host, port, unix_socket, dbname, charset, user, pass. Some may be omitted or NULL.
 * @return PDO
 * @author Francis Avila
 */
function connect_PDO($settings)
{
    $emulate_prepares_below_version = '5.1.17';

    $dsndefaults = array_fill_keys(array('host', 'port', 'unix_socket', 'dbname', 'charset'), null);
    $dsnarr = array_intersect_key($settings, $dsndefaults);
    $dsnarr += $dsndefaults;

    // connection options I like
    $options = array(
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
    );

    // connection charset handling for old php versions
    if ($dsnarr['charset'] and version_compare(PHP_VERSION, '5.3.6', '<')) {
        $options[PDO::MYSQL_ATTR_INIT_COMMAND] = 'SET NAMES '.$dsnarr['charset'];
    }
    $dsnpairs = array();
    foreach ($dsnarr as $k => $v) {
        if ($v===null) continue;
        $dsnpairs[] = "{$k}={$v}";
    }

    $dsn = 'mysql:'.implode(';', $dsnpairs);
    $dbh = new PDO($dsn, $settings['user'], $settings['pass'], $options);

    // Set prepared statement emulation depending on server version
    $serverversion = $dbh->getAttribute(PDO::ATTR_SERVER_VERSION);
    $emulate_prepares = (version_compare($serverversion, $emulate_prepares_below_version, '<'));
    $dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, $emulate_prepares);

    return $dbh;
}
Фрэнсис Авила
источник
26
Re # 2: конечно , значения , которые MySQL принимает в качестве параметров (в нативных подготовленных заявлений) не получить разобран для SQL вообще ? Таким образом, риск внедрения должен быть ниже, чем при использовании эмуляции подготовки PDO, где любой недостаток в экранировании (например, исторические проблемы mysql_real_escape_stringс многобайтовыми символами) все равно оставит один уязвимым для атак инъекций?
eggyal
2
@eggyal, вы делаете предположения о том, как реализованы подготовленные операторы. PDO может иметь ошибку в его эмулируемых подготовительных действиях, но MySQL тоже может иметь ошибки. AFAIK, не было обнаружено никаких проблем с эмулированными подготовками, которые могли бы заставить литералы параметров проходить без экранирования.
Фрэнсис Авила
2
Отличный ответ, но у меня вопрос: если отключить ЭМУЛЯЦИЮ, не будет ли выполнение медленнее? PHP должен будет отправить подготовленный оператор в MySQL для проверки и только после этого отправить параметры. Итак, если вы используете подготовленный оператор 5 раз, PHP будет разговаривать с MySQL 6 раз (вместо 5). Разве это не замедлит работу? Кроме того, я думаю, что больше шансов, что в PDO могут быть ошибки в процессе проверки, чем в MySQL ...
Раду Мурзеа
6
Обратите внимание на моменты, приведенные в этом ответе, для повторной эмуляции подготовленного оператора с использованием mysql_real_escape_stringпод капотом и последующих уязвимостей, которые могут возникнуть (в очень конкретных крайних случаях).
eggyal
6
+1 Хороший ответ! Но для записи, если вы используете встроенную подготовку, параметры никогда не экранируются и не объединяются в запрос SQL даже на стороне сервера MySQL. К тому времени, когда вы выполните и предоставите параметры, запрос будет проанализирован и преобразован во внутренние структуры данных в MySQL. Прочтите этот блог инженера-оптимизатора MySQL, который объясняет этот процесс: guilhembichot.blogspot.com/2014/05/ ... Я не говорю, что это означает, что нативная подготовка лучше, поскольку мы доверяем коду PDO правильное экранирование (что я делать).
Билл Карвин
9

Остерегайтесь отключения PDO::ATTR_EMULATE_PREPARES(включение нативной подготовки), когда ваш PHP pdo_mysqlне компилируется mysqlnd.

Поскольку old libmysqlне полностью совместим с некоторыми функциями, это может привести к странным ошибкам, например:

  1. Потеря PDO::PARAM_INTстарших битов для 64-битных целых чисел при привязке как (0x12345678AB будет обрезано до 0x345678AB на 64-битной машине)
  2. Невозможность делать простые запросы вроде LOCK TABLES(выдает SQLSTATE[HY000]: General error: 2030 This command is not supported in the prepared statement protocol yetисключение)
  3. Необходимо получить все строки из результата или закрыть курсор перед следующим запросом (с помощью mysqlndили эмулированной подготовкой он автоматически выполняет эту работу за вас и не выходит из синхронизации с сервером mysql)

Эти ошибки я обнаружил в своем простом проекте при миграции на другой сервер, который использовался libmysqlдля pdo_mysqlмодуля. Может багов гораздо больше, не знаю. Также я тестировал свежий 64-битный debian jessie, все перечисленные ошибки возникают, когда я apt-get install php5-mysql, и исчезают, когда я apt-get install php5-mysqlnd.

Когда PDO::ATTR_EMULATE_PREPARESустановлено значение true (по умолчанию) - этих ошибок не происходит, потому что PDO вообще не использует подготовленные операторы в этом режиме. Итак, если вы используете pdo_mysqlна основе libmysql(подстрока «mysqlnd» не отображается в поле «Версия клиентского API» pdo_mysqlраздела в phpinfo) - отключать не следует PDO::ATTR_EMULATE_PREPARES.

Указатель мудреца
источник
3
актуальна ли эта озабоченность в 2019 году ?!
oldboy
8

Я бы отключил эмуляцию подготовки, поскольку вы используете 5.1, что означает, что PDO будет использовать встроенные функции подготовленных операторов.

PDO_MYSQL будет использовать встроенную поддержку подготовленных операторов, присутствующую в MySQL 4.1 и выше. Если вы используете старую версию клиентских библиотек mysql, PDO будет имитировать их за вас.

http://php.net/manual/en/ref.pdo-mysql.php

Я отказался от MySQLi для PDO ради подготовленных именованных операторов и лучшего API.

Однако, чтобы сбалансировать, PDO работает незначительно медленнее, чем MySQLi, но об этом следует помнить. Я знал это, когда делал выбор и решил, что лучший API и использование отраслевого стандарта важнее, чем использование пренебрежимо быстрой библиотеки, которая связывает вас с конкретным движком. FWIW Я думаю, что команда PHP также благосклонно смотрит на PDO, а не на MySQLi в будущем.

Уилл Морган
источник
Спасибо за информацию! Как невозможность использовать кеш запросов повлияла на вашу производительность или вы даже использовали его раньше?
Эндрю Энсли
Я все равно не могу сказать, что в качестве основы я использую кеши на нескольких уровнях. Однако вы всегда можете явно использовать SELECT SQL_CACHE <остальная часть оператора>.
Уилл Морган
Даже не знал, что есть опция SELECT SQL_CACHE. Однако, похоже, это все равно не сработает. Из документации: «Результат запроса кэшируется, если он кэшируется ...» dev.mysql.com/doc/refman/5.1/en/query-cache-in-select.html
Эндрю Энсли
Да. Это зависит от характера запроса, а не от особенностей платформы.
Уилл Морган
Я прочитал это как означающее «Результат запроса кэшируется, если что-то еще не препятствует его кешированию », что, как я читал до этого, включало подготовленные операторы. Однако, благодаря ответу Фрэнсиса Авилы, я знаю, что это уже не так для моей версии MySQL.
Эндрю Энсли
6

Я бы рекомендовал включить реальные PREPAREвызовы базы данных, так как эмуляция не улавливает все ... например, она подготовит INSERT;!

var_dump($dbh->prepare('INSERT;'));
$dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
var_dump($dbh->prepare('INSERT;'));

Выход

object(PDOStatement)#2 (1) {
  ["queryString"]=>
  string(7) "INSERT;"
}
bool(false)

Я с радостью получу удар по производительности для кода, который действительно работает.

FWIW

Версия PHP: PHP 5.4.9-4ubuntu2.4 (cli)

Версия MySQL: 5.5.34-0ubuntu0

quickshiftin
источник
Это интересный момент. Я предполагаю, что эмуляция откладывает синтаксический анализ на стороне сервера до фазы выполнения. Хотя в этом нет ничего страшного (неправильный SQL в конечном итоге выйдет из строя), лучше позволить prepareвыполнить работу, которую он должен. (Кроме того, я всегда предполагал, что синтаксический анализатор параметров на стороне клиента обязательно будет иметь собственные ошибки.)
Альваро Гонсалес
1
IDK, если вам интересно, но вот небольшая заметка о другом ложном поведении, которое я заметил с PDO, которое с самого начала привело меня в эту кроличью нору. Кажется, не хватает обработки нескольких запросов.
quickshiftin
Я только что посмотрел на некоторые библиотеки миграции на GitHub ... Что вы знаете, эта в значительной степени делает то же самое, что и мой пост в блоге.
quickshiftin
6

Я удивлен, что никто не упомянул одну из главных причин для отключения эмуляции. При включенной эмуляции PDO возвращает все целые числа и числа с плавающей запятой в виде строк . Когда вы выключаете эмуляцию, целые числа и числа с плавающей запятой в MySQL становятся целыми числами и числами с плавающей запятой в PHP.

Для получения дополнительной информации см. Принятый ответ на этот вопрос: PHP + PDO + MySQL: как мне вернуть целочисленные и числовые столбцы из MySQL как целые числа и числа в PHP? ,

Dallin
источник
5

Зачем переключать эмуляцию на «ложь»?

Основная причина этого заключается в том, что ядро ​​базы данных выполняет подготовку вместо PDO, поскольку запрос и фактические данные отправляются отдельно, что повышает безопасность. Это означает, что когда параметры передаются в запрос, попытки ввести в них SQL блокируются, поскольку подготовленные операторы MySQL ограничены одним запросом. Это означает, что настоящий подготовленный оператор завершится ошибкой при передаче второго запроса в параметре.

Главный аргумент против использования движка базы данных для подготовки и PDO - это два обращения к серверу - одно для подготовки, а другое для передачи параметров - но я думаю, что дополнительная безопасность того стоит. Кроме того, по крайней мере в случае MySQL, кеширование запросов не было проблемой, начиная с версии 5.1.

https://tech.michaelseiler.net/2016/07/04/dont-emulate-prepared-statements-pdo-mysql/

Гарри Бош
источник
1
Кэширование запросов в любом случае отсутствует: кэш запросов устарел в MySQL 5.7.20 и удален в MySQL 8.0.
Álvaro González
0

Для записи

PDO :: ATTR_EMULATE_PREPARES = верно

Это могло вызвать неприятный побочный эффект. Он может возвращать значения типа int в виде строки.

PHP 7.4, pdo с mysqlnd.

Выполнение запроса с PDO :: ATTR_EMULATE_PREPARES = true

Столбец: id
Тип: целое число
Значение: 1

Выполнение запроса с PDO :: ATTR_EMULATE_PREPARES = false

Столбец: id
Тип: строка
Значение: «1»

В любом случае десятичные значения всегда возвращаются строкой, независимо от конфигурации :-(

Магеллана
источник
десятичные значения всегда возвращаются, строка - единственный правильный способ
здравый смысл
Да, с точки зрения MySQL, но со стороны PHP это неправильно. И Java, и C # рассматривают Decimal как числовое значение.
magallanes
Нет, это не так. Это так же правильно для всей информатики. Если вы думаете, что это неправильно, то вам нужен другой тип произвольной точности
Your Common Sense