PDO MySQL: Użyj PDO :: ATTR_EMULATE_PREPARES czy nie?

117

Oto, o czym do tej pory czytałem PDO::ATTR_EMULATE_PREPARES:

  1. Przygotować emulacja PDO jest lepsza dla wydajności od MySQL ojczystym przygotować omija cache zapytań .
  2. Natywne przygotowanie MySQL jest lepsze pod względem bezpieczeństwa (zapobiega iniekcji SQL) .
  3. Natywne przygotowanie MySQL jest lepsze do raportowania błędów .

Nie wiem już, na ile prawdziwe są te stwierdzenia. Moim największym zmartwieniem przy wyborze interfejsu MySQL jest zapobieganie wstrzykiwaniu SQL. Drugi problem to wydajność.

Moja aplikacja używa obecnie proceduralnego MySQLi (bez przygotowanych instrukcji) i całkiem sporo wykorzystuje pamięć podręczną zapytań. Rzadko będzie ponownie wykorzystywać przygotowane oświadczenia w jednym wniosku. Rozpocząłem przechodzenie na PDO ze względu na wymienione parametry i bezpieczeństwo przygotowanych zestawień.

Używam MySQL 5.1.61iPHP 5.3.2

Powinienem pozostawić PDO::ATTR_EMULATE_PREPARESwłączony czy nie? Czy istnieje sposób, aby zarówno wydajność pamięci podręcznej zapytań, jak i bezpieczeństwo przygotowywanych instrukcji?

Andrew Ensley
źródło
3
Szczerze? Po prostu używaj MySQLi. Jeśli już działa, korzystając z przygotowanych oświadczeń w ramach tego, PDO jest w zasadzie bezcelową warstwą abstrakcji. EDYCJA : PDO jest naprawdę przydatne w aplikacjach typu green field, w których nie masz pewności, która baza danych trafia do zaplecza.
jmkeyes,
1
Przepraszam, moje pytanie było wcześniej niejasne. Edytowałem to. W tej chwili aplikacja nie korzysta z przygotowanych wyciągów w MySQLi; po prostu mysqli_run_query (). Z tego, co przeczytałem, przygotowane przez MySQLi instrukcje również omijają pamięć podręczną zapytań.
Andrew Ensley,

Odpowiedzi:

108

Aby odpowiedzieć na Twoje obawy:

  1. MySQL> = 5.1.17 (lub> = 5.1.21 dla instrukcji PREPAREi EXECUTE) może używać przygotowanych instrukcji w pamięci podręcznej zapytań . Twoja wersja MySQL + PHP może więc używać przygotowanych instrukcji z pamięcią podręczną zapytań. Zwróć jednak uwagę na zastrzeżenia dotyczące buforowania wyników zapytań w dokumentacji MySQL. Istnieje wiele rodzajów zapytań, których nie można buforować lub które są bezużyteczne, mimo że są buforowane. Z mojego doświadczenia wynika, że ​​pamięć podręczna zapytań i tak nie jest często bardzo korzystna. Zapytania i schematy wymagają specjalnej konstrukcji, aby maksymalnie wykorzystać pamięć podręczną. Często buforowanie na poziomie aplikacji i tak jest konieczne na dłuższą metę.

  2. Natywne przygotowania nie mają żadnego znaczenia dla bezpieczeństwa. Pseudo przygotowane instrukcje będą nadal unikały wartości parametrów zapytania, zostanie to po prostu wykonane w bibliotece PDO z ciągami znaków zamiast na serwerze MySQL przy użyciu protokołu binarnego. Innymi słowy, ten sam kod PDO będzie równie podatny (lub niewrażliwy) na ataki polegające na wstrzykiwaniu, niezależnie od EMULATE_PREPARESustawień. Jedyna różnica polega na tym, gdzie następuje zamiana parametru - w przypadku EMULATE_PREPARES, w bibliotece PDO; bez EMULATE_PREPARES, występuje na serwerze MySQL.

  3. Bez EMULATE_PREPARESciebie mogą pojawić się błędy składniowe w czasie przygotowania, a nie w czasie wykonywania; z EMULATE_PREPARESwas dostanie tylko błędy składniowe w czasie wykonywania, ponieważ PDO nie posiada zapytanie dać MySQL aż do czasu wykonania. Zauważ, że ma to wpływ na kod, który napiszesz ! Zwłaszcza jeśli używasz PDO::ERRMODE_EXCEPTION!

Dodatkowa uwaga:

  • Koszt a prepare()(przy użyciu przygotowanych instrukcji przygotowanych natywnie) prepare();execute()może być trochę wolniejszy niż wysyłanie zwykłego zapytania tekstowego przy użyciu emulowanych gotowych instrukcji. W wielu systemach baz danych plan zapytań dla a prepare()jest również buforowany i może być współdzielony z wieloma połączeniami, ale nie sądzę, aby MySQL to robił. Jeśli więc nie użyjesz ponownie przygotowanego obiektu instrukcji do wielu zapytań, ogólne wykonanie może być wolniejsze.

Na koniec uważam, że przy starszych wersjach MySQL + PHP należy emulować przygotowane instrukcje, ale w przypadku najnowszych wersji należy wyłączyć emulację.

Po napisaniu kilku aplikacji korzystających z PDO stworzyłem funkcję połączenia PDO, która ma, moim zdaniem, najlepsze ustawienia. Prawdopodobnie powinieneś użyć czegoś takiego lub dostosować preferowane ustawienia:

/**
 * 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
źródło
26
Re nr 2: na pewno docenia, że MySQL odbiera jako parametry (do rodzimych sporządzanych sprawozdań) nie uzyskać analizowany dla SQL w ogóle ? Więc ryzyko wstrzyknięcia musi być niższe niż przy użyciu emulacji przygotowania PDO, gdzie jakakolwiek wada ucieczki (np. Problemy historyczne związane mysql_real_escape_stringze znakami wielobajtowymi) nadal pozostawiałaby otwartą na ataki wstrzykiwania?
jajeczny
2
@eggyal, robisz założenia dotyczące implementacji przygotowanych instrukcji. PDO może mieć błąd w swoich emulowanych przygotowaniach przed ucieczką, ale MySQL może również zawierać błędy. AFAIK, nie wykryto żadnych problemów z emulowanymi przygotowaniami, które mogłyby spowodować, że literały parametrów przechodzą bez zmiany znaczenia.
Francis Avila
2
Świetna odpowiedź, ale mam pytanie: czy jeśli wyłączysz EMULACJĘ, wykonanie nie będzie wolniejsze? PHP musiałby przesłać przygotowane oświadczenie do MySQL w celu walidacji i dopiero wtedy przesłać parametry. Jeśli więc użyjesz przygotowanej instrukcji 5 razy, PHP będzie rozmawiać z MySQL 6 razy (zamiast 5). Czy to nie spowolni? Poza tym myślę, że jest większa szansa, że ​​PDO może mieć błędy w procesie walidacji, niż MySQL ...
Radu Murzea 08.04.13
6
Zwróć uwagę na punkty przedstawione w tej odpowiedzi, ponownie przygotowaną emulację instrukcji, używając mysql_real_escape_stringpod maską i wynikających z tego luk w zabezpieczeniach, które mogą się pojawić (w bardzo szczególnych przypadkach skrajnych).
eggyal
6
+1 Dobra odpowiedź! Ale dla przypomnienia, jeśli używasz przygotowania natywnego, parametry nigdy nie są usuwane ani łączone w kwerendzie SQL, nawet po stronie serwera MySQL. Zanim wykonasz i podasz parametry, zapytanie zostało przeanalizowane i przekształcone w wewnętrzne struktury danych w MySQL. Przeczytaj ten blog napisany przez inżyniera ds. Optymalizacji MySQL, który wyjaśnia ten proces: guilhembichot.blogspot.com/2014/05/… Nie mówię, że to oznacza, że ​​przygotowanie natywne jest lepsze, o ile ufamy, że kod PDO wykona poprawne ucieczki (co ja robić).
Bill Karwin,
9

Uważaj na wyłączanie PDO::ATTR_EMULATE_PREPARES(włączanie natywnych przygotowań), gdy PHP pdo_mysqlnie jest kompilowane mysqlnd.

Ponieważ stary libmysqlnie jest w pełni kompatybilny z niektórymi funkcjami, może prowadzić do dziwnych błędów, na przykład:

  1. Utrata najbardziej znaczących bitów dla 64-bitowych liczb całkowitych podczas wiązania jako PDO::PARAM_INT(0x12345678AB zostanie przycięte do 0x345678AB na komputerze 64-bitowym)
  2. Brak możliwości tworzenia prostych zapytań, takich jak LOCK TABLES(zgłasza SQLSTATE[HY000]: General error: 2030 This command is not supported in the prepared statement protocol yetwyjątek)
  3. Musisz pobrać wszystkie wiersze z wyniku lub zamknąć kursor przed następnym zapytaniem (z mysqlndlub emulowanym przygotowuje, automatycznie robi to za Ciebie i nie traci synchronizacji z serwerem mysql)

Te błędy znalazłem w moim prostym projekcie po migracji na inny serwer, który był używany libmysqldla pdo_mysqlmodułu. Może jest znacznie więcej błędów, nie wiem. Testowałem również na świeżej, 64-bitowej wersji debian jessie, wszystkie wymienione błędy pojawiają się, gdy ja apt-get install php5-mysql, i znikają, gdy ja apt-get install php5-mysqlnd.

Kiedy PDO::ATTR_EMULATE_PREPARESjest ustawione na true (domyślnie) - te błędy i tak się nie zdarzają, ponieważ PDO w ogóle nie używa przygotowanych instrukcji w tym trybie. Tak więc, jeśli używasz w pdo_mysqloparciu o libmysql(podciąg "mysqlnd" nie pojawia się w polu "Wersja API klienta" w pdo_mysqlsekcji phpinfo) - nie powinieneś go PDO::ATTR_EMULATE_PREPARESwyłączać.

Sage Pointer
źródło
3
czy ta obawa jest nadal aktualna w 2019 roku ?!
oldboy
8

Wyłączyłbym emulację przygotowań, gdy używasz 5.1, co oznacza, że ​​PDO będzie korzystać z natywnej funkcji przygotowanych instrukcji.

PDO_MYSQL będzie korzystać z natywnej obsługi przygotowanych instrukcji obecnych w MySQL 4.1 i nowszych. Jeśli używasz starszej wersji bibliotek klienta mysql, PDO będzie je emulować.

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

Porzuciłem MySQLi na rzecz PDO ze względu na przygotowane nazwane instrukcje i lepsze API.

Aby jednak zachować równowagę, PDO działa pomijalnie wolniej niż MySQLi, ale warto o tym pamiętać. Wiedziałem o tym, kiedy dokonałem wyboru i zdecydowałem, że lepsze API i użycie standardu branżowego jest ważniejsze niż użycie pomijalnie szybszej biblioteki, która wiąże Cię z określonym silnikiem. FWIW Myślę, że zespół PHP również przychylnie patrzy na PDO zamiast MySQLi w przyszłości.

Will Morgan
źródło
Dziękuję za informację. W jaki sposób brak możliwości korzystania z pamięci podręcznej zapytań wpłynął na wydajność, czy w ogóle z niej korzystałeś?
Andrew Ensley
Nie mogę powiedzieć, że ramy i tak używam pamięci podręcznych na wielu poziomach. Zawsze możesz jednak jawnie użyć SELECT SQL_CACHE <reszta instrukcji>.
Will Morgan
Nie wiedziałem nawet, że istnieje opcja SELECT SQL_CACHE. Jednak wydaje się, że to nadal nie zadziała. Z dokumentacji: „Wynik zapytania jest zapisywany w pamięci podręcznej, jeśli można go zapisać w
Andrew Ensley
Tak. To zależy od natury zapytania, a nie od specyfiki platformy.
Will Morgan
Czytam to w ten sposób, że „Wynik zapytania jest przechowywany w pamięci podręcznej, chyba że coś innego uniemożliwia jego buforowanie ”, co - z tego, co czytałem do tej pory - zawierało przygotowane oświadczenia. Jednak dzięki odpowiedzi Francisa Avili wiem, że nie jest to już prawdą w przypadku mojej wersji MySQL.
Andrew Ensley
6

Polecam włączenie prawdziwych PREPAREwywołań bazy danych, ponieważ emulacja nie przechwytuje wszystkiego ... na przykład się przygotuje INSERT;!

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

Wyjście

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

Chętnie wezmę wydajność dla kodu, który faktycznie działa.

FWIW

Wersja PHP: PHP 5.4.9-4ubuntu2.4 (cli)

Wersja MySQL: 5.5.34-0ubuntu0

quickshiftin
źródło
To interesująca kwestia. Wydaje mi się, że emulacja odkłada analizę po stronie serwera do fazy wykonania. Chociaż nie jest to wielka sprawa (niewłaściwy kod SQL w końcu się nie powiedzie), jest to czystsze, aby preparewykonać zadanie, do którego powinno. (Dodatkowo zawsze zakładałem, że parser parametrów po stronie klienta będzie musiał mieć własne błędy.)
Álvaro González
1
IDK, jeśli jesteś zainteresowany, ale oto mały opis niektórych innych fałszywych zachowań, które zauważyłem w przypadku PDO, które prowadzą mnie do tej króliczej nory. Wydaje się, że brakuje obsługi wielu zapytań.
quickshiftin
Właśnie przyjrzałem się niektórym bibliotekom migracji na GitHubie ... Co wiesz, ta robi dokładnie to samo, co mój wpis na blogu.
quickshiftin
6

Jestem zaskoczony, że nikt nie wymienił jednego z największych powodów, dla których warto wyłączyć emulację. Przy włączonej emulacji PDO zwraca wszystkie liczby całkowite i zmiennoprzecinkowe jako łańcuchy . Po wyłączeniu emulacji liczby całkowite i zmiennoprzecinkowe w MySQL stają się liczbami całkowitymi i zmiennoprzecinkowymi w PHP.

Aby uzyskać więcej informacji, zapoznaj się z zaakceptowaną odpowiedzią na to pytanie: PHP + PDO + MySQL: jak zwrócić kolumny całkowite i liczbowe z MySQL jako liczby całkowite i liczby w PHP? .

dallin
źródło
5

Po co zmieniać emulację na „fałsz”?

Głównym tego powodem jest fakt, że przygotowanie bazy danych przez silnik bazy danych zamiast PDO polega na tym, że zapytanie i rzeczywiste dane są przesyłane oddzielnie, co zwiększa bezpieczeństwo. Oznacza to, że gdy parametry są przekazywane do zapytania, próby wstrzyknięcia do nich SQL są blokowane, ponieważ przygotowane przez MySQL instrukcje są ograniczone do pojedynczego zapytania. Oznacza to, że prawdziwie przygotowana instrukcja zakończy się niepowodzeniem po przekazaniu drugiego zapytania w parametrze.

Głównym argumentem przeciwko używaniu silnika bazy danych do przygotowania vs PDO są dwie wizyty na serwerze - jedna w celu przygotowania, a druga w celu przekazania parametrów - ale myślę, że dodatkowe zabezpieczenia są tego warte. Ponadto, przynajmniej w przypadku MySQL, buforowanie zapytań nie było problemem od wersji 5.1.

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

Harry Bosh
źródło
1
Buforowanie zapytań i tak zniknęło : pamięć podręczna zapytań jest przestarzała od wersji MySQL 5.7.20 i została usunięta w MySQL 8.0.
Álvaro González
0

Tak dla porządku

PDO :: ATTR_EMULATE_PREPARES = true

Może to spowodować nieprzyjemny efekt uboczny. Może zwrócić wartości int jako ciąg.

PHP 7.4, pdo z mysqlnd.

Uruchamianie zapytania z PDO :: ATTR_EMULATE_PREPARES = true

Kolumna: id
Typ: liczba całkowita
Wartość: 1

Uruchamianie zapytania z PDO :: ATTR_EMULATE_PREPARES = false

Kolumna: identyfikator
Typ: ciąg
Wartość: „1”

W każdym przypadku wartości dziesiętne są zawsze zwracane jako ciąg znaków, niezależnie od konfiguracji :-(

magallanes
źródło
wartości dziesiętne są zawsze zwracane jako jedyny prawidłowy sposób
Twój zdrowy rozsądek
Tak, z punktu widzenia MySQL, ale po stronie PHP jest to błąd. Zarówno Java, jak i C # traktują Decimal jako wartość liczbową.
magallanes
Nie, nie jest. Jak wszystko jest poprawne dla całej informatyki. Jeśli uważasz, że to źle, potrzebujesz innego rodzaju, o arbitralnej precyzji
Twój zdrowy rozsądek