Подготовка запросов с привязкой: DatabasePrepare

Во многих случаях в SQL-запросы необходимо встроить параметры. В принципе, поскольку SQL-запрос представляет собой "изначально" строку, отвечающую специальному синтаксису, её можно сформировать обычным вызовом StringFormat или конкатенацией, дополнив в нужных местах значениями параметров. Данный прием уже использовался нами в запросах на создание таблицы ("CREATE TABLE %s '%s' (%s);"), но здесь только часть параметров содержала данные (список значений подставлялся вместо %s внутри круглых скобок), а остальные представляли собой опцию и имя таблицы. В данном разделе речь пойдет исключительно о подстановке данных в запрос. Делать это "родным" для SQL способом важно по нескольким причинам.

Прежде всего, SQL-запрос лишь передается "движку" SQLite в виде строки, а там разбирается на компоненты, проверяется на корректность и неким образом "компилируется" (конечно, это не компилятор MQL5). Затем откомпилированный запрос выполняется базой. Именно поэтому мы взяли слово "изначально" в кавычки.

Когда один и тот же запрос нужно выполнить с различными параметрами (например, вставить в таблицу много записей — а мы к этой задаче медленно приближаемся), отдельная компиляция и проверка запроса для каждой записи — довольно неэффективна. Правильнее откомпилировать запрос один раз, а затем выполнять массовым образом, просто подставляя разные значения.

Данная операция компиляции называется подготовкой запроса и выполняется функцией DatabasePrepare.

Подготовленные запросы имеют и еще одно предназначение — с их помощью "движок" SQLite возвращает результаты выполнения запросов в код MQL5 (подробнее об этом рассказано в разделах Выполнение подготовленных запросов и Раздельное чтение полей записи результатов запроса).

Последний, не менее важный нюанс, связанный с параметризованными запросами, заключается в том, что они защищают вашу программу от потенциальных хакерских атак под названием "внедрение SQL" (SQL injection). Это, в первую очередь, критично для баз данных публичных сайтов, где вводимая пользователями информация записывается в базу путем встраивания в SQL-запросы: если в этом случае применить простую форматную подстановку '%s', пользователь сможет ввести вместо предполагаемых данных некую длинную строку с дополнительными командами SQL, и она станет частью исходного SQL-запроса, исказив его смысл. Если же SQL-запрос скомпилирован, его не удастся изменить входными данными: они всегда обрабатываются как данные.

Хотя MQL-программа не является серверной, все же и она может сохранять в базе информацию, получаемую от пользователя.

int DatabasePrepare(int database, const string sql, ...)

Функция DatabasePrepare создает в указанной базе данных дескриптор для запроса в строке sql. База данных database должна быть заранее открыта функцией DatabaseOpen.

Места расположения параметров запроса указываются в строке sql с помощью фрагментов '?1', '?2', '?3', и так далее. Нумерация означает индекс параметра, используемый в будущем, при назначении ему входной величины, в DatabaseBind-функциях. Номера в строке sql не обязаны идти по порядку и могут повторяться, если один и тот же параметр нужно вставить в разные места запроса.

Внимание! Индексация в подстановочных фрагментах '?n' начинается с 1, в то время как в DatabaseBind-функциях — с 0. Например, параметр '?1' в тексте запроса получит значение при вызове DatabaseBind с индексом 0, параметр '?2' — по индексу 1, и так далее. Такое постоянное смещение на 1 сохраняется даже в том случае, если в нумерации параметров '?n' есть пропуски (случайные или намеренные).

Если все параметры планируется привязывать строго по порядку, можно применить сокращенную запись: на месте каждого параметра просто указать символ '?' без номера: в этом случае параметры автоматически нумеруются. Любой параметр '?' без номера получает номер на 1 больше максимального из прочитанных левее параметров (с явно указанными номерами или рассчитанные по такому же принципу, а самый первый получит номер 1, то есть '?1').

Таким образом, запрос:

SELECT * FROM table WHERE risk > ?1 AND signal = ?2

эквивалентен:

SELECT * FROM table WHERE risk > ? AND signal = ?

Если часть параметров постоянна или запрос подготавливается для однократного исполнения с целью получить результат, значения параметров можно передать непосредственно в функцию DatabasePrepare, списком через запятую, вместо многоточия (также как в Print или Comment).

Параметры запроса разрешено использовать только для задания значений в столбцах таблицы (при записи, изменении или в условиях отбора). Названия таблиц, столбцов, опции, ключевые слова SQL — нельзя передавать через параметры '?'/'?n'.

Сама функция DatabasePrepare не выполняет запрос. Возвращаемый из неё дескриптор затем должен передаваться в вызовы функций DatabaseRead или DatabaseReadBind — именно они выполняют запрос и делают доступным для чтения результат (это может быть одна запись или много). Разумеется, если в запросе есть заместители параметров ('?' или '?n'), и значения для них не были указаны в DatabasePrepare, перед выполнением запроса требуется осуществить привязку параметров и данных с помощью соответствующих DatabaseBind-функций.

Если какому-либо параметру так и не было назначено значение, во время выполнения запроса вместо него подставляется NULL.

В случае ошибки функция DatabasePrepare вернет INVALID_HANDLE.

Пример использования DatabasePrepare мы представим в следующих разделах, после изучения других функций, связанных с подготовленными запросами.