PDO MySQL: ¿Usa PDO :: ATTR_EMULATE_PREPARES o no?

117

Esto es lo que he leído hasta ahora PDO::ATTR_EMULATE_PREPARES:

  1. La emulación de preparación de PDO es mejor para el rendimiento, ya que la preparación nativa de MySQL evita la caché de consultas .
  2. La preparación nativa de MySQL es mejor para la seguridad (evitando la inyección de SQL) .
  3. La preparación nativa de MySQL es mejor para la notificación de errores .

Ya no sé qué tan cierta es ninguna de estas declaraciones. Mi mayor preocupación al elegir una interfaz MySQL es evitar la inyección de SQL. La segunda preocupación es el rendimiento.

Mi aplicación utiliza actualmente MySQLi procedimental (sin declaraciones preparadas) y utiliza bastante la caché de consultas. Rara vez reutilizará declaraciones preparadas en una sola solicitud. Comencé a moverme a PDO para los parámetros con nombre y la seguridad de las declaraciones preparadas.

Estoy usando MySQL 5.1.61yPHP 5.3.2

¿Debo dejar PDO::ATTR_EMULATE_PREPAREShabilitado o no? ¿Hay alguna manera de tener tanto el rendimiento de la caché de consultas como la seguridad de las declaraciones preparadas?

Andrew Ensley
fuente
3
¿Honestamente? Sigue usando MySQLi. Si ya está funcionando usando declaraciones preparadas debajo de eso, PDO es básicamente una capa inútil de abstracción. EDITAR : PDO es realmente útil para aplicaciones de campo verde en las que no está seguro de qué base de datos va al back-end.
jmkeyes
1
Lo siento, mi pregunta no estaba clara antes. Lo he editado. La aplicación no utiliza declaraciones preparadas en MySQLi en este momento; solo mysqli_run_query (). Por lo que he leído, las declaraciones preparadas por MySQLi también pasan por alto la caché de consultas.
Andrew Ensley

Respuestas:

108

Para responder a sus inquietudes:

  1. MySQL> = 5.1.17 (o> = 5.1.21 para las declaraciones PREPAREy EXECUTE) puede usar declaraciones preparadas en la caché de consultas . Entonces, su versión de MySQL + PHP puede usar declaraciones preparadas con la caché de consultas. Sin embargo, tenga en cuenta las advertencias para almacenar en caché los resultados de las consultas en la documentación de MySQL. Hay muchos tipos de consultas que no se pueden almacenar en caché o que son inútiles aunque estén en caché. En mi experiencia, la caché de consultas no suele ser una gran ganancia de todos modos. Las consultas y los esquemas necesitan una construcción especial para aprovechar al máximo la caché. A menudo, el almacenamiento en caché a nivel de aplicación termina siendo necesario de todos modos a largo plazo.

  2. Native se prepara no hace ninguna diferencia para la seguridad. Las declaraciones pseudopreparadas aún escaparán de los valores de los parámetros de consulta, solo se hará en la biblioteca PDO con cadenas en lugar de en el servidor MySQL utilizando el protocolo binario. En otras palabras, el mismo código PDO será igualmente vulnerable (o no vulnerable) a los ataques de inyección independientemente de su EMULATE_PREPARESconfiguración. La única diferencia es dónde se produce el reemplazo del parámetro, con EMULATE_PREPARES, ocurre en la biblioteca PDO; sin EMULATE_PREPARES, ocurre en el servidor MySQL.

  3. Sin EMULATE_PREPARESél, puede obtener errores de sintaxis en el tiempo de preparación en lugar de en el tiempo de ejecución; con EMULATE_PREPARES, solo obtendrá errores de sintaxis en el momento de la ejecución porque PDO no tiene una consulta para entregar a MySQL hasta el momento de la ejecución. ¡Tenga en cuenta que esto afecta el código que escribirá ! ¡Especialmente si estás usando PDO::ERRMODE_EXCEPTION!

Una consideración adicional:

  • Hay un costo fijo para a prepare()(usando declaraciones preparadas nativas), por lo queprepare();execute() con declaraciones preparadas nativas puede ser un poco más lento que emitir una consulta de texto sin formato usando declaraciones preparadas emuladas. En muchos sistemas de bases de datos, el plan de consulta para a también prepare()se almacena en caché y puede compartirse con múltiples conexiones, pero no creo que MySQL haga esto. Entonces, si no reutiliza su objeto de declaración preparado para múltiples consultas, su ejecución general puede ser más lenta.

Como recomendación final , creo que con versiones anteriores de MySQL + PHP, debería emular declaraciones preparadas, pero con sus versiones más recientes debería desactivar la emulación.

Después de escribir algunas aplicaciones que usan PDO, he creado una función de conexión PDO que tiene lo que creo que son las mejores configuraciones. Probablemente debería usar algo como esto o ajustar su configuración preferida:

/**
 * 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;
}
Francis Avila
fuente
26
Re # 2: seguramente los valores que MySQL recibe como parámetros (para declaraciones nativas preparadas) no se analizan para SQL en absoluto ? Entonces, ¿el riesgo de inyección debe ser menor que el uso de la emulación de preparación de PDO, donde cualquier falla en el escape (por ejemplo, los problemas históricos mysql_real_escape_stringcon caracteres de varios bytes) aún dejaría a uno abierto a ataques de inyección?
eggyal
2
@eggyal, está haciendo suposiciones sobre cómo se implementan las declaraciones preparadas. PDO puede tener un error en su preparación emulada para escapar, pero MySQL también puede tener errores. AFAIK, no se han descubierto problemas con las preparaciones emuladas que podrían hacer que los literales de los parámetros pasen sin escapar.
Francis Avila
2
Impresionante respuesta, pero tengo una pregunta: si desactivas EMULATION, ¿no será más lenta la ejecución? PHP tendría que enviar la declaración preparada a MySQL para su validación y solo entonces enviar los parámetros. Entonces, si usa la declaración preparada 5 veces, PHP hablará con MySQL 6 veces (en lugar de 5). ¿No lo hará esto más lento? Además, creo que hay una mayor posibilidad de que PDO tenga errores en el proceso de validación, en lugar de MySQL ...
Radu Murzea
6
Tenga en cuenta los puntos señalados en esta respuesta sobre la emulación de declaraciones preparadas utilizando mysql_real_escape_stringbajo el capó y las consiguientes vulnerabilidades que pueden surgir (en casos extremos muy particulares).
eggyal
6
+1 ¡Buena respuesta! Pero para el registro, si usa la preparación nativa, los parámetros nunca se escapan ni se combinan en la consulta SQL, incluso en el lado del servidor MySQL. En el momento en que ejecuta y proporciona los parámetros, la consulta se ha analizado y transformado en estructuras de datos internas en MySQL. Lea este blog de un ingeniero optimizador de MySQL que explica este proceso: guilhembichot.blogspot.com/2014/05/… No estoy diciendo que esto signifique que la preparación nativa es mejor, en la medida en que confiamos en el código PDO para escapar correctamente (lo que yo hacer).
Bill Karwin
9

Tenga cuidado con la desactivación PDO::ATTR_EMULATE_PREPARES(activando la preparación nativa) cuando su PHP pdo_mysqlno está compilado contramysqlnd .

Como lo antiguo libmysqlno es totalmente compatible con algunas funciones, puede provocar errores extraños, por ejemplo:

  1. Perder los bits más significativos para enteros de 64 bits cuando se vincula como PDO::PARAM_INT(0x12345678AB se recortará a 0x345678AB en una máquina de 64 bits)
  2. Incapacidad para realizar consultas simples como LOCK TABLES(lanza una SQLSTATE[HY000]: General error: 2030 This command is not supported in the prepared statement protocol yetexcepción)
  3. Necesita recuperar todas las filas del resultado o cerrar el cursor antes de la siguiente consulta (con mysqlndo emulado se prepara automáticamente hace esto por usted y no se desincroniza con el servidor mysql)

Estos errores los descubrí en mi proyecto simple cuando migré a otro servidor que usé libmysqlpara pdo_mysqlmodule. Quizás haya muchos más errores, no lo sé. También probé en debian jessie de 64 bits, todos los errores enumerados ocurren cuando yo apt-get install php5-mysql, y desaparecen cuandoapt-get install php5-mysqlnd .

Cuando PDO::ATTR_EMULATE_PREPARESse establece en verdadero (por defecto): estos errores no ocurren de todos modos, porque PDO no usa declaraciones preparadas en absoluto en este modo. Por lo tanto, si usa la subcadena pdo_mysqlbasada en libmysql(la subcadena "mysqlnd" no aparece en el campo de la pdo_mysqlsección "Versión de la API del cliente" en phpinfo), no debe PDO::ATTR_EMULATE_PREPARESapagar.

Puntero sabio
fuente
3
¿Esta preocupación sigue siendo válida en 2019?
oldboy
8

Desactivaría emular prepares mientras ejecuta 5.1, lo que significa que PDO aprovechará la funcionalidad de declaración preparada nativa.

PDO_MYSQL aprovechará el soporte nativo de declaraciones preparadas presente en MySQL 4.1 y superior. Si está utilizando una versión anterior de las bibliotecas cliente de mysql, PDO las emulará por usted.

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

Dejé MySQLi para PDO por las declaraciones con nombre preparadas y la mejor API.

Sin embargo, para equilibrarlo, PDO se comporta insignificantemente más lento que MySQLi, pero es algo a tener en cuenta. Lo supe cuando tomé la decisión y decidí que una mejor API y el uso del estándar de la industria era más importante que usar una biblioteca insignificantemente más rápida que lo vincula a un motor en particular. FWIW Creo que el equipo de PHP también está mirando favorablemente a PDO sobre MySQLi para el futuro.

Will Morgan
fuente
Gracias por esa información. ¿Cómo ha afectado su rendimiento el hecho de no poder usar la caché de consultas o incluso la estaba usando antes?
Andrew Ensley
No puedo decir que como marco estoy usando cachés en múltiples niveles de todos modos. Sin embargo, siempre puede usar explícitamente SELECT SQL_CACHE <resto de la declaración>.
Will Morgan
Ni siquiera sabía que había una opción SELECT SQL_CACHE. Sin embargo, parece que eso todavía no funcionaría. De los documentos: "El resultado de la consulta se almacena en caché si se puede almacenar en caché ..." dev.mysql.com/doc/refman/5.1/en/query-cache-in-select.html
Andrew Ensley
Si. Eso depende de la naturaleza de la consulta, más que de las especificaciones de la plataforma.
Will Morgan
Leí que significa "El resultado de la consulta se almacena en caché a menos que algo más impida que se pueda almacenar en caché ", que, por lo que había leído hasta entonces, incluía declaraciones preparadas. Sin embargo, gracias a la respuesta de Francis Avila, sé que eso ya no es cierto para mi versión de MySQL.
Andrew Ensley
6

Recomendaría habilitar PREPAREllamadas a bases de datos reales ya que la emulación no captura todo ..., por ejemplo, ¡se preparará INSERT;!

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

La salida

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

Con mucho gusto recibiré un golpe de rendimiento para el código que realmente funciona.

FWIW

Versión de PHP: PHP 5.4.9-4ubuntu2.4 (cli)

Versión de MySQL: 5.5.34-0ubuntu0

quickshiftin
fuente
Es un punto interesante. Supongo que la emulación pospone el análisis del lado del servidor a la fase de ejecución. Si bien no es un gran problema (el SQL incorrecto eventualmente fallará), es más limpio dejar que preparehaga el trabajo que se supone que debe hacer. (Además, siempre he asumido que el analizador de parámetros del lado del cliente necesariamente tendrá errores propios).
Álvaro González
1
IDK si está interesado, pero aquí hay una pequeña reseña sobre algún otro comportamiento espurio que noté con PDO que me llevó a este agujero de conejo para empezar. Parece que falta el manejo de múltiples consultas.
quickshiftin
Acabo de ver algunas bibliotecas de migraciones en GitHub ... ¿Qué sabes? Esta hace exactamente lo mismo que la publicación de mi blog.
quickshiftin
5

¿Por qué cambiar la emulación a 'falso'?

La razón principal de esto es que hacer que el motor de la base de datos realice la preparación en lugar de PDO es que la consulta y los datos reales se envían por separado, lo que aumenta la seguridad. Esto significa que cuando los parámetros se pasan a la consulta, los intentos de inyectar SQL en ellos se bloquean, ya que las declaraciones preparadas de MySQL se limitan a una sola consulta. Eso significa que una verdadera declaración preparada fallaría cuando se pasara una segunda consulta en un parámetro.

El principal argumento en contra del uso del motor de base de datos para la preparación frente a la PDO son los dos viajes al servidor, uno para la preparación y otro para que se pasen los parámetros, pero creo que la seguridad adicional vale la pena. Además, al menos en el caso de MySQL, el almacenamiento en caché de consultas no ha sido un problema desde la versión 5.1.

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

Harry Bosh
fuente
1
El almacenamiento en caché de consultas desapareció de todos modos: el caché de consultas está en desuso a partir de MySQL 5.7.20 y se elimina en MySQL 8.0.
Álvaro González
5

Me sorprende que nadie haya mencionado una de las principales razones para desactivar la emulación. Con la emulación activada, PDO devuelve todos los números enteros y flotantes como cadenas . Cuando desactiva la emulación, los números enteros y flotantes en MySQL se convierten en números enteros y flotantes en PHP.

Para obtener más información, consulte la respuesta aceptada para esta pregunta: PHP + PDO + MySQL: ¿cómo devuelvo columnas enteras y numéricas de MySQL como enteros y numéricos en PHP? .

dallin
fuente
0

Para el registro

PDO :: ATTR_EMULATE_PREPARES = verdadero

Podría generar un efecto secundario desagradable. Podría devolver valores int como cadena.

PHP 7.4, pdo con mysqlnd.

Ejecutando una consulta con PDO :: ATTR_EMULATE_PREPARES = true

Columna:
tipo de identificación : entero
Valor: 1

Ejecutando una consulta con PDO :: ATTR_EMULATE_PREPARES = false

Columna: id
Tipo: cadena
Valor: "1"

En cualquier caso, los valores decimales siempre se devuelven como una cadena, independientemente de la configuración :-(

magallanes
fuente
los valores decimales siempre se devuelven, una cadena es la única forma correcta
su sentido común
Sí, desde el punto de vista de MySQL, pero está mal en el lado de PHP. Tanto Java como C # consideran Decimal como un valor numérico.
magallanes
No, no lo es. Todo es correcto para toda la informática. Si cree que está mal, entonces necesita otro tipo, de precisión arbitraria
su sentido común