подготовленное заявление для быстрой массовой вставки - PullRequest
0 голосов
/ 26 августа 2018

В двух словах

Есть ли способ в Perl использовать подготовленные операторы (для предотвращения внедрения SQL) для вставки 1 миллиона записей менее чем за 2 минуты в MySQL-таблицу?


Подробно

Существует онлайн-ресурс ( Wikimedia ), из которого я хочу загрузить файл ( dewiktionary-latest-all-title-in-ns0.gz ), который содержит почти 1 миллион названий статей (каждая статья представляет собой описание немецкого слова в Викисловарь). Я хочу проверять этот список один раз в неделю, а затем реагировать на новые или удаленные заголовки. Для этого я хочу автоматически загружать этот список раз в неделю и вставлять его в базу данных.

Хотя я доверяю Викимедиа, вам никогда не следует слишком сильно доверять вещам, приходящим из Интернета. Поэтому, чтобы предотвратить внедрение SQL и другие проблемы с безопасностью, я всегда использую подготовленные операторы в Perl. Убедитесь, что интерпретатор SQL не имеет возможности интерпретировать контент как код.

Обычно я бы сделал это так:

программа 1

#!/usr/bin/perl -w

use strict;
use warnings;
use LWP::UserAgent;
use DBI;

# DOWNLOAD FROM INTERNET =========================
# create User-Agent:
my $ua = LWP::UserAgent->new;
# read content from Internet
my $response = $ua->get('https://<rest_of_URL>');
# decode content
my $content = $response->decoded_content;

#turn into a list
my @list = split(/\n/,$content);

# STORE IN DATABASE ==============================
# connect with database (create DataBase-Handle):
my $dbh = DBI->connect(
    'DBI:mysql:database=<name_of_DB>;host=localhost',
    '<user>','<password>',
    {mysql_enable_utf8mb4 => 1}
);
# SQL statement
my $SQL = 'INSERT INTO `mytable`(`word`) VALUES(?)';
# prepare statement (create Statement Handle)
my $SH = $dbh->prepare($SQL);
#execute in a loop
foreach my $word (@list) {
    $SH->execute($word);
}
# disconnect from database
$dbh->disconnect;
# end of program
exit(0);

Обратите внимание на эту строку (строка 27):

my $SQL = 'INSERT INTO `mytable`(`word`) VALUES(?)';

В командной строке SQL стоит вопросительный знак в качестве заполнителя. В следующей строке эта командная строка SQL подготавливается (т.е. создается подготовленный оператор), и в цикле выполняется этот оператор, что означает, что каждый раз новое значение ($ word) будет вставляться в таблицу, не имея любой шанс выполнить это значение, потому что интерпретатор SQL не видит это значение. Поэтому, что бы злоумышленник не написал в загруженный файл, это никогда не приведет к внедрению кода.

Но:
Это очень медленно. Загрузка выполняется в течение нескольких секунд, но цикл вставки работает более четырех часов.


Существует более быстрое решение, оно выглядит так:

программа 2

# The code above the SQL-Statement is exactly
# the same as in the 1st program
#-------------------------------------------------
# SQL statement
my $SQL = 'INSERT INTO `mytable`(`word`) VALUES ';  # <== NO '?'!
# attach values in a loop
# initiate comma with empty string
my $comma = '';
foreach my $word (@list) {
    # escape escapecharacter
    $word =~ s/\\/\\\\/g;
    # escape quotes
    $word =~ s/'/\\'/g;
    # put the value in quotes and then in brackets, add the comma
    # and then append it to the SQL command string
    $SQL .= $comma."('".$word."')";
    # comma must be a comma
    $comma = ',';
}
# Now prepare this mega-statement
my $SH = $dbh->prepare($SQL);
# and execute it without any parameter
$SH->execute();
# disconnect from database
$dbh->disconnect;
# end of program
exit(0);

(Это упрощено, так как оператор SQL станет слишком длинным, чтобы быть принятым MySQL. Вам нужно разбить его на разделы из примерно 5000 значений и выполнить их. Но это не важно для проблема, о которой я говорю здесь.)

Это работает очень быстро. Все значения (почти 1 миллион строк в новой таблице) вставляются менее чем за 2 минуты, это более чем в 100 раз быстрее.

Как видите, я создаю одно большое заявление, но без заполнителей. Я записываю значения непосредственно в команду SQL. Мне просто нужно было избежать обратной косой черты, которая будет интерпретироваться как escape-символы и одинарные кавычки, которые будут интерпретироваться как конец строки.

Но остальные значения остаются незащищенными и видимыми для интерпретатора SQL. Потенциальный злоумышленник может найти способ вставить код SQL в значения, которые будут выполнены. Это может повредить мою базу данных или даже предоставить права суперпользователя злоумышленнику. (повышение привилегий, вызванное внедрением кода)


Итак, вот мой вопрос:

Есть ли способ использовать подготовленные операторы, как в программе 1, даже для операторов, которые генерируются динамически, как в программе 2?

Или есть еще одна возможность быстро и безопасно вставить большие объемы данных в таблицу MySQL?

Ответы [ 2 ]

0 голосов
/ 26 августа 2018

(Этот ответ написан как автор вопроса.)

e.dan привел меня к правильной идее с своим ответом , так что спасибо, e.dan!

Вот быстрое решение, использующее подготовленные операторы:

# The code above the SQL-Statement is exactly
# the same as in the 1st program in the question
#-------------------------------------------------
# SQL statement
my $SQL = 'INSERT INTO `mytable`(`word`) VALUES ';
# Counter
my $cnt   = 0;
# initiate comma with empty string
my $comma = '';
# An array to store the parameters (This array does the trick!)
my @param = ();
# loop through all words
foreach my $word (@list) {
    # (no escaping needed)
    # attach a question mark in brackets to the query string
    $SQL .= $comma."(?)";
    # and push the value into the parameter-array
    push(@param,$word);
    # next time it must be a comma
    $comma = ',';
    # increment the counter
    $cnt++;
    # limit reached?
    if ($cnt >= 5000) {
        # Yes, limit reached
        # prepare the string with 5000 question marks
        my $SH = $dbh->prepare($SQL);
        # hand over a list of 5000 values and execute the prepared statement
        # (for Perl a comma separated list and an array are equal
        # if used as parameter for a function call)
        $SH->execute(@param);
        # Reset the variables
        $SQL = 'INSERT INTO `mytable`(`word`) VALUES ';
        $cnt = 0;
        $comma = '';
        @param = ();
    }
}
# is there something left at the end?
if ($comma ne '') {
    # Yes, there is something left at the end
    # prepare the string with many (but less than 5000) question marks
    my $SH = $dbh->prepare($SQL);
    # hand over the list of values and execute the prepared statement
    $SH->execute(@param);
}
# disconnect from database
$dbh->disconnect;
# end of program
exit(0);

Хитрость в том, что когда вы вызываете функцию или метод в Perl, вы можете передать параметры в виде скаляров, разделенных запятыми:

object->method($scalar1, $scalar2, $scalar3);

Но вы также можете передать массив:

my $@array = ($scalar1, $scalar2, $scalar3);
object->method(@array);

Итак, вы можете использовать массив для передачи переменного количества параметров, а также вы можете легко передать более 5000 (или даже больше) параметров.

Кстати:
Эта версия была даже быстрее, чем версия 2 из моего вопроса.

0 голосов
/ 26 августа 2018

Ваша небольшая заметка, выделенная курсивом, на самом деле весьма актуальна:

(Это упрощено, поскольку оператор SQL станет слишком длинным, чтобы быть принятым MySQL. Вам нужно разбить его на частив разделах около 5000 значений и выполните их. Но это не важно для проблемы, о которой я говорю здесь.)

Я думаю, что ваше «неподготовленное утверждение» (ненастоящий термин) быстрее, потому что вы загружаете 5000 записей за раз, а не по одной, а не потому, что это не подготовленный оператор.

Попробуйте создать подготовленный оператор с 5000 ? sthis:

my $SQL = 'INSERT INTO `mytable`(`word`) VALUES ' . '(?),'x4999 . '(?)';

Затем создайте список из 5000 слов за один раз и выполните с этим подготовленный оператор.Вам придется иметь дело с последним набором (предположительно) менее чем 5000 слов со вторым динамически сгенерированным подготовленным оператором соответствующего количества слов в последнем пакете.

Вы также можете посмотреть на LOAD DATA INFILE для массовой загрузки.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...