- Знакомство с принципами работы с базой данных в MQL5
- Основы SQL
- Структура (схема) таблиц: типы данных и ограничения
- Интеграция ООП (MQL5) и SQL: концепция ORM
- Создание, открытие и закрытие базы данных
- Выполнение запросов без привязки к данным MQL5
- Проверка существования таблицы в базе данных
- Подготовка запросов с привязкой: DatabasePrepare
- Удаление и сброс подготовленных запросов
- Привязка данных к параметрам запроса:DatabaseBind/Array
- Выполнение подготовленных запросов: DatabaseRead/Bind
- Раздельное чтение полей: DatabaseColumn-функции
- Примеры CRUD-операций в SQLite через объекты ORM
- Транзакции
- Импорт и экспорт таблицы базы данных
- Печать таблиц и SQL-запросов в журнал
- Пример поиска торговой стратегии средствами SQLite
Выполнение запросов без привязки к данным MQL5
Некоторые SQL-запросы представляют собой команды, которые достаточно отправить в "движок" как есть, и не требуют ни переменных входных данных, ни получения результатов. Например, если наша MQL-программа должна создать в базе таблицу, индекс или представление с определенной структурой и именем, мы можем прописать её константной строкой с оператором "CREATE ...". Кроме того, такие запросы удобно использовать для пакетной обработки записей или их комбинирования (слияния, вычисления агрегированных показателей, однотипной модификации). То есть одним запросом можно преобразовать данные таблиц целиком или заполнить на их основе другие таблицы. В последующих запросах можно будет анализировать эти результаты.
Во всех этих случаях важно лишь получить подтверждение успешности действия. Запросы такого типа выполняются с помощью функции DatabaseExecute.
bool DatabaseExecute(int database, const string sql)
Функция исполняет запрос в базе данных, указанной дескриптором database. Сам запрос передается в виде готовой строки sql.
Функция возвращает признак успеха (true) или ошибки (false).
Например, мы можем дополнить свой класс DBSQLite таким методом (дескриптор уже имеется внутри объекта).
class DBSQLite
|
Тогда скрипт, создающий новую таблицу (а при необходимости, предварительно, и саму базу данных), может выглядеть так (DBcreateTable.mq5).
input string Database = "MQL5Book/DB/Example1";
|
После выполнения скрипта попробуйте открыть указанную базу в MetaEditor и убедиться, что в ней имеется пустая таблица с единственным текстовым полем "msg". Но это можно сделать и программным способом (см. следующий раздел).
Интересно, что если мы запустим скрипт второй раз с теми же параметрами, то получим ошибку (хотя и некритическую, без принудительного закрытия программы).
database error, table table1 already exists
|
Дело в том, что нельзя повторно создать уже существующую таблицу. Но SQL позволяет подавить эту ошибку и создать таблицу, только если её до сих пор не было, а в противном случае практически ничего не делать и вернуть признак успеха. Для этого достаточно добавить в запрос "IF NOT EXISTS" перед именем.
db.execute(StringFormat("CREATE TABLE IF NOT EXISTS %s (msg text)", Table)); |
На практике таблицы требуются для хранения информации об объектах прикладной области: котировках, сделках, торговых сигналах. Поэтому желательно автоматизировать создание таблиц на основе описания объектов в MQL5. Как мы увидим далее, функции SQLite предоставляют возможность привязать результаты запросов к структурам MQL5 (но не классам). В связи с этим, в рамках ORM-обертки, разработаем механизм по генерации SQL-запроса "CREATE TABLE" по описанию struct конкретного вида в MQL5.
Для этого требуется зарегистрировать имена и типы полей структуры каким-либо образом в общем списке в момент компиляции, а затем, уже на стадии исполнения программы, по этому списку можно формировать SQL-запросы.
На стадии компиляции происходит разбор нескольких категорий сущностей MQL5, которыми можно воспользоваться для выявления типов и имен:
Прежде всего, следует напомнить, что собираемые описания полей относятся к контексту конкретной структуры и их нельзя перемешивать, потому что в программе может оказаться много разных структур с потенциально совпадающими названиями и типами. Иными словами, информацию желательно аккумулировать в раздельных списках по каждому типу структуры. Для этого идеально подойдет шаблонный тип, параметром шаблона которого (S) будет выступать прикладная структура. Назовем шаблон DBEntity.
template<typename S>
|
Внутри шаблона — многомерный массив prototype, в который и будем записывать описание полей. Чтобы перехватить тип и имя прикладного поля потребуется объявить внутри DBEntity еще одну шаблонную структуру DBField: на этот раз её параметр T является типом самого поля. В конструкторе мы имеем информацию об этом типе (typename(T)), а также получаем название поля (и опционально, ограничение — о нем чуть ниже) в виде параметров.
template<typename S>
|
Поле f не используется, но оно нужно, потому что структуры не могут быть пустыми.
Допустим, что у нас есть прикладная структура Data (DBmetaProgramming.mq5).
struct Data
|
Мы можем сделать её аналог, унаследованный от DBEntity<DataDB>, но с подмененными полями на основе DBField, идентичными исходному набору.
struct DataDB: public DBEntity<DataDB>
|
За счет подстановки названия структуры в параметр родительного шаблона, структура предоставляет программе информацию о собственных свойствах.
Обратите внимание на одномоментное определение переменной proto вместе с декларацией структуры. Это нужно, потому что в шаблонах каждый конкретный параметризованный тип компилируется только в случае, если в исходном коде создается хотя бы один объект такого типа. И для нас важно, чтобы создание этого прото-объекта происходило в самом начале запуска программы, в момент инициализации глобальных переменных.
Под идентификатором DB_FIELD скрывается макрос:
#define DB_FIELD(T,N) struct T##_##N: DBField<T> { T##_##N() : DBField<T>(#N) { } } \
|
Вот как он раскрывается для отдельного поля:
struct Type_Name: DBField<Type>
|
Здесь структура также не только определяется, но и сразу же создается: фактически она подменяет собой оригинальное поле.
Поскольку структура DBField содержит единственную переменную f нужного типа, размеры и внутреннее двоичное представление Data и DataDB идентично. В этом легко убедиться, запустив скрипт DBmetaProgramming.mq5.
void OnStart()
|
Он выводит в журнал:
DBEntity<Data>::DBField<long>::DBField<long>(const string,const string)
|
Правда, для доступа к полям в DataDB нужно писать нечто неудобное: data._long_id.f, data._string_name.f, data._datetime_timestamp.f, data._double_income.f.
Мы не будем так делать не только и не столько из-за неудобства, а потому что данный способ конструирования мета-структур не совместим с принципами привязки данных к запросам SQL. В следующих разделах мы приступим к изучению Database-функций, позволяющих получать записи таблиц и результатов SQL-запросов в структуры MQL5, однако там разрешено использовать только простые структуры без наследования и статических членов объектных типов. Поэтому требуется слегка изменить принцип выявления мета-информации.
Нам придется оставлять исходные типы структур неизменными, а описание для базы данных фактически повторять, следя за тем, чтобы не было разночтений (опечаток). Это не очень удобно, но иного способа в данный момент нет.
Мы перенесем декларацию экземпляров DBEntity и DBField за пределы прикладных структур. При этом макроc DB_FIELD получит дополнительный параметр (S), в котором нужно будет передать тип прикладной структуры (ранее он неявно брался за счет объявления внутри самой структуры).
#define DB_FIELD(S,T,N) \
|
Поскольку у столбцов таблицы могут быть ограничения, их также потребуется при необходимости передавать в конструктор DBField. Для этой цели добавим пару макросов с соответствующими параметрами (в принципе, ограничений у одного столбца может быть несколько, но обычно, не более двух).
#define DB_FIELD_C1(S,T,N,C1) \
|
Все три макроса, как и дальнейшие наработки, попадают в заголовочный файл DBSQLite.mqh.
Важно отметить, что данная "самодельная" привязка объектов к таблице востребована только для ввода данных в базу, потому что чтение данных из таблицы в объект реализовано в MQL5 с помощью функции DatabaseReadBind.
Реализацию DBField также усовершенствуем. Напомним, что типы MQL5 не соответствуют один в один классам хранения SQL, в связи с чем нужно выполнить преобразование при заполнении элемента prototype[n][0]. Этим занимается статический метод affinity.
template<typename T>
|
Использованные здесь текстовые константы обобщенных типов SQL вынесены в отдельное пространство имен: потребность в них может возникнуть в разных местах MQL-программ, и следует гарантировать отсутствие конфликтов имен.
namespace DB_TYPE
|
Заготовки возможных ограничений также описаны в своей группе для удобства (как подсказки).
namespace DB_CONSTRAINT
|
Поскольку среди ограничений есть такие, которые требуют параметров (места под них помечены привычным форматным модификатором '%s'), добавим проверку их наличия — вот окончательный вид конструктора DBField.
template<typename T>
|
Благодаря тому, что комбинация макросов и вспомогательных объектов DBEntity<S> и DBField<T> заполняет массив прототипов, в классе DBSQLite появляется возможность реализовать автоматическую генерацию SQL-запроса на создание таблицы структур.
Метод createTable шаблонизирован типом прикладной структуры и содержит заготовку запроса ("CREATE TABLE %s %s (%s);"). Первым аргументом для неё является опциональная инструкция "IF NOT EXISTS", вторым — имя таблицы, которое по умолчанию берется как тип параметра шаблона typename(S), но при необходимости его можно заменить чем-то еще с помощью входного параметра name (если он не равен NULL). Наконец третий аргумент в скобках — это список столбцов таблицы: он формируется вспомогательным методом columns на основе массива DBEntity<S>::prototype.
class DBSQLite
|
Для каждого столбца описание составляется из имени, типа и необязательного ограничения. Дополнительно существует возможность передать общее ограничение на таблицу (table_constraints).
Перед тем как отправить сформированный SQL-запрос в функцию DatabaseExecute, метод createTable производит отладочный вывод текста запроса в журнал (весь такой вывод в классах ORM можно централизованно отключить подменой макроса PRTF).
Теперь все готово для написания тестового скрипта DBcreateTableFromStruct.mq5, который по декларации структуры создал бы соответствующую таблицу в SQLite. Во входном параметре зададим только имя базы, а имя таблицы программа выберет сама по типу структуры.
#include <MQL5Book/DBSQLite.mqh>
|
В главной функции OnStart создаем таблицу вызовом createTable с параметрами по умолчанию. Если не хотим получить признак ошибки при повторных попытках создания, нужно передать true первым параметром (db.createTable<Struct>(true)).
void OnStart()
|
Метод hasTable проверяет наличие таблицы в базе по её (таблицы) имени. Реализацию этого метода мы покажем в следующем разделе, а пока запустим скрипт. После первого запуска создание таблицы пройдет успешно, и в журнале можно увидеть SQL-запрос (он отображается с переводами строк, как мы его формировали в коде).
sql=CREATE TABLE Struct (id INTEGER PRIMARY KEY,
|
Второй запуск вернет ошибку из вызова DatabaseExecute, потому что данная таблица уже существует, о чем дополнительно говорит и результат hasTable.
sql=CREATE TABLE Struct (id INTEGER PRIMARY KEY,
|