Знакомство с принципами работы с базой данных в MQL5

Базы данных хранят информацию в виде таблиц. Получение, модификация и добавление новых данных в них делается с помощью запросов на языке SQL. Про его специфику мы расскажем в следующих разделах. А пока продемонстрируем на примере скрипта DatabaseRead.mq5, никак не связанного с трейдингом, как создать простую базу данных и получить из неё информацию. Все упомянутые здесь функции будут подробно описаны позднее. Сейчас важно представить себе общие принципы.

Создание и закрытие базы с помощью встроенных функций DatabaseOpen/DatabaseClose аналогичны работе с файлами — так же создаем дескриптор для базы данных, проверяем его и закрываем в конце.

void OnStart()
{
   string filename = "company.sqlite";
   // создадим или откроем базу данных
   int db = DatabaseOpen(filenameDATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE);
   if(db == INVALID_HANDLE)
   {
      Print("DB: "filename" open failed with code "_LastError);
      return;
   }
   ... // дальнейшая работа с базой данных
   // закрываем базу данных
   DatabaseClose(db);
}

После открытия базы убедимся в отсутствии в ней таблицы под нужным нам именем — если таблица уже существует, то при попытке вставить в неё такие же данные, как в нашем примере, произойдет ошибка, поэтому используем функцию DatabaseTableExists.

Удаление и создание таблицы совершается с помощью запросов, которые отправляются в базу двумя вызовами функции DatabaseExecute и сопровождаются контролем ошибок.

   ...
   // если таблица COMPANY существует, то удалим её
   if(DatabaseTableExists(db"COMPANY"))
   {
      if(!DatabaseExecute(db"DROP TABLE COMPANY"))
      {
         Print("Failed to drop table COMPANY with code "_LastError);
         DatabaseClose(db);
         return;
      }
   }
   // создаем таблицу COMPANY 
   if(!DatabaseExecute(db"CREATE TABLE COMPANY("
     "ID      INT     PRIMARY KEY NOT NULL,"
     "NAME    TEXT    NOT NULL,"
     "AGE     INT     NOT NULL,"
     "ADDRESS CHAR(50),"
     "SALARY  REAL );"))
   {
      Print("DB: "filename" create table failed with code "_LastError);
      DatabaseClose(db);
      return;
   }
   ...

Поясним суть SQL-запросов. В таблице COMPANY у нас всего 5 полей: ID записи, имя, возраст, адрес и зарплата. Причем поле ID является ключом, то есть уникальным индексом. Индексы позволяют однозначно определять каждую запись и могут использоваться в разных таблицах для того чтобы связывать их между собой. Это аналогично тому, как идентификатор позиции связывает между собой все сделки и ордера, которые относятся к конкретной позиции.

Теперь необходимо заполнить таблицу данными, делается это с помощью запроса "INSERT":

   // вставляем данные в таблицу 
   if(!DatabaseExecute(db,
      "INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY) VALUES (1,'Paul',32,'California',25000.00); "
      "INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY) VALUES (2,'Allen',25,'Texas',15000.00); "
      "INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY) VALUES (3,'Teddy',23,'Norway',20000.00);"
      "INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY) VALUES (4,'Mark',25,'Rich-Mond',65000.00);"))
   {
      Print("DB: "filename" insert failed with code "_LastError);
      DatabaseClose(db);
      return;
   }
   ...

Здесь в таблицу COMPANY добавляются 4 записи, для каждой записи указывается список полей и значений, которые будут записаны в эти поля. Записи вставляются отдельными запросами "INSERT...", которые объединены в одну строку, через специальный символ-разделитель ';', но мы могли бы каждую запись вставлять в таблицу отдельным вызовом DatabaseExecute.

Так как по окончании работы скрипта база будет сохранена в файл "company.sqlite", то при последующем его запуске мы попытались бы записать те же самые данные в таблицу COMPANY с таким же ID. Это привело бы к ошибке — именно поэтому мы ранее удалили таблицу, чтобы при каждом запуске скрипта начинать работу с нуля.

Теперь получим все записи из таблицы COMPANY, где поле SALARY > 15000. Делается это с помощью функции DatabasePrepare, которая "компилирует" текст запроса и возвращает его дескриптор для последующего использования в функциях DatabaseRead или DatabaseReadBind.

   // подготовим запрос с дескриптором
   int request = DatabasePrepare(db"SELECT * FROM COMPANY WHERE SALARY>15000");
   if(request == INVALID_HANDLE)
   {
      Print("DB: "filename" request failed with code "_LastError);
      DatabaseClose(db);
      return;
   }
   ...

После того как запрос успешно создан, необходимо получить результаты его выполнения. Это можно сделать с помощью функции DatabaseRead, которая при первом вызове выполнит запрос и перейдет на первую запись в результатах. При каждом последующем вызове она будет считывать следующую запись, пока не дойдет до конца. В этом случае она вернет false, что означает "записей больше нет".

   // распечатаем все записи с зарплатой больше 15000
   int idage;
   string nameaddress;
   double salary;
   Print("Persons with salary > 15000:");
   for(int i = 0DatabaseRead(request); i++)
   {
      // прочитаем значения каждого поля из полученной записи по его номеру
      if(DatabaseColumnInteger(request0id) && DatabaseColumnText(request1name) &&
         DatabaseColumnInteger(request2age) && DatabaseColumnText(request3address) &&
         DatabaseColumnDouble(request4salary))
         Print(i":  "id" "name" "age" "address" "salary);
      else
      {
         Print(i": DatabaseRead() failed with code "_LastError);
         DatabaseFinalize(request);
         DatabaseClose(db);
         return;
      }
   }
   // удалим дескриптор после использования
   DatabaseFinalize(request);

Результатом выполнения будет:

Persons with salary > 15000:
0:  1 Paul 32 California 25000.0
1:  3 Teddy 23 Norway 20000.0
2:  4 Mark 25 Rich-Mond  65000.0

Таким образом, функция DatabaseRead позволяет пройти по всем записям из результата запроса и далее получить полную информацию о каждом столбце в полученной таблице через DatabaseColumn-функции. Эти функции предназначены для универсальной работы с результатами любого запроса, но платой за это становится избыточный код.

Если структура результатов запроса заранее известна, то лучше воспользоваться функцией DatabaseReadBind, которая позволяет считать сразу всю запись целиком в структуру. Мы можем переделать предыдущий пример таким образом и представить под новым названием DatabaseReadBind.mq5. Сначала объявим структуру Person:

struct Person
{
   int    id;
   string name;
   int    age;
   string address;
   double salary;
};

Затем сделаем вычитку каждой записи из результатов запроса с помощью DatabaseReadBind(request, person) в цикле, пока функция возвращает значение true:

   Person person;
   Print("Persons with salary > 15000:");
   for(int i = 0DatabaseReadBind(requestperson); i++)
      Print(i":  "person.id" "person.name" "person.age,
         " "person.address" "person.salary);
   DatabaseFinalize(request);

Таким образом, мы сразу получаем из текущей записи значения всех полей и нам не требуется вычитывать их по отдельности.

Этот вводный пример был взят из статьи SQLite: нативная работа с базами данных на SQL в MQL5, где кроме него рассмотрено несколько вариантов прикладного применения базы данных для трейдеров. В частности, там можно найти восстановление истории позиций из сделок, анализ торгового отчета в разрезе стратегий, рабочих символов или наиболее предпочтительных торговых часов, а также приемы работы с результатами оптимизации.

Для освоения этого материала может потребоваться некоторое минимальное знание языка SQL, поэтому рассмотрим его вкратце в следующих разделах.