English Deutsch 日本語
preview
Упрощение работы с базами данных в MQL5 (Часть 2): Создание сущностей с помощью метапрограммирования

Упрощение работы с базами данных в MQL5 (Часть 2): Создание сущностей с помощью метапрограммирования

MetaTrader 5Примеры |
251 0
joaopedrodev
joaopedrodev

Введение

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

Если мы рассматриваем более надежную систему, то ответ будет отрицательным. Работа исключительно с SQL делает код многословным, повторяющимся и подверженным ошибкам, особенно по мере того, как приложение растет и начинает обрабатывать множество таблиц, связей и проверок. Вот тут-то и появляется ORM (Object-Relational Mapping, объектно-реляционное отображение): способ преодолеть разрыв между объектно-ориентированным миром, в котором мы программируем, и реляционным миром, в котором существуют данные. Первым шагом в этом направлении является создание способа представления таблиц базы данных непосредственно в виде классов в MQL5.

В этой второй статье мы узнаем, как использовать часто недооцениваемую, но чрезвычайно мощную языковую функцию: #define. Это позволит нам автоматизировать и стандартизировать создание структур, избегая дублирования и упрощая дальнейшее расширение. С его помощью мы создадим наши первые сущности (классы, представляющие таблицы), а также механизм для описания метаданных столбцов, таких как тип данных, первичный ключ, автоинкремент, обязательные поля и значения по умолчанию.

Этот подход станет основой для всего последующего: репозиториев, построителей запросов и автоматического создания таблиц. Другими словами, мы начинаем формировать ORM на MQL5, который мы задумывали с самого начала.


Что такое #define и как это работает в MQL5

В языках семейства C (и MQL5 относится к этой группе) директива #define является инструментом препроцессора, то есть чем-то, что компилятор интерпретирует еще до компиляции окончательного кода. Она не генерирует функции и не создает переменные; она разумно заменяет текст, почти как система "быстрого доступа" или макрос.

На практике это означает, что мы можем написать паттерн один раз и повторно использовать его в нескольких местах кода, уменьшая количество дублирования и ошибок. Более того, мы можем преобразовать #define в инструмент метапрограммирования, способный генерировать сложные структуры, такие как сущности нашей базы данных, из простых определений.

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

1. Простейшая #define: псевдонимы и прямые замены

Наиболее распространенный вариант использования — создание текстового ярлыка с помощью директивы #define.

#define PI 3.14159
#define AUTHOR "João Pedro"

//--- Use
int OnInit()
  {
   double area = PI * 2 * 2;
   Print("Code written by", AUTHOR);
   return(INIT_SUCCEEDED);
  }

Здесь, когда компилятор обнаружит значение PI, он заменит его значением 3.14159. Здесь нет проверки типа или контекста; это чистая замена текста. Это полезно, но все же тривиально.

2. Параметры в макросах

С помощью #define мы также можем создавать макросы, которые получают параметры.

#define SQUARE(x) (x * x)

int OnInit()
  {
   Print("Square of 5: ", SQUARE(5));
   Print("Square of 10: ", SQUARE(10));
  }

При компиляции SQUARE(5) будет заменен буквально на (5 * 5). Это дает вам представление о том, как мы можем инкапсулировать повторяющиеся шаблоны в формы многократного использования.

3. # operator: преобразование аргументов в строки

Малоизученной характеристикой MQL5 является # operator, преобразующий аргумент, передаваемый макросу, в строковый литерал.

#define META(name) Print("Variable name: ", #name)

int OnInit()
  {
   int value = 42;
   META(value);
  }

Обратите внимание, что при этом выводится не содержимое переменной, а ее название. Этот трюк чрезвычайно полезен для создания журналов или метаданных из самих идентификаторов кода.

4. ## operator: объединение идентификаторов

Ещё одна расширенная функция — ## operator, который объединяет токены (фрагменты кода).

#define META(name) Print("Concatenated: ", name##Id)

int OnInit()
  {
   int userId = 7;
   META(user);
  }

Компилятор буквально склеивает user и Id , получая userId. Этот метод позволяет динамически генерировать имена переменных, методов или констант, что в противном случае было бы невозможно в MQL5.

5. Макросы как параметры для других макросов

До сих пор мы рассматривали #define как простую замену или с параметрами. Но вот тут-то все и становится интересным, когда мы понимаем, что макросы также могут получать другие макросы в качестве аргументов. Это открывает своего рода "механизм повторения", где мы можем генерировать сложные блоки кода из одного определения.

Посмотрите на этот пример:

// Step 1 - Macro that describes a set of operations
#define MATH_OPERATIONS(OP) \
  OP(Add, +)                \
  OP(Sub, -)                \
  OP(Mul, *)                \
  OP(Div, /)

// Step 2 - Macro that generates functions from the list above
#define GENERATE_FUNCTION(name, symbol) \
  double name(double a, double b) { return a symbol b; }

// Step 3 - Expansion: Creates multiple functions at once
MATH_OPERATIONS(GENERATE_FUNCTION)

// Step 4 - Use
int OnInit()
  {
   Print("2 + 3 = ", Add(2,3));
   Print("10 - 7 = ", Sub(10,7));
   Print("6 * 4 = ", Mul(6,4));
   Print("20 / 5 = ", Div(20,5));
   return INIT_SUCCEEDED;
  }

Что происходит в этом примере? Пойдем поэтапно:

  • Шаг 1: Здесь мы создаем макрос, который сам по себе не генерирует ничего полезного. В нем просто перечислены четыре элемента (Add, Sub, Mul, Div), каждый из которых связан с символом математической операции. Деталь заключается в том, что каждая строка вызывает макрос OP, и мы пока не знаем, как он будет реализован. Это означает, что MATH_OPERATIONS работает как шаблон, но ему все равно нужно получить "инструмент", который подсказывает ему, что делать с каждым элементом в списке.

  • Шаг 2: Этот макрос уже делает нечто конкретное: получив имя и символ, он создает функцию, которая применяет операцию. Например, если мы передадим (Add, +), будет создана следующая функция:

    double Add(double a, double b) { return a + b; }

    И так далее для остальных (Sub, Mul и Div).

  • Шаг 3: А теперь начинается волшебство: мы передаем GENERATE_FUNCTION в качестве параметра списку операций. Это приводит к тому, что компилятор расширяет каждый вызов OP внутри MATH_OPERATIONS, заменяя OP на GENERATE_FUNCTION. В итоге результат будет эквивалентен написанию текста вручную:

    double Add(double a, double b) { return a + b; }
    double Sub(double a, double b) { return a - b; }
    double Mul(double a, double b) { return a * b; }
    double Div(double a, double b) { return a / b; }
  • Шаг 4: Здесь мы напрямую используем функции, созданные компилятором из комбинации макросов. Программисту может показаться, что функции с названиями Add, Sub, Mul и Div существовали всегда, но на самом деле они были встроены автоматически.

Подводя итог:

  • Макрос может выводить список элементов (MATH_OPERATIONS).
  • Другой макрос определяет, как каждый элемент должен быть преобразован в код (GENERATE_FUNCTION).
  • При объединении этих двух вышеуказанных макросов компилятор автоматически генерирует набор функций (или методов, свойств, классов и т.д.).

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


Создание класса, представляющего таблицу (Сущность)

Давайте углубимся в концепцию сущности, которая по сути является классом, отражающим таблицу базы данных. Другими словами, если у нас в базе данных есть таблица под названием Account со столбцами типа id, number, balance и owner, то в коде у нас есть класс Account со свойствами, которые представляют каждый из этих столбцов. Это позволяет нам работать с данными, как с объектами, не беспокоясь постоянно непосредственно об SQL.

Давайте представим, что у нас в базе данных есть таблица Account. Чтобы представить это в MQL5, мы бы создали класс, подобный этому:

class Account
  {
public:
   ulong             id;        // unique identifier
   double            number;    // account number
   double            balance;   // available balance
   string            owner;     // account owner

                     Account(void);
                    ~Account(void);

   //--- Converts the data into a string
   string            ToString();
  };
Account::Account(void)
  {
  }
Account::~Account(void)
  {
  }
string Account::ToString(void)
  {
   return("Account[id="+ (string)id+ ", number="+ (string)number+ ", balance="+ (string)balance+ ", owner="+ (string)owner+ "]");
  }

Этот код работает, но у него есть две классические проблемы:

  1. Повторение: Для каждой таблицы нам нужно вручную переписать все свойства и конструкторы. Если в системе 20 таблиц, то будет 20 почти идентичных классов, различающихся только полями.
  2. Плохая масштабируемость: Если поле таблицы изменяется (например, при переименовании number в account_number), нам нужно вручную изменить его как в базе данных, так и в классе, что может привести к несогласованности.

Вот тут-то и пригодится метапрограммирование с помощью #define. Использование макросов для автоматической генерации сущности очень похоже на то, что мы видели в предыдущем разделе: мы создаем список столбцов в виде макроса и на его основе автоматически генерируем соответствующий класс. Для этого разберем это по шагам:

Шаг 1 — Определение столбцов с помощью макроса
#define ACCOUNT_COLUMNS(COLUMN) \
  COLUMN(ulong,  id,      0)   \
  COLUMN(double, number,  0.0) \
  COLUMN(double, balance, 0.0) \
  COLUMN(string, owner,   "")

Здесь ACCOUNT_COLUMNS определяет все столбцы в таблице Account. Обратите внимание, что каждый столбец описывается типом, именем и значением по умолчанию.

Секрет кроется в параметре COLUMN, который будет передан в другом макросе позже, чтобы определить, что делать с каждым элементом.

Шаг 2 — Создание макроса, генерирующего атрибуты класса
#define ENTITY_FIELD(type, name, default_value) type name;

#define ENTITY_DEFAULT(type, name, default_value) name = default_value;

#define ENTITY_TO_STRING(type, name, default_value) _s += #name+"="+(string)name+", ";

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

  • ENTITY_FIELD → создает атрибут класса.
    • Учитывая эти параметры: ENTITY_FIELD(ulong, id, 0) генерирует идентификатор ulong;
    • Другими словами, он объявляет переменную только с указанием ее типа.
  • ENTITY_DEFAULT → инициализирует атрибут значением по умолчанию внутри конструктора.
    • Учитывая эти параметры: ENTITY_DEFAULT(double, balance, 0.0) генерирует баланс = 0.0;
    • Это гарантирует, что каждый объект в классе изначально имеет согласованные значения.
  • ENTITY_TO_STRING → генерирует строку для отображения значений атрибутов.
    • Учитывая эти параметры: ENTITY_TO_STRING(string, owner, ""), объединяет имя и значение _s += "owner="+(string)owner+", "; со строкой "_s".
    • Таким образом, мы можем создать универсальный метод ToString, который выводит все поля класса без необходимости вручную прописывать каждый атрибут.
Шаг 3 - Создание макроса main для сущности
#define ENTITY(name, COLUMN) \
class name \
  { \
public: \
                     COLUMN(ENTITY_FIELD) \
                     name(void){COLUMN(ENTITY_DEFAULT)}; \
                    ~name(void){}; \
   string            ToString(void) \
     { \
      string _s = ""; \
      COLUMN(ENTITY_TO_STRING) \
      _s = StringSubstr(_s,0,StringLen(_s)-2); \
      return(#name+ "["+_s+"]"); \
     } \
  };

Это макрос, который фактически собирает весь класс. Давайте разберем его построчно:

  1. class name {...};

    • Создаёт класс с заданным именем.
    • Пример: ENTITY(Account, ACCOUNT_COLUMNS) → класс Account {...};
  2. COLUMN(ENTITY_FIELD)

    • Для каждого столбца в списке применим макрос ENTITY_FIELD.
    • Результат: объявляет все атрибуты класса.
  3. Конструктор name(void){COLUMN(ENTITY_DEFAULT)};

    • Конструктор вызывает функцию COLUMN(ENTITY_DEFAULT), что означает инициализацию всех атрибутов их значениями по умолчанию.
  4. Деструктор ~name(void){};

    • Здесь мы лишь явно указываем пустой деструктор.
  5. Метод ToString()

    • Формирует строку _s путем конкатенации name=value из каждого поля.
    • Используется COLUMN(ENTITY_TO_STRING) для применения этой логики ко всем столбцам.
    • Удаляет замыкающую запятую при использовании функции StringSubstr.
    • Выводится подобное сообщение:

    Account[id=1, number=12345, balance=500.0, owner=João]

Шаг 4 - Создание сущности
ENTITY(Account, ACCOUNT_COLUMNS)

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

Разница между ручной версией и версией #define огромна с точки зрения масштабируемости и удобства обслуживания. Теперь для создания новой сущности просто:

  1. Определим её столбцы в макросе TABLE_COLUMNS.
  2. Вызовем DEFINE_ENTITY(Table, TABLE_COLUMNS).

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


Метаданные столбца: инкапсуляция свойств

До сих пор мы создавали сущности с атрибутами и даже умудрялись автоматически выводить их значения с помощью макросов. Но есть проблема: эти атрибуты по-прежнему остаются «немыми».

Они умеют хранить данные, но не умеют описывать самих себя. Например, мы не можем спросить атрибут: «Являешься ли ты первичным ключом (PK)?», «Можешь ли ты принимать значения NULL?», «Является ли это полем с автоинкрементом?», или «Какой фактический тип данных используется в базе данных (INTEGER, TEXT, REAL и т. д.)?»

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

Идея состоит в создании класса метаданных под названием IColumnMetadata. Он будет отвечать за хранение всей информации о столбце:

  • Имя поля (m_name)
  • Логический тип в MQL5 (m_type)
  • Тип базы данных (m_db_type)
  • Может ли он принимать значение NULL (m_nullable)
  • Является ли он auto_increment (m_auto_increment)
  • Является ли он первичным ключом (m_primary_key)
  • Является ли он уникальным (m_unique)

Таким образом, каждый столбец сущности может содержать полное описание, которое в будущем будет использоваться для автоматической генерации операторов CREATE TABLE, проверки соответствия базы данных сущности и корректного сопоставления значений между MQL5 и SQL.

Мы создаём новую папку с именем TickORM внутри папки includes, а внутри неё — папку metadata и новый файл IColumnMetadata.mqh. В итоге получаем следующий путь: <MQL5/Includes/TickORM/metadata/ColumnMetadata.mqh>. И создаём следующий класс:

//+------------------------------------------------------------------+
//| class abstract : IColumnMetadata                                 |
//|                                                                  |
//| [PROPERTY]                                                       |
//| Name        : IColumnMetadata                                    |
//| Heritage    : No heritage                                        |
//| Description : Stores all the information in a column.            |
//|                                                                  |
//+------------------------------------------------------------------+
class IColumnMetadata
  {
private:
   
   //--- Props
   string            m_name;
   string            m_type;
   string            m_db_type;
   bool              m_nullable;
   bool              m_auto_increment;
   bool              m_primary_key;
   bool              m_unique;

public:
                     IColumnMetadata(string name, string type,string db_type,bool nullable,bool auto_increment,bool primary_key,bool unique);
                     IColumnMetadata(void);
                    ~IColumnMetadata(void);

   //--- Get Props
   string            Name(void);
   string            Type(void);
   string            DbType(void);
   bool              Nullable(void);
   bool              AutoIncrement(void);
   bool              PrimaryKey(void);
   bool              Unique(void);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
IColumnMetadata::IColumnMetadata(string name,string type,string db_type,bool nullable,bool auto_increment,bool primary_key,bool unique)
  {
   m_name = name;
   m_type = type;
   m_db_type = db_type;
   m_nullable = nullable;
   m_auto_increment = auto_increment;
   m_primary_key = primary_key;
   m_unique = unique;
  }
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
IColumnMetadata::IColumnMetadata(void)
  {
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
IColumnMetadata::~IColumnMetadata(void)
  {
  }
//+------------------------------------------------------------------+
//| Get name                                                         |
//+------------------------------------------------------------------+
string IColumnMetadata::Name(void)
  {
   return m_name;
  }
//+------------------------------------------------------------------+
//| Get type                                                         |
//+------------------------------------------------------------------+
string IColumnMetadata::Type(void)
  {
   return m_type;
  };
//+------------------------------------------------------------------+
//| Get database type                                                |
//+------------------------------------------------------------------+
string IColumnMetadata::DbType(void)
  {
   return m_db_type;
  };
//+------------------------------------------------------------------+
//| Get is nullable                                                  |
//+------------------------------------------------------------------+
bool IColumnMetadata::Nullable(void)
  {
   return m_nullable;
  };
//+------------------------------------------------------------------+
//| Get is auto increment                                            |
//+------------------------------------------------------------------+
bool IColumnMetadata::AutoIncrement(void)
  {
   return m_auto_increment;
  };
//+------------------------------------------------------------------+
//| Get is primary key                                               |
//+------------------------------------------------------------------+
bool IColumnMetadata::PrimaryKey(void)
  {
   return m_primary_key;
  };
//+------------------------------------------------------------------+
//| Get is unique                                                    |
//+------------------------------------------------------------------+
bool IColumnMetadata::Unique(void)
  {
   return m_unique;
  };
//+------------------------------------------------------------------+

Обратите внимание, что этот класс не обладает каким-либо интеллектом или сложной логикой; он просто хранит данные столбцов таблицы. Такой уровень абстракции поначалу может показаться бюрократическим, но именно он позволит нам перейти на следующий этап:

  • Объект может раскрывать свои метаданные.
  • ORM может сканировать эти метаданные и автоматически генерировать SQL-запросы для создания соответствующей таблицы в базе данных.

Без этого уровня каждый раз, когда нам нужно было бы создать таблицу, нам приходилось бы вручную писать весь оператор CREATE TABLE..., а именно такого рода повторения мы и хотим исключить.

Представьте, что у нас есть таблица Trades с тремя столбцами:

  1. id: целое число, первичный ключ, auto_increment.
  2. символ: обязательная строка, не может иметь значение NULL.
  3. объём: требуется также десятичное число.

С помощью нашего класса метаданных мы можем описать их следующим образом:

IColumnMetadata id("id", "int", "INTEGER", false, true, true, true);
IColumnMetadata symbol("symbol", "string", "TEXT", false, false, false, false);
IColumnMetadata volume("volume", "double", "REAL", false, false, false, false);

Теперь это не просто атрибут: это объект, который знает себя, который знает, как объяснить свои правила и ограничения.

Теперь это не просто атрибут: это объект, который знает свои свойства, способный объяснить свои правила и ограничения.


Создание класса ITableMetadata: полное описание сущности

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

Иными словами, нам нужна структура, которая может хранить все IColumnMetadata сущности. Если IColumnMetadata описывает поле, то создаваемый нами ITableMetadata описывает всю сущность целиком. Необходимо ответить и на другие вопросы, например: «Как называется таблица?», «Каков первичный ключ?», «Сколько столбцов она содержит?», и «Каковы свойства каждого столбца?»

Кроме того, метаданные должны быть расширяемыми, то есть у каждой сущности должна быть своя версия метаданных, но все они должны использовать один и тот же «базовый интерфейс».

Давайте создадим новый файл под названием TableMetadata.mqh в каталоге <MQL5/Include/TickORM/metadata/TableMetadata.mqh>. Мы уже импортировали ColumnMetadata.mqh.

//+------------------------------------------------------------------+
//| Import                                                           |
//+------------------------------------------------------------------+
#include "PropertyMetadata.mqh"
//+------------------------------------------------------------------+
//| class abstract : IEntityMetadata                                 |
//|                                                                  |
//| [PROPERTY]                                                       |
//| Name        : IEntityMetadata                                    |
//| Heritage    : No heritage                                        |
//| Description : Stores all the information in a table.             |
//|                                                                  |
//+------------------------------------------------------------------+
class ITableMetadata
  {
protected:
   IColumnMetadata  *m_properties[];

public:
                     ITableMetadata(void);
                    ~ITableMetadata(void);

   //--- Add new column
   void              AddColumn(IColumnMetadata *column);
   IColumnMetadata   *Column(int index);
   int               ColumnSize(void);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
ITableMetadata::ITableMetadata(void)
  {
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
ITableMetadata::~ITableMetadata(void)
  {
   int size = ArraySize(m_columns);
   for(int i=0;i<size;i++)
     {
      delete m_columns[i];
     }
  }
//+------------------------------------------------------------------+
//| Add new column                                                   |
//+------------------------------------------------------------------+
void ITableMetadata::AddColumn(IColumnMetadata *column)
  {
   int size = ArraySize(m_columns);
   ArrayResize(m_columns,size+1);
   m_columns[size] = column;
  }
//+------------------------------------------------------------------+
//| Get property metadata                                            |
//+------------------------------------------------------------------+
IColumnMetadata *ITableMetadata::Column(int index)
  {
   return(m_columns[index]);
  }
//+------------------------------------------------------------------+
//| Get size columns                                                 |
//+------------------------------------------------------------------+
int ITableMetadata::ColumnSize(void)
  {
   return(ArraySize(m_columns));
  }
//+------------------------------------------------------------------+

Обратите внимание, что я уже добавил массив объекта IColumnMetadata, где каждая позиция в массиве представляет столбец таблицы. Я также добавил несколько базовых методов для работы с массивом.

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

class ITableMetadata
  {
public:
   //--- Virtual methods (will be implemented in the child class)
   virtual string    TableName(void);
   virtual string    PrimaryKey(void);
  };
//+------------------------------------------------------------------+
//| Get table name                                                   |
//+------------------------------------------------------------------+
string ITableMetadata::TableName(void)
  {
   return(NULL);
  }
//+------------------------------------------------------------------+
//| Get is primary key                                               |
//+------------------------------------------------------------------+
string ITableMetadata::PrimaryKey(void)
  {
   return(NULL);
  }
//+------------------------------------------------------------------+


Собираем всё воедино

До настоящего момента мы прошли два основных этапа: сначала мы вручную создали класс сущности; затем рассмотрели, как использовать макро-метапрограммирование для автоматической генерации этих классов. Но по-прежнему отсутствует один важный элемент: нам необходимо централизовать определение сущностей и их столбцов в одном месте. Это место должно уметь преобразовывать нативные типы MQL5 в типы SQL и одновременно организованно записывать все метаданные таблиц.

Именно в этом и заключается роль файла TickORM.mqh, который будет расположен по адресу <MQL5/TickORM/TickORM.mqh>. Это можно рассматривать как мост, соединяющий написанный на MQL5 код, с реляционной моделью базы данных. Логика проста: для каждой сущности мы создаём класс, производный от ITableMetadata, который автоматически записывает все столбцы таблицы.

Это позволяет ORM не только понимать структуру таблицы, но и то, как создавать, проверять и изменять столбцы, не требуя от разработчиков вручную выполнять те же преобразования для каждой новой сущности.

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

class AccountMetadata : public ITableMetadata
  {
public:
                     AccountMetadata(void);
                    ~AccountMetadata(void);
   
   string            TableName();
   string            PrimaryKey(void);
  };
AccountMetadata::AccountMetadata(void)
  {
   this.AddColumn(new IColumnMetadata("id","ulong","INTEGER",false,true,true,true));
   this.AddColumn(new IColumnMetadata("number","ulong","REAL",false,false,false,false));
   this.AddColumn(new IColumnMetadata("balance","ulong","REAL",false,false,false,false));
   this.AddColumn(new IColumnMetadata("owner","ulong","TEXT",false,false,false,false));
  }
AccountMetadata::~AccountMetadata(void)
  {
  }
string AccountMetadata::TableName(void)
  {
   return("Account");
  }
string AccountMetadata::PrimaryKey(void)
  {
   int size = ArraySize(m_columns);
   for(int i=0;i<size;i++)
     {
      if(m_columns[i].PrimaryKey())
        {
         return(m_columns[i].Name());
        }
     }
   return(NULL);
  }

Данная реализация достаточна для того, чтобы мы могли запрашивать метаданные таблицы. Использовать это можно так:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   AccountMetadata metadata;
   int size_cols = metadata.ColumnSize();
   Print("table name: ",metadata.TableName());
   Print("ptimary key: ",metadata.PrimaryKey());
   Print("size columns: ",size_cols);
   for(int i=0;i<size_cols;i++)
     {
      IColumnMetadata *column = metadata.Column(i);
      Print("===");
      Print("Column name: "+column.Name());
      Print("Type: "+column.Type());
      Print("DbType: "+column.DbType());
      Print("Nullable: "+column.Nullable());
      Print("PrimaryKey: "+column.PrimaryKey());
      Print("Unique: "+column.Unique());
     }
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+

На выходе консоли четко отображается вся полученная информация:

table name: Account
ptimary key: id
size columns: 4
===
Column name: id
Type: ulong
DbType: INTEGER
Nullable: false
PrimaryKey: true
Unique: true
===
Column name: number
Type: ulong
DbType: REAL
Nullable: false
PrimaryKey: false
Unique: false
===
Column name: balance
Type: ulong
DbType: REAL
Nullable: false
PrimaryKey: false
Unique: false
===
Column name: owner
Type: ulong
DbType: TEXT
Nullable: false
PrimaryKey: false
Unique: false

Обратите внимание, что все необходимые данные для таблицы и ее столбцов уже доступны во время выполнения программы. Сейчас не хватает автоматизации создания этого класса метаданных с помощью директивы #define, чтобы нам не приходилось вручную повторять всю эту структуру для каждой новой сущности. Это будет следующим этапом.


Автоматизация создания класса метаданных

Когда мы начинаем подключать сущности MQL5 к базе данных, сразу же возникает проблема: типы данных не говорят на одном языке. В коде MQL5 мы используем int, double, string и другие типы данных. В базе данных типы различаются: INTEGER, REAL, TEXT и т. д.

Если бы мы не разработали четкое правило перевода, каждый столбец пришлось бы сопоставлять вручную, что было бы трудоемко и чревато ошибками. Чтобы избежать этой доработки, мы создали небольшой «словарь преобразований», используя #define:

#define MQL5_TO_SQL_int        "INTEGER"
#define MQL5_TO_SQL_double     "REAL"
#define MQL5_TO_SQL_float      "REAL"
#define MQL5_TO_SQL_long       "INTEGER"
#define MQL5_TO_SQL_ulong      "INTEGER"
#define MQL5_TO_SQL_datetime   "INTEGER"
#define MQL5_TO_SQL_string     "TEXT"
#define MQL5_TO_SQL_bool       "INTEGER" 
#define DB_TYPE_FROM_MQL5(type) MQL5_TO_SQL_##type

Это работает просто: если мы объявляем столбец как int в сущности, макрос DB_TYPE_FROM_MQL5 автоматически преобразует его в "INTEGER". Это гарантирует, что каждый тип MQL5 всегда будет иметь свой соответствующий тип в базе данных, без необходимости запоминать или вручную повторять это сопоставление.

Теперь, когда типы определены, нам нужен способ организации метаданных для каждой таблицы. Для этого мы динамически создаём класс, который называется name##Metadata (например, AccountMetadata). Этот класс наследует от ITableMetadata и имеет две основные функции:

  • TableName(): возвращает имя сущности (которое будет использоваться в качестве имени таблицы).
  • PrimaryKey(): автоматически определяет, какой столбец помечен как первичный ключ.
#define ENTITY_META_DATA(name, COLUMNS) \
class name##Metadata : public ITableMetadata \
  { \
public: \
                     name##Metadata(void) \
     { \
      COLUMNS(ENTITY_META_DATA_COLUMNS); \
     } \
                    ~name##Metadata(void){}; \
   string            TableName() { return(#name); }; \
   string            PrimaryKey(void) \
     { \
      int size = ArraySize(m_columns); \
      for(int i=0;i<size;i++) \
        { \
         if(m_columns[i].PrimaryKey()) \
           { \
            return(m_columns[i].Name()); \
           } \
        } \
      return(NULL); \
     } \
  };

Наконец, для создания полной сущности (класс + метаданные) мы используем два макроса: первый определяет столбцы, а второй автоматически генерирует сущность и ее класс метаданных:

#define ACCOUNT_COLUMNS(COLUMN) \
  COLUMN(ulong,  id,      false, 0, true,  true,  false) \
  COLUMN(double, number,  false, 0, false, false, false) \
  COLUMN(double, balance, false, 0, false, false, false) \
  COLUMN(string, owner,   false,"", false, false, false)

ENTITY(Account, COLUMNS)
ENTITY_META_DATA(Account, COLUMNS)

Здесь, всего в нескольких строках, мы объявляем всю структуру таблицы Account:

  • id является первичным ключом (primary = true) и имеет автоинкремент (auto_inc = true),
  • number и balance являются необходимыми числами,
  • owner - это необходимый текст.

Иными словами, имея единое место описания, мы смогли создать класс Account и его класс метаданных AccountMetadata, готовые к использованию ORM.


Заключение и следующие шаги

Мы завершили очередной этап создания нашего ORM. В этой статье мы прошли важный путь:

  • Мы начали с того, что лучше поняли, как работает #define в MQL5, не только для простых констант, но и как инструмент метапрограммирования.
  • Мы перешли к созданию сущностей (классов, представляющих наши таблицы) и рассмотрели, как упростить их определение с помощью макросов.
  • Мы дополнили эти сущности метаданными столбцов, описывающими такие атрибуты, как тип, первичный ключ, автоинкремент, уникальность и возможность присвоения значения NULL.
  • Наконец, мы объединили все данные в файле TickORM.mqh, связав типы MQL5 с SQL и автоматизировав генерацию классов метаданных.

Эта основа имеет решающее значение: теперь у нас есть не только сущности, но и полное описание их свойств. Это станет тем механизмом, который позволит ORM интеллектуально и автоматически работать с базами данных.

В следующей статье мы сделаем еще один решающий шаг: мы создадим уровень Repository (репозиторий). Этот уровень будет отвечать за работу с данными без необходимости вручную писать SQL-запросы. Вместо этого мы будем вызывать функции типа accountRepository.Save(account) или ordersRepository.FindById(1), а ORM позаботится обо всем остальном.

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

Название файла Описание
Include/TickORM/metadata/ColumnMetadata.mq5
Интерфейс, представляющий данные столбцов
Include/TickORM/metadata/TableMetadata.mqh Интерфейс, представляющий табличные данные
Include/TickORM/TickORM.mqh
Основной файл

Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/19594

Прикрепленные файлы |
TickORM.zip (3.02 KB)
Статистический арбитраж на основе коинтегрированных акций (Часть 6): Система оценки Статистический арбитраж на основе коинтегрированных акций (Часть 6): Система оценки
В данной статье мы предлагаем систему оценки стратегий возврата к среднему значению, основанную на статистическом арбитраже коинтегрированных акций. В статье предлагаются критерии, которые варьируются от ликвидности и транзакционных издержек до количества рангов коинтеграции и времени возврата к среднему значению, при этом учитываются стратегические критерии — частота данных (временной интервал) и период обратного обзора для тестов на коинтеграцию, которые оцениваются до того, как будет сформирован итоговый оценочный балл (rank_score). Предоставляются файлы, необходимые для воспроизведения бэктеста, а также приводятся комментарии к его результатам.
Моделирование рынка (Часть 23): Первые шаги на SQL (VI) Моделирование рынка (Часть 23): Первые шаги на SQL (VI)
В этой статье мы рассмотрим, как выполнить визуализацию и, следовательно, поймем, как структурирована база данных. Это было сделано с помощью анализа внутренней структуры базы данных. Хотя подобные вещи могут показаться излишними, они вполне оправданы, если мы действительно намерены стать администраторами баз данных. Да, есть люди, которые зарабатывают на жизнь, поддерживая и создавая базы данных.
Создание самооптимизирующихся советников на MQL5 (Часть 16): Идентификация линейных систем на основе обучения с учителем Создание самооптимизирующихся советников на MQL5 (Часть 16): Идентификация линейных систем на основе обучения с учителем
Идентификация линейной системы может быть объединена с процессом обучения корректировке ошибки в алгоритме обучения с учителем. Это позволяет нам создавать приложения, основанные на методах статистического моделирования, не наследуя при этом уязвимость, связанную с ограничительными допущениями модели. Классические алгоритмы обучения с учителем имеют ряд ограничений, которые можно устранить, объединив эти модели с регулятором обратной связи, способным корректировать модель с учетом текущей рыночной конъюнктуры.
Создание прибыльной торговой системы (Часть 2): Тонкости управления размером позиции Создание прибыльной торговой системы (Часть 2): Тонкости управления размером позиции
Даже при использовании системы с положительными ожиданиями, на успех или неудачу может повлиять размер позиции. Это ключевой аспект управления рисками — преобразование статистических преимуществ в реальные результаты при одновременной защите вашего капитала.