Выполнение запросов без привязки к данным MQL5

Некоторые SQL-запросы представляют собой команды, которые достаточно отправить в "движок" как есть, и не требуют ни переменных входных данных, ни получения результатов. Например, если наша MQL-программа должна создать в базе таблицу, индекс или представление с определенной структурой и именем, мы можем прописать её константной строкой с оператором "CREATE ...". Кроме того, такие запросы удобно использовать для пакетной обработки записей или их комбинирования (слияния, вычисления агрегированных показателей, однотипной модификации). То есть одним запросом можно преобразовать данные таблиц целиком или заполнить на их основе другие таблицы. В последующих запросах можно будет анализировать эти результаты.

Во всех этих случаях важно лишь получить подтверждение успешности действия. Запросы такого типа выполняются с помощью функции DatabaseExecute.

bool DatabaseExecute(int database, const string sql)

Функция исполняет запрос в базе данных, указанной дескриптором database. Сам запрос передается в виде готовой строки sql.

Функция возвращает признак успеха (true) или ошибки (false).

Например, мы можем дополнить свой класс DBSQLite таким методом (дескриптор уже имеется внутри объекта).

class DBSQLite
{
   ...
   bool execute(const string sql)
   {
      return DatabaseExecute(handlesql);
   }
};

Тогда скрипт, создающий новую таблицу (а при необходимости, предварительно, и саму базу данных), может выглядеть так (DBcreateTable.mq5).

input string Database = "MQL5Book/DB/Example1";
input string Table = "table1";
   
void OnStart()
{
   DBSQLite db(Database);
   if(db.isOpen())
   {
      PRTF(db.execute(StringFormat("CREATE TABLE %s (msg text)"Table))); // true
   }
}

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

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

database error, table table1 already exists
db.execute(StringFormat(CREATE TABLE %s (msg text),Table))=false / DATABASE_ERROR(5601)

Дело в том, что нельзя повторно создать уже существующую таблицу. Но 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>
struct DBEntity
{
   static string prototype[][3]; // 0 - тип, 1 - имя, 2 - ограничения
   ...
};
   
template<typename T>
static string DBEntity::prototype[][3];

Внутри шаблона — многомерный массив prototype, в который и будем записывать описание полей. Чтобы перехватить тип и имя прикладного поля потребуется объявить внутри DBEntity еще одну шаблонную структуру DBField: на этот раз её параметр T является типом самого поля. В конструкторе мы имеем информацию об этом типе (typename(T)), а также получаем название поля (и опционально, ограничение — о нем чуть ниже) в виде параметров.

template<typename S>
struct DBEntity
{
   ...
   template<typename T>
   struct DBField
   {
      T f;
      DBField(const string nameconst string constraints = "")
      {
         const int n = EXPAND(prototype);
         prototype[n][0] = typename(T);
         prototype[n][1] = name;
         prototype[n][2] = constraints;
      }
   };

Поле f не используется, но оно нужно, потому что структуры не могут быть пустыми.

Допустим, что у нас есть прикладная структура Data (DBmetaProgramming.mq5).

struct Data
{
   long id;
   string name;
   datetime timestamp;
   double income;
};

Мы можем сделать её аналог, унаследованный от DBEntity<DataDB>, но с подмененными полями на основе DBField, идентичными исходному набору.

struct DataDBpublic DBEntity<DataDB>
{
   DB_FIELD(longid);
   DB_FIELD(stringname);
   DB_FIELD(datetimetimestamp);
   DB_FIELD(doubleincome);
proto;

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

Обратите внимание на одномоментное определение переменной proto вместе с декларацией структуры. Это нужно, потому что в шаблонах каждый конкретный параметризованный тип компилируется только в случае, если в исходном коде создается хотя бы один объект такого типа. И для нас важно, чтобы создание этого прото-объекта происходило в самом начале запуска программы, в момент инициализации глобальных переменных.

Под идентификатором DB_FIELD скрывается макрос:

#define DB_FIELD(T,Nstruct T##_##NDBField<T> { T##_##N() : DBField<T>(#N) { } } \
   _##T##_##N;

Вот как он раскрывается для отдельного поля:

   struct Type_NameDBField<Type>
   {
      Type_Name() : DBField<Type>(Name) { }
   } _Type_Name;

Здесь структура также не только определяется, но и сразу же создается: фактически она подменяет собой оригинальное поле.

Поскольку структура DBField содержит единственную переменную f нужного типа, размеры и внутреннее двоичное представление Data и DataDB идентично. В этом легко убедиться, запустив скрипт DBmetaProgramming.mq5.

void OnStart()
{
   PRTF(sizeof(Data));
   PRTF(sizeof(DataDB));
   ArrayPrint(DataDB::prototype);
}

Он выводит в журнал:

DBEntity<Data>::DBField<long>::DBField<long>(const string,const string)
long id
DBEntity<Data>::DBField<string>::DBField<string>(const string,const string)
string name
DBEntity<Data>::DBField<datetime>::DBField<datetime>(const string,const string)
datetime timestamp
DBEntity<Data>::DBField<double>::DBField<double>(const string,const string)
double income
sizeof(Data)=36 / ok
sizeof(DataDB)=36 / ok
            [,0]        [,1]        [,2]
[0,] "long"      "id"        ""         
[1,] "string"    "name"      ""         
[2,] "datetime"  "timestamp" ""         
[3,] "double"    "income"    ""         

Правда, для доступа к полям в 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) \
   struct S##_##T##_##NDBEntity<S>::DBField<T> \
   { \
      S##_##T##_##N() : DBEntity<S>::DBField<T>(#N) {} \
   }; \
   const S##_##T##_##N _##S##_##T##_##N;

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

#define DB_FIELD_C1(S,T,N,C1) \
   struct S##_##T##_##NDBEntity<S>::DBField<T> \
   {
      S##_##T##_##N() : DBEntity<S>::DBField<T>(#NC1) {} \
   }; \
   const S##_##T##_##N _##S##_##T##_##N;
   
#define DB_FIELD_C2(S,T,N,C1,C2) \
   struct S##_##T##_##NDBEntity<S>::DBField<T> \
   { \
      S##_##T##_##N() : DBEntity<S>::DBField<T>(#NC1 + " " + C2) {} \
   }; \
   const S##_##T##_##N _##S##_##T##_##N;

Все три макроса, как и дальнейшие наработки, попадают в заголовочный файл DBSQLite.mqh.

Важно отметить, что данная "самодельная" привязка объектов к таблице востребована только для ввода данных в базу, потому что чтение данных из таблицы в объект реализовано в MQL5 с помощью функции DatabaseReadBind.

Реализацию DBField также усовершенствуем. Напомним, что типы MQL5 не соответствуют один в один классам хранения SQL, в связи с чем нужно выполнить преобразование при заполнении элемента prototype[n][0]. Этим занимается статический метод affinity.

   template<typename T>
   struct DBField
   {
      T f;
      DBField(const string nameconst string constraints = "")
      {
         const int n = EXPAND(prototype);
         prototype[n][0] = affinity(typename(T));
         ...
      }
      
      static string affinity(const string type)
      {
         const static string ints[] =
         {
            "bool""char""short""int""long",
            "uchar""ushort""uint""ulong""datetime",
            "color""enum"
         };
         for(int i = 0i < ArraySize(ints); ++i)
         {
            if(type == ints[i]) return DB_TYPE::INTEGER;
         }
         
         if(type == "float" || type == "double"return DB_TYPE::REAL;
         if(type == "string"return DB_TYPE::TEXT;
         return DB_TYPE::BLOB;
      }
   };

Использованные здесь текстовые константы обобщенных типов SQL вынесены в отдельное пространство имен: потребность в них может возникнуть в разных местах MQL-программ, и следует гарантировать отсутствие конфликтов имен.

namespace DB_TYPE
{
   const string INTEGER = "INTEGER";
   const string REAL = "REAL";
   const string TEXT = "TEXT";
   const string BLOB = "BLOB";
   const string NONE = "NONE";
   const string _NULL = "NULL";
}

Заготовки возможных ограничений также описаны в своей группе для удобства (как подсказки).

namespace DB_CONSTRAINT
{
   const string PRIMARY_KEY = "PRIMARY KEY";
   const string UNIQUE = "UNIQUE";
   const string NOT_NULL = "NOT NULL";
   const string CHECK = "CHECK (%s)"// требует выражения
   const string CURRENT_TIME = "CURRENT_TIME";
   const string CURRENT_DATE = "CURRENT_DATE";
   const string CURRENT_TIMESTAMP = "CURRENT_TIMESTAMP";
   const string AUTOINCREMENT = "AUTOINCREMENT";
   const string DEFAULT = "DEFAULT (%s)"// требует выражения (константы, функции)
}

Поскольку среди ограничений есть такие, которые требуют параметров (места под них помечены привычным форматным модификатором '%s'), добавим проверку их наличия — вот окончательный вид конструктора DBField.

   template<typename T>
   struct DBField
   {
      T f;
      DBField(const string nameconst string constraints = "")
      {
         const int n = EXPAND(prototype);
         prototype[n][0] = affinity(typename(T));
         prototype[n][1] = name;
         if(StringLen(constraints) > 0       // обходим ошибку STRING_SMALL_LEN(5035)
            && StringFind(constraints"%") >= 0)
         {
            Print("Constraint requires an expression (skipped): "constraints);
         }
         else
         {
            prototype[n][2] = constraints;
         }
      }

Благодаря тому, что комбинация макросов и вспомогательных объектов 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
{
   ...
   template<typename S>
   bool createTable(const string name = NULL,
      const bool not_exist = falseconst string table_constraints = ""const
   {
      const static string query = "CREATE TABLE %s %s (%s);";
      const string fields = columns<S>(table_constraints);
      if(fields == NULL)
      {
         Print("Structure '"typename(S), "' with table fields is not initialized");
         SetUserError(4);
         return false;
      }
      // попытка создать уже существующую таблицу даст ошибку,
      // если не использовать IF NOT EXISTS
      const string sql = StringFormat(query,
         (not_exist ? "IF NOT EXISTS" : ""),
         StringLen(name) ? name : typename(S), fields);
      PRTF(sql);
      return DatabaseExecute(handlesql);
   }
      
   template<typename S>
   string columns(const string table_constraints = ""const
   {
      static const string continuation = ",\n";
      string result = "";
      const int n = ArrayRange(DBEntity<S>::prototype0);
      if(!nreturn NULL;
      for(int i = 0i < n; ++i)
      {
         result += StringFormat("%s%s %s %s",
            i > 0 ? continuation : "",
            DBEntity<S>::prototype[i][1], DBEntity<S>::prototype[i][0],
            DBEntity<S>::prototype[i][2]);
      }
      if(StringLen(table_constraints))
      {
         result += continuation + table_constraints;
      }
      return result;
   }
};

Для каждого столбца описание составляется из имени, типа и необязательного ограничения. Дополнительно существует возможность передать общее ограничение на таблицу (table_constraints).

Перед тем как отправить сформированный SQL-запрос в функцию DatabaseExecute, метод createTable производит отладочный вывод текста запроса в журнал (весь такой вывод в классах ORM можно централизованно отключить подменой макроса PRTF).

Теперь все готово для написания тестового скрипта DBcreateTableFromStruct.mq5, который по декларации структуры создал бы соответствующую таблицу в SQLite. Во входном параметре зададим только имя базы, а имя таблицы программа выберет сама по типу структуры.

#include <MQL5Book/DBSQLite.mqh>
   
input string Database = "MQL5Book/DB/Example1";
   
struct Struct
{
   long id;
   string name;
   double income;
   datetime time;
};
   
DB_FIELD_C1(StructlongidDB_CONSTRAINT::PRIMARY_KEY);
DB_FIELD(Structstringname);
DB_FIELD(Structdoubleincome);
DB_FIELD(Structstringtime);

В главной функции OnStart создаем таблицу вызовом createTable с параметрами по умолчанию. Если не хотим получить признак ошибки при повторных попытках создания, нужно передать true первым параметром (db.createTable<Struct>(true)).

void OnStart()
{
   DBSQLite db(Database);
   if(db.isOpen())
   {
      PRTF(db.createTable<Struct>());
      PRTF(db.hasTable(typename(Struct)));
   }
}

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

sql=CREATE TABLE  Struct (id INTEGER PRIMARY KEY,
name TEXT ,
income REAL ,
time TEXT ); / ok
db.createTable<Struct>()=true / ok
db.hasTable(typename(Struct))=true / ok

Второй запуск вернет ошибку из вызова DatabaseExecute, потому что данная таблица уже существует, о чем дополнительно говорит и результат hasTable.

sql=CREATE TABLE  Struct (id INTEGER PRIMARY KEY,
name TEXT ,
income REAL ,
time TEXT ); / ok
database error, table Struct already exists
db.createTable<Struct>()=false / DATABASE_ERROR(5601)
db.hasTable(typename(Struct))=true / ok