Simplificación de bases de datos en MQL5 (Parte 2): Uso de la metaprogramación para crear entidades
Introducción
En el artículo anterior, dimos los primeros pasos para comprender cómo gestiona MQL5 las bases de datos: creamos tablas, insertamos, actualizamos y eliminamos registros, e incluso exploramos las funciones de transacciones, así como la importación y exportación de datos. Todo esto se hizo de forma directa, escribiendo SQL manualmente y llamando a las funciones nativas que ofrece el lenguaje. Este paso fue fundamental, ya que sentó las bases sobre las que se construirá cualquier capa de abstracción. Pero ahora surge una pregunta inevitable: ¿queremos escribir código SQL cada vez que necesitemos gestionar datos en nuestros robots e indicadores?
Si consideramos un sistema más robusto, la respuesta es no. Trabajar exclusivamente con SQL hace que el código resulte extenso, repetitivo y propenso a errores, sobre todo a medida que la aplicación crece y empieza a gestionar múltiples tablas, relaciones y validaciones. Aquí es donde entra en juego un ORM (mapeo objeto-relacional): una forma de tender un puente entre el mundo orientado a objetos en el que programamos y el mundo relacional en el que se almacenan los datos. El primer paso en esta dirección consiste en crear una forma de representar las tablas de la base de datos directamente como clases en MQL5.
En este segundo artículo, aprenderemos a utilizar una característica del lenguaje que a menudo se subestima, pero que resulta extremadamente potente: #define. Esto nos permitirá automatizar y estandarizar la creación de estructuras, evitando duplicidades y facilitando futuras ampliaciones. Con él, crearemos nuestras primeras entidades (clases que representan tablas) y también un mecanismo para describir los metadatos de las columnas, como el tipo de datos, la clave primaria, el autoincremento, los campos obligatorios y los valores por defecto.
Este enfoque servirá de base para todo lo que venga a continuación: repositorios, constructores de consultas y creación automática de tablas. En otras palabras, estamos empezando a dar forma al ORM de MQL5 tal y como lo habíamos concebido desde el principio.
Qué es #define y cómo funciona en MQL5
En los lenguajes de la familia C (y MQL5 forma parte de este grupo), #define es una herramienta de preprocesamiento, es decir, algo que el compilador interpreta incluso antes de compilar el código final. No genera funciones ni crea variables; sustituye texto de forma inteligente, casi como un sistema de abreviaturas textuales o macros.
En la práctica, esto significa que podemos escribir un patrón una sola vez y reutilizarlo en varios puntos del código, lo que reduce la duplicación y los errores. Es más, podemos convertir #define en una herramienta de metaprogramación, capaz de generar estructuras complejas, como nuestras entidades de base de datos, a partir de definiciones sencillas.
Empezaremos por analizar los diferentes usos, desde los más básicos hasta los más avanzados.
1. El #define más sencillo: alias y sustituciones directas
El caso de uso más habitual es utilizar #define para crear un atajo de texto.
#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); }
En este caso, cada vez que el compilador encuentre «PI», lo sustituirá por el valor 3.14159. No se comprueba ni el tipo ni el contexto; se trata de una mera sustitución de texto. Esto es útil, pero sigue siendo trivial.
2. Parámetros en las macros
Con #define, también podemos crear macros que acepten parámetros.
#define SQUARE(x) (x * x) int OnInit() { Print("Square of 5: ", SQUARE(5)); Print("Square of 10: ", SQUARE(10)); }
Al compilarse, SQUARE(5) se sustituirá literalmente por (5 * 5). Esto nos da una idea de cómo podemos encapsular patrones repetitivos en formas reutilizables.
3. El operador #: convertir argumentos en cadenas de caracteres
Una característica poco explorada de MQL5 es el operador #, que convierte el argumento pasado a la macro en una cadena literal.
#define META(name) Print("Variable name: ", #name) int OnInit() { int value = 42; META(value); }
Tenga en cuenta que no muestra el contenido de la variable, sino su nombre. Este truco resulta extremadamente útil para generar registros o crear metadatos a partir de los propios identificadores de código.
4. El operador ##: concatenación de identificadores
Otra función avanzada es el operador ##, que concatena tokens (fragmentos de código).
#define META(name) Print("Concatenated: ", name##Id) int OnInit() { int userId = 7; META(user); }
El compilador une literalmente «user» con «Id», formando «userId». Esta técnica permite generar de forma dinámica nombres de variables, métodos o constantes, algo que, de otro modo, sería imposible en MQL5.
5. Macros como parámetros de otras macros
Hasta ahora, hemos visto #define como una simple sustitución o con parámetros. Pero aquí es donde la cosa se pone interesante, cuando nos damos cuenta de que las macros también pueden recibir otras macros como argumentos. Esto abre la posibilidad de crear una especie de mecanismo para generar repeticiones de código con el que podemos generar bloques de código complejos a partir de una única definición.
Fíjese en este ejemplo:
// 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; }
¿Qué ocurre en este ejemplo? Vamos paso a paso:
-
Paso 1: Aquí creamos una macro que, por sí sola, no genera nada útil. Simplemente enumera cuatro elementos («Add», «Sub», «Mul» y «Div»), cada uno de ellos asociado al símbolo de una operación matemática. El detalle es que cada línea llama a una macro OP, cuya implementación aún desconocemos. Esto significa que MATH_OPERATIONS funciona como una plantilla, pero aun así necesita recibir una «herramienta» que le indique qué hacer con cada elemento de la lista.
-
Paso 2: Esta macro ya realiza una acción concreta: a partir de un nombre y un símbolo, crea una función que aplica la operación. Por ejemplo, si pasamos (Add, +), se creará la siguiente función:
double Add(double a, double b) { return a + b; }
Y así sucesivamente con el resto (Sub, Mul y Div).
-
Paso 3: Ahora viene la magia: pasamos GENERATE_FUNCTION como parámetro a la lista de operaciones. Esto hace que el compilador expanda cada llamada a OP dentro de MATH_OPERATIONS, sustituyendo OP por GENERATE_FUNCTION. El resultado final será equivalente a escribirlo a mano:
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; }
-
Paso 4: Aquí usamos directamente las funciones creadas por el compilador a partir de la combinación de macros. Para el programador, parece como si siempre hubieran existido funciones llamadas Add, Sub, Mul y Div, pero, en realidad, se generaron automáticamente.
En resumen:
- Una macro puede enumerar elementos (MATH_OPERATIONS).
- Otra macro define cómo debe transformarse cada elemento en código (GENERATE_FUNCTION).
- Combinando ambas, el compilador genera automáticamente un conjunto de funciones (o métodos, propiedades, clases, etc.).
Esta técnica de pasar macros como parámetros a otras macros es una herramienta increíble que utilizaremos para eliminar la repetición, estandarizar estructuras y crear bloques ampliables. Más adelante, aplicaremos la misma lógica a las tablas y columnas de las bases de datos, pero aquí queda claro que la idea no se limita a las bases de datos: podemos utilizar esta técnica en cualquier situación en la que se repitan patrones.
Creación de una clase que represente una tabla (entidad)
Vamos a profundizar en el concepto de «entidad», que es básicamente una clase que refleja una tabla de una base de datos. En otras palabras, si en la base de datos tenemos una tabla llamada «Account» con columnas como «id», «number», «balance» y «owner», entonces en el código tenemos una clase «Account» con propiedades que representan cada una de estas columnas. Esto nos permite manipular los datos como objetos, sin tener que preocuparnos directamente por el SQL en todo momento.
Imaginemos que tenemos la tabla «Account» en la base de datos. Para representarla en MQL5, crearíamos una clase como esta:
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+ "]"); }
Este código funciona, pero presenta dos problemas típicos:
- Repetición: Para cada tabla, tenemos que reescribir manualmente todas las propiedades y constructores. Si el sistema tiene 20 tablas, habrá 20 clases prácticamente idénticas, en las que solo variarán los campos.
- Escalabilidad deficiente: si cambia un campo de una tabla (por ejemplo, al cambiar el nombre de «number» a «account_number»), debemos modificarlo manualmente tanto en la base de datos como en la clase, lo que conlleva el riesgo de que se produzcan inconsistencias.
Aquí es donde entra en juego la metaprogramación con #define. Al utilizar macros para generar automáticamente la entidad, la idea es muy similar a la que vimos en la sección anterior: creamos una lista de columnas como macro y, a partir de ella, generamos automáticamente la clase correspondiente. Vamos a hacerlo paso a paso:
Paso 1: Definición de las columnas mediante una macro#define ACCOUNT_COLUMNS(COLUMN) \ COLUMN(ulong, id, 0) \ COLUMN(double, number, 0.0) \ COLUMN(double, balance, 0.0) \ COLUMN(string, owner, "")
Aquí, ACCOUNT_COLUMNS define todas las columnas de la tabla Account. Tenga en cuenta que cada columna se describe mediante su tipo, nombre y valor por defecto.
El secreto está en el parámetro COLUMN, que se pasará más adelante a otra macro para decidir qué hacer con cada elemento.
Paso 2: Creación de la macro que genera los atributos de la clase#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+", ";
Estas macros son los pilares fundamentales de nuestra entidad. Cada una de ellas toma la terna (tipo, nombre, valor por defecto) definida en la lista de columnas y genera un fragmento de código específico:
- ENTITY_FIELD → crea el atributo de clase.
- Con estos parámetros, ENTITY_FIELD(ulong, id, 0) genera ulong id;
- En otras palabras, solo declara la variable con su tipo.
- ENTITY_DEFAULT → inicializa el atributo con un valor por defecto dentro del constructor.
- Teniendo en cuenta estos parámetros: ENTITY_DEFAULT(double, balance, 0.0) genera balance = 0.0;
- Esto garantiza que todos los objetos de la clase empiecen con valores coherentes.
- ENTITY_TO_STRING → genera una cadena para mostrar los valores de los atributos.
- Con estos parámetros, ENTITY_TO_STRING(string, owner, "") concatena el nombre y el valor a la cadena "_s" mediante la instrucción _s += "owner="+(string)owner+", ";
- De esta forma, podemos crear un método ToString genérico que muestre todos los campos de la clase sin tener que escribir manualmente cada atributo.
#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+"]"); \ } \ };
Esta es la macro que se encarga de ensamblar toda la clase. Analicémosla línea por línea:
-
class name {...};
- Crea una clase con el nombre indicado.
- Ejemplo: ENTITY(Account, ACCOUNT_COLUMNS) → class Account {...};
-
COLUMN(ENTITY_FIELD)
- Aplique la macro ENTITY_FIELD a cada columna de la lista.
- Resultado: declara todos los atributos de la clase.
-
Constructor name(void){COLUMN(ENTITY_DEFAULT)};
- El constructor llama a COLUMN(ENTITY_DEFAULT), lo que significa que inicializa todos los atributos con sus valores por defecto.
-
Destructor ~name(void){};
- Aquí solo hacemos explícito el destructor vacío.
-
Método ToString()
- Crea una cadena _s concatenando name=value de cada campo.
- Utiliza COLUMN(ENTITY_TO_STRING) para aplicar esta lógica a todas las columnas.
- Elimina la coma final con StringSubstr.
- Muestra algo como esto:
Account[id=1, number=12345, balance=500.0, owner=João]
ENTITY(Account, ACCOUNT_COLUMNS)
Esta única línea genera automáticamente la clase equivalente a la que escribimos manualmente al principio. Una vez hecho esto, podemos utilizar la clase «Account» con total normalidad:

La diferencia entre la versión manual y la versión con #define es enorme en cuanto a escalabilidad y facilidad de mantenimiento. Ahora, para crear una nueva entidad, basta con:
- Definir sus columnas en una macro TABLE_COLUMNS.
- Llamar a DEFINE_ENTITY(Table, TABLE_COLUMNS).
Esto elimina las repeticiones, facilita las actualizaciones (si cambia el nombre de una columna, puede modificarlo en la macro) y nos abre nuevas posibilidades para ampliar este concepto, por ejemplo, añadiendo constructores automáticos, métodos de serialización, integración con SQL y mucho más.
Metadatos de las columnas: encapsulación de las propiedades
Hasta ahora, hemos creado entidades que tienen atributos e incluso hemos conseguido mostrar sus valores automáticamente mediante macros. Pero hay un problema: estos atributos siguen sin capacidad descriptiva propia.
Saben cómo almacenar datos, pero no saben cómo describirse a sí mismos. Por ejemplo, no podemos preguntarle a un atributo: «¿Es una clave primaria (PK)?», «¿Admite valores nulos?», «¿Es un campo autoincremental?» o «¿Cuál es el tipo real en la base de datos (INTEGER, TEXT, REAL, etc.)?».
Esta información se denomina «metadatos», es decir, datos sobre los datos. Si queremos que nuestro ORM sea capaz de generar código SQL automáticamente (por ejemplo, para crear tablas o validar estructuras), necesitamos una capa adicional que almacene estas propiedades y permita que la entidad se describa a sí misma.
La idea es crear una clase de metadatos llamada IColumnMetadata. Esta se encargará de almacenar toda la información relativa a una columna:
- Nombre del campo (m_name)
- Tipo lógico en MQL5 (m_type)
- Tipo de base de datos (m_db_type)
- Si admite valores nulos (m_nullable)
- Si es autoincremental (m_auto_increment)
- Si es una clave primaria (m_primary_key)
- Si es único (m_unique)
De esta forma, cada columna de la entidad puede contener una descripción completa, que se utilizará en el futuro para generar automáticamente sentencias CREATE TABLE, comprobar si la base de datos se ajusta a la entidad y establecer correctamente la correspondencia entre los valores de MQL5 y SQL.
Luego creamos una nueva carpeta llamada TickORM dentro de includes y, dentro de ella, una carpeta llamada metadata y un nuevo archivo llamado IColumnMetadata.mqh; la ruta resultante es: <MQL5/Includes/TickORM/metadata/ColumnMetadata.mqh>. Y creamos la siguiente clase:
//+------------------------------------------------------------------+ //| 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; }; //+------------------------------------------------------------------+
Tenga en cuenta que la clase no tiene ninguna inteligencia ni lógica compleja; simplemente almacena los datos de las columnas de la tabla. Este nivel de abstracción puede parecer burocrático al principio, pero es precisamente lo que nos permitirá dar el siguiente paso:
- Una entidad puede exponer sus metadatos.
- El ORM puede analizar estos metadatos y generar automáticamente código SQL para crear la tabla correspondiente en la base de datos.
Sin esta capa, cada vez que quisiéramos crear una tabla, tendríamos que escribir manualmente toda la instrucción CREATE TABLE..., que es precisamente el tipo de repetición que queremos eliminar.
Imaginemos que tenemos una tabla «Trades» con tres columnas:
- id: entero, clave primaria, auto_increment.
- symbol: cadena obligatoria, no puede ser nula.
- volume: número decimal, también obligatorio.
Con nuestra clase de metadatos, podemos describirlas así:
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);
Ahora ya no es un simple atributo: es un objeto que se conoce a sí mismo, que sabe explicar sus reglas y restricciones.
Ahora ya no es un simple atributo: es un objeto que se conoce a sí mismo y puede explicar sus reglas y restricciones.
Creación de la clase ITableMetadata: descripción completa de la entidad
Si antes trabajábamos a nivel de columna, ahora tenemos que dar un paso más y pensar a nivel de la tabla completa. Cada tabla (o entidad) no es solo un conjunto de atributos: también tiene un nombre propio, una clave primaria y un conjunto de metadatos de las columnas.
En otras palabras, necesitamos una estructura capaz de almacenar todos los objetos `IColumnMetadata` de una entidad. Si IColumnMetadata describe un campo, la clase ITableMetadata que vamos a crear describe la entidad completa. Debe responder a otras preguntas como: «¿Cuál es el nombre de la tabla?», «¿Cuál es la clave primaria?», «¿Cuántas columnas tiene?» y «¿Cuáles son las propiedades de cada columna?».
Además, debe ser extensible, lo que significa que cada entidad tendrá su propia versión de metadatos, pero todas podrán seguir la misma «interfaz base».
vamos a crear ahora un nuevo archivo llamado TableMetadata.mqh en el directorio <MQL5/Include/TickORM/metadata/TableMetadata.mqh>. Ya hemos importado 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)); } //+------------------------------------------------------------------+
Tenga en cuenta que ya he añadido un array de objetos IColumnMetadata, en el que cada posición representa una columna de la tabla. También he añadido algunos métodos básicos para manipular el array.
Por último, hemos añadido dos métodos virtuales. Esto significa que cualquier clase derivada puede sobrescribir los métodos `TableName()` y `PrimaryKey()` para describirse a sí misma, ya que solo las clases derivadas conocen el nombre de la tabla. Por eso, hemos creado una implementación base que devuelve 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); } //+------------------------------------------------------------------+
Conectándolo todo
Hasta ahora, hemos dado dos pasos fundamentales: en primer lugar, hemos creado manualmente una clase de entidad; a continuación, hemos visto cómo utilizar la metaprogramación con macros para generar automáticamente estas clases. Pero aún falta una pieza importante: tenemos que centralizar la definición de las entidades y sus columnas en un único lugar. Esta ubicación debe ser capaz de convertir los tipos nativos de MQL5 en tipos SQL y, al mismo tiempo, registrar todos los metadatos de las tablas de forma organizada.
Esta es precisamente la función del archivo TickORM.mqh, que se ubicará en <MQL5/TickORM/TickORM.mqh>. Podemos considerarlo como un puente que conecta el código escrito en MQL5 con el modelo de base de datos relacional. La lógica es sencilla: para cada entidad, creamos una clase derivada de `ITableMetadata` que registra automáticamente todas las columnas de la tabla.
Esto permite que el ORM no solo comprenda la estructura de la tabla, sino también cómo crear, validar y manipular columnas, sin que los desarrolladores tengan que realizar manualmente las mismas conversiones para cada nueva entidad.
Para comprender mejor la utilidad de esto, sigamos un enfoque paso a paso: creemos manualmente una clase de metadatos para la tabla «Account», que tiene cuatro columnas básicas. De momento no vamos a utilizar macros aquí, precisamente para aclarar el trabajo que queremos automatizar:
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); }
Esta implementación nos basta para consultar los metadatos de la tabla. Así es como podemos utilizarla:
//+------------------------------------------------------------------+ //| 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); } //+------------------------------------------------------------------+
La salida de la consola muestra claramente toda la información capturada:
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
Tenga en cuenta que ya disponemos de todos los datos esenciales para la tabla y sus columnas en tiempo de ejecución. Lo que falta ahora es automatizar la creación de esta clase de metadatos mediante #define, para no tener que repetir manualmente toda esta estructura cada vez que se añada una nueva entidad. Este será el siguiente paso.
Automatización de la creación de la clase de metadatos
Cuando empezamos a conectar entidades de MQL5 a la base de datos, surge inmediatamente un problema: los tipos de datos no «hablan» el mismo idioma. En el código MQL5 utilizamos los tipos int, double, string y otros. En la base de datos, los tipos son diferentes: INTEGER, REAL, TEXT, etc.
Si no estableciéramos una regla de traducción clara, habría que definir manualmente la correspondencia de tipos para cada columna, lo cual resultaría laborioso y propenso a errores. Para evitar este trabajo repetitivo, hemos creado un pequeño «diccionario de conversión» utilizando #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
Funciona de forma muy sencilla: si declaramos una columna como int en la entidad, la macro DB_TYPE_FROM_MQL5 la convierte automáticamente en "INTEGER". De este modo se garantiza que cada tipo de MQL5 tenga siempre su tipo correspondiente en la base de datos, sin necesidad de recordar ni repetir manualmente esta correspondencia.
Ahora que ya se han resuelto los tipos, necesitamos una forma de organizar los metadatos de cada tabla. Para ello, creamos dinámicamente una clase denominada name##Metadata (por ejemplo, AccountMetadata). Esta clase hereda de ITableMetadata y tiene dos funciones principales:
- TableName(): devuelve el nombre de la entidad (que se utilizará como nombre de la tabla).
- PrimaryKey(): identifica automáticamente qué columna se ha marcado como clave primaria.
#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); \ } \ };
Por último, para crear una entidad completa (clase + metadatos), utilizamos dos macros: la primera define las columnas y la segunda genera automáticamente la entidad y su clase de metadatos:
#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)
Aquí, en unas pocas líneas, declaramos toda la estructura de la tabla «Account»:
- «id» es la clave primaria (primary = true) y es autoincremental (auto_inc = true),
- number y balance son campos numéricos obligatorios,
- owner es un campo de texto obligatorio.
En otras palabras, a partir de una única definición centralizada, hemos podido crear la clase `Account` y su clase de metadatos `AccountMetadata`, listas para ser utilizadas por el ORM.
Conclusión y próximos pasos
Hemos llegado al final de otra fase en el desarrollo de nuestro ORM. En este artículo hemos recorrido un camino importante:
- Empezamos por comprender mejor cómo funciona #define en MQL5, no solo para constantes sencillas, sino como herramienta de metaprogramación.
- A continuación, pasamos a crear entidades (las clases que representan nuestras tablas) y vimos cómo simplificar su definición mediante macros.
- Hemos enriquecido estas entidades con metadatos de columna, que describen atributos como el tipo, la clave primaria, el autoincremento, la unicidad y la posibilidad de valores nulos.
- Por último, lo centralizamos todo en el archivo TickORM.mqh, vinculando los tipos de MQL5 con SQL y automatizando la generación de clases de metadatos.
Esta base es fundamental: ahora no solo contamos con entidades, sino también con la descripción completa de sus propiedades, y esto será el motor que permitirá al ORM manipular la base de datos de forma inteligente y automática.
En el próximo artículo daremos otro paso decisivo: crearemos la capa de repositorio. Esta capa se encargará de manipular los datos sin necesidad de escribir código SQL manualmente. En su lugar, realizaremos llamadas como `accountRepository.Save(account)` u `ordersRepository.FindById(1)`, y el ORM se encargará del resto.
En otras palabras, si hasta ahora hemos aprendido a describir la estructura de las tablas, en el próximo artículo aprenderemos a manejar los datos de forma clara, organizada y segura.
| Nombre del archivo | Descripción |
|---|---|
| Include/TickORM/metadata/ColumnMetadata.mq5 | Interfaz que representa los datos de la columna |
| Include/TickORM/metadata/TableMetadata.mqh | Interfaz que representa los datos de la tabla |
| Include/TickORM/TickORM.mqh | Archivo principal |
Traducción del inglés realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/en/articles/19594
Advertencia: todos los derechos de estos materiales pertenecen a MetaQuotes Ltd. Queda totalmente prohibido el copiado total o parcial.
Este artículo ha sido escrito por un usuario del sitio web y refleja su punto de vista personal. MetaQuotes Ltd. no se responsabiliza de la exactitud de la información ofrecida, ni de las posibles consecuencias del uso de las soluciones, estrategias o recomendaciones descritas.
Ecuación simbólica de previsión del precio con SymPy
Desarrollo de un kit de herramientas para el análisis de la acción del precio (Parte 41): Creación de un EA estadístico de niveles de precio en MQL5
Desarrollo de un sistema de ruptura basado en la volatilidad
Creación de un sistema de trading profesional con Heikin Ashi (Parte 2): Desarrollo de un EA
- Aplicaciones de trading gratuitas
- 8 000+ señales para copiar
- Noticias económicas para analizar los mercados financieros
Usted acepta la política del sitio web y las condiciones de uso