English Русский 中文 Deutsch 日本語 Português 한국어 Français Italiano Türkçe
SQL y MQL5: Trabajando con la base de datos SQLite

SQL y MQL5: Trabajando con la base de datos SQLite

MetaTrader 5Ejemplos | 18 abril 2014, 12:54
2 936 0
---
---

Small. Fast. Reliable.
Choose any of three.

 

Introducción

Muchos desarrolladores han pensado sobre la necesidad de usar en sus proyectos bases de datos para el almacenamiento de información, pero cada vez que han querido ponerse a ello, la idea del gasto de tiempo que significa la instalación de un servidor SQL les ha echado para atrás. Si para un programador no es complicado realizar esto (o ya tiene instalado un SGBD (SQL por sus siglas en inglés) con otros fines), para un usuario común esta operación conlleva muchas dudas y cuestiones, o incluso le quita las ganas de instalar un software.

En tal caso, el programador puede, sencillamente, dejar a un lado el SGBD, ya que entiende que, salvo él, son muy pocos los que usan esta opción. Al final se lleva a cabo el trabajo con archivos (con frecuencia más de uno, si los datos son dispares): se usa CSV, con menos frecuencia XML o JSON, o bien se graban datos binarios con un tamaño de estructura muy grande, etc.

¡Pero resulta que existe una alternativa magnífica a los servidores SQL! Además, no es necesario instalar software adicional, todo el trabajo tiene lugar a nivel local en su proyecto, e incluso así, usted estará utilizando toda la potencia del lenguaje SQL. El nombre de esta alternativa es: SQLite.

El objetivo de este artículo es hacer posible que el usuario comience a usar SQLite rápidamente. Por eso, no voy a profundizar en los detalles y conjuntos de parámetros posibles o las banderas de funciones, sino que voy a crear un conector que ejecuta comandos SQL, y mostraré su utilización con ejemplos.

Los requisitos imprescindibles para trabajar con el artículo son:

  • Buen humor ;)
  • Extraiga el archivo de la aplicación que viene con el artículo a una carpeta del terminal de trabajo MetaTrader 5
  • Instale cualquier utility para el visionado de las bases SQLite (por ejemplo, SQLiteStudio)
  • Añada al registro la documentación oficial sobre SQLite http://www.sqlite.org

Contenido

1. Principios de SQLite
2. Trabajar con SQLite API
    2.1. Apertura y cierre de las bases de datos
    2.2. Ejecución de requerimientos SQL
    2.3. Obtención de datos de los recuadros
    2.4. Grabación de los datos paramétricos con unión
    2.5. Transacciones / Multirow insert (ejemplo de creación de un recuadro de operaciones de la cuenta comercial)
3. Montaje de la DLL de 64 bit versión SQLite3


1. Principios de funcionamiento de SQLite

SQLite es un SGBD (Sistema de Gestión de Bases de Datos) cuya principal peculiaridad es la ausencia de un sevidor SQL "oyente". El servidor mismo será su aplicación. Trabajar con las bases de datos en SQLite es, en la práctica, un trabajo con archivos (en un disco o en la memoria). Puede archivar todos los datos o trasladarlos a otro ordenador sin ninguna exigencia en particular en lo que respecta a su instalación.

El uso de SQLite proporciona tanto al desarrollador como al usuario varias ventajas indiscutibles:

  • no hay que instalar software adicional;
  • los datos se almacenan en un archivo local, lo que posibilita la transparencia en su control y gestión, es decir, los datos se pueden ver y redactar independientemente de su aplicación;
  • posibilidad de importar o exportar recuadros a otros SGBD;
  • en el código se usan los requerimientos SQL habituales, lo que permite, en cualquier momento, poner a trabajar la aplicación con otros SGBD.

Se puede trabajar con SQLite de tres maneras:

  1. usando archivos DLL con el conjunto completo de funciones API;
  2. usar los comandos shell de un archivo EXE;
  3. compilar el proyecto, incluyendo en él los códigos fuente SQLite API.

En este artículo trataré el uso de la primera posibilidad, ya que es la más frecuente en MQL5.

 

2. Trabajar con SQLite3 API

En el trabajo con el conector se usarán las siguientes funciones SQLite:

//--- funciones generales
sqlite3_open
sqlite3_prepare
sqlite3_step
sqlite3_finalize
sqlite3_close
sqlite3_exec

//--- funciones para obtener la descripción de un error
sqlite3_errcode
sqlite3_extended_errcode
sqlite3_errmsg

//--- funciones para almacenar datos
sqlite3_bind_null
sqlite3_bind_int
sqlite3_bind_int64
sqlite3_bind_double
sqlite3_bind_text
sqlite3_bind_blob

//--- funciones para obtener datos
sqlite3_column_count
sqlite3_column_name
sqlite3_column_type
sqlite3_column_bytes
sqlite3_column_int
sqlite3_column_int64
sqlite3_column_double
sqlite3_column_text
sqlite3_column_blob

Asímismo, necesitaremos las funciones de bajo nivel msvcrt.dll para trabajar con los índices:

strlen
strcpy
memcpy

Dado que estoy creando un conector que funciona con terminales de 32 y 64 bits, entonces es necesario ocuparse del tamaño del índice enviado en la función API. Dividimos sus nombres:

// para el terminal de 32 bits
#define PTR32                int
#define sqlite3_stmt_p32     PTR32
#define sqlite3_p32          PTR32
#define PTRPTR32             PTR32

// para el terminal de 64 bits
#define PTR64                long
#define sqlite3_stmt_p64     PTR64
#define sqlite3_p64          PTR64
#define PTRPTR64             PTR64

Todas las funciones API se recargan para los índices de 32 y 64 bits, en caso necesario. Quiero llamar la atención sobre el hecho de todos los índices en el conector tendrán un tamaño de 64 bits. Su paso a un tamaño de 32 bits tendrá lugar directamente en las funciones API recargadas. El código fuente de la importación de las funciones API se encuentra en el archivo SQLite3Import.mqh


Tipos de datos SQLite

En la tercera versión de SQLite se contemplan cinco tipos de datos

Tipo
Descripción
NULL Ausencia de datos.
INTEGER Entero de un tamaño de 1, 2, 3, 4, 6 u 8 bytes, dependiendo de la amplitud del valor almacenado.
REAL Con coma flotante, tamaño 8 bytes.
TEXT Línea con el símbolo final \0 en codificación UTF-8 o UTF-16.
BLOB Datos binarios arbitrarios


Para indicar los tipos de campo al crear los recuadros en un requerimiento SQL, se pueden usar igualmente otros nombres de tipos, por ejemplo, BIGINT o INT, que están aceptados en los SGBD. En este caso, SQLite los pasará a algunos de sus tipos internos, en este caso a INTEGER. Podrá leer con más detalle sobre los tipos de datos y sus relaciones en la documentación http://www.sqlite.org/datatype3.html


2.1. Apertura y cierre de las bases de datos

Como ya sabe usted, la base de datos en SQLite3 es un archivo normal. La apertura de una base de datos es, en la práctica, la apertura de de un archivo y la obtención de su handle.

La apertura se lleva a cabo con la ayuda de la función sqlite3_open:

int sqlite3_open(const uchar &filename[], sqlite3_p64 &ppDb);

filename [in]  - ruta al archivo, o bien al nombre del archivo, si el archivo se abre en su ubicación actual.
ppDb     [out] - es la variable donde grabará la dirección del handle del archivo de la base.

Retorna SQLITE_OK en caso de éxito o de código de error.

El archivo de la base se cierra con la función sqlite3_close:

int sqlite3_close(sqlite3_p64 ppDb);

ppDb [in] - handle del archivo

Retorna SQLITE_OK en caso de éxito o de código de error.


Creamos en el conector las funciones de apertura y cierre de la BD.

//+------------------------------------------------------------------+
//| CSQLite3Base class                                               |
//+------------------------------------------------------------------+
class CSQLite3Base
  {
   sqlite3_p64       m_db;             // pointer to database file
   bool              m_bopened;        // flag "Is m_db handle valid"
   string            m_dbfile;         // path to database file

public:
                     CSQLite3Base();   // constructor
   virtual          ~CSQLite3Base();   // destructor


public:
   //--- connection to database 
   bool              IsConnected();
   int               Connect(string dbfile);
   void              Disconnect();
   int               Reconnect();
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CSQLite3Base::CSQLite3Base()
  {
   m_db=NULL;
   m_bopened=false;
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CSQLite3Base::~CSQLite3Base()
  {
   Disconnect();
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool CSQLite3Base::IsConnected()
  {
   return(m_bopened && m_db);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
int CSQLite3Base::Connect(string dbfile)
  {
   if(IsConnected())
      return(SQLITE_OK);
   m_dbfile=dbfile;
   return(Reconnect());
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void CSQLite3Base::Disconnect()
  {
   if(IsConnected())
      ::sqlite3_close(m_db);
   m_db=NULL;
   m_bopened=false;
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
int CSQLite3Base::Reconnect()
  {
   Disconnect();
   uchar file[];
   StringToCharArray(m_dbfile,file);
   int res=::sqlite3_open(file,m_db);
   m_bopened=(res==SQLITE_OK && m_db);
   return(res);
  }

El conector ya no sabe abrir ni cerrar la base de datos. Vamos a comprobar el funcionamiento con un script muy sencillo:

#include <MQH\Lib\SQLite3\SQLite3Base.mqh>

CSQLite3Base sql3; // database connector
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void OnStart()
{   
//--- open database connection
   if(sql3.Connect("SQLite3Test.db3")!=SQLITE_OK)
      return;
//--- close connection
    sql3.Disconnect();
}

Inicie el script en el modo depuración, haga una profunda inspiración y compruebe el funcionamiento de cada línea. Como resultado del trabajo, aparecerá una base de datos en la carpeta de instalación del terminal MetaTrader 5. Ya puede alegrarse de que todo haya salido bien. Pasemos al apartado siguiente.


2.2. Ejecución de requerimientos SQL

Cualquier requerimiento SQL en SQLite3 supera un mínimo de tres etapas:

  1. sqlite3_prepare - es la verificación y obtención de la lista de instrucciones (statement);
  2. sqlite3_step - es la ejecución de dichas instrucciones;
  3. sqlite3_finalize - es la finalización y la limpieza de la memoria.

Este esquema, básicamente, viene bien para crear o eliminar recuadros, o bien para grabar datos no binarios, es decir, para los casos en los que el requerimiento SQL no presupone el retorno de algunos datos, además del informe de su correcta ejecución.

Si el requerimiento presupone que se vayan a recibir datos o grabar datos binarios, entonces, en la segunda etapa tendremos que utilizar las funciones sqlite3_column_хх o sqlite3_bind_хх, respectivamente. En el siguiente apartado veremos detalladamente estas funciones.

Escribimos el método CSQLite3Base::Query para la ejecución de un requerimiento SQL sencillo:

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
int CSQLite3Base::Query(string query)
  {
//--- check connection
   if(!IsConnected())
      if(!Reconnect())
         return(SQLITE_ERROR);
//--- check query string
   if(StringLen(query)<=0)
      return(SQLITE_DONE);
   sqlite3_stmt_p64 stmt=0; // variable for pointer
//--- get pointer
   PTR64 pstmt=::memcpy(stmt,stmt,0);
   uchar str[];
   StringToCharArray(query,str);
//--- prepare statement and check result
   int res=::sqlite3_prepare(m_db,str,-1,pstmt,NULL);
   if(res!=SQLITE_OK)
      return(res);
//--- execute
   res=::sqlite3_step(pstmt);
//--- clean
   ::sqlite3_finalize(pstmt);
//--- return result
   return(res);
  }

Como puede ver, las funciones sqlite3_prepare, sqlite3_step y sqlite3_finalize van una detrás de otra.

Veamos la aplicación de CSQLite3Base::Query en el trabajo con recuadros en SQLite:

// Crear un recuadro (CREATE TABLE)
sql3.Query("CREATE TABLE IF NOT EXISTS `TestQuery` (`ticket` INTEGER, `open_price` DOUBLE, `comment` TEXT)");

Tras ejecutar este comando, en la base de datos aparecerá un recuadro:

// Renombrar columna  (ALTER TABLE  RENAME)
sql3.Query("ALTER TABLE `TestQuery` RENAME TO `Trades`");

// Añadir columna (ALTER TABLE  ADD COLUMN)
sql3.Query("ALTER TABLE `Trades` ADD COLUMN `profit`");

Tras ejecutar estos comandos, tendremos un recuadro con un nombre nuevo y un campo añadido:

// Añadir fila (INSERT INTO)
sql3.Query("INSERT INTO `Trades` VALUES(3, 1.212, 'info', 1)");

// Actualizar fila (UPDATE)
sql3.Query("UPDATE `Trades` SET `open_price`=5.555, `comment`='New price'  WHERE(`ticket`=3)")

Como resultado, tras añadir un fila y realizar cambios, en el recuadro aparecerá la siguiente anotación:

Y, al fin, los comandos siguientes, ejecutados consecutivamente, limpiarán la base de datos.

// Borrar todas las filas del recuadro (DELETE FROM)
sql3.Query("DELETE FROM `Trades`")

// Eliminar recuadro (DROP TABLE)
sql3.Query("DROP TABLE IF EXISTS `Trades`");

// Compresión de la base de datos (VACUUM)
sql3.Query("VACUUM");

Antes de ver el próximo apartado, necesitaremos un método para obtener la descripción de errores. La experiencia me dice que el código de error dice mucho, pero la descripción del error nos mostrará el lugar del texto del requerimiento SQL donde está el error, y esto acelerará su búsqueda y corrección significativamente.

const PTR64 sqlite3_errmsg(sqlite3_p64 db);

db [in] - handle obtenido con la función sqlite3_open

Retorna el índice de la línea con la descripción del error.

En el conector añadimos el método para obtener esta línea desde índice con la ayuda de strcpy y strlen.

//+------------------------------------------------------------------+
//| Error message                                                    |
//+------------------------------------------------------------------+
string CSQLite3Base::ErrorMsg()
  {
   PTR64 pstr=::sqlite3_errmsg(m_db);  // get message string
   int len=::strlen(pstr);             // length of string
   uchar str[];
   ArrayResize(str,len+1);             // prepare buffer
   ::strcpy(str,pstr);                 // read string to buffer
   return(CharArrayToString(str));     // return string
  }


2.3. Obtener datos del recuadro

Como ya recordé al comienzo del apartado 2.2, la lectura de datos se realiza con la ayuda de la función sqlite3_column_хх. De una manera esquemática, se puede representar de la manera siguiente:

  1. sqlite3_prepare
  2. sqlite3_column_count - averiguamos la cantidad de columnas del recuadro obtenido
  3. Por ahora, el resultado del paso es sqlite3_step == SQLITE_ROW
    1. sqlite3_column_хх - leemos la celda de esta línea
  4. sqlite3_finalize

Ya que nos aproximamos al apartado común de lectura y grabación de datos, ha llegado un momento muy adecuado para describir las tres clases de contenedores con cuya ayuda tendrá lugar el cambio. El modelo necesario de datos nos vendrá dictado por el modo de almacenamiento de información en la BD:

Base de datos
|
El recuadro es una matriz de filas.
|
La fila es una matriz de celdas.
|
La celda es un búfer de bytes de longitud al azar.


//+------------------------------------------------------------------+
//| CSQLite3Table class                                              |
//+------------------------------------------------------------------+
class CSQLite3Table
  {

public:
   string            m_colname[]; // column name
   CSQLite3Row       m_data[];    // database rows
//...
  };
//+------------------------------------------------------------------+
//| CSQLite3Row class                                                |
//+------------------------------------------------------------------+
class CSQLite3Row
  {

public:
   CSQLite3Cell      m_data[];
//...
  };
//+------------------------------------------------------------------+
//| CSQLite3Cell class                                               |
//+------------------------------------------------------------------+
class CSQLite3Cell
  {

public:
   enCellType        type;
   CByteImg          buf;
//...
  };

Como puede ver, las conexiones CSQLite3Row y CSQLite3Table son primitivas , se trata de matrices corrientes de datos. La clase de celda CSQLite3Cell también tiene la matriz de datos uchar + el campo Tipo de datos. Como matriz de bytes ejerce la clase CByteImage (por analogía con CFastFile, que usted ya conoce).

Para que sea más cómodo trabajar con el conector y el control de tipo de datos de la celda, he creado la enumeración:

enum enCellType
  {
   CT_UNDEF,
   CT_NULL,
   CT_INT,
   CT_INT64,
   CT_DBL,
   CT_TEXT,
   CT_BLOB,
   CT_LAST
  };

Preste atención al hecho de que añadimos a los cinco tipos principales de SQLite3 el tipo CT_UNDEF para identificar el estado inicial de la celda. El tipo entero INTEGER se divide en dos CT_INT y CT_INT64, de acuerdo con la funciones divididas de manera análoga sqlite3_bind_intXX, sqlite3_column_intXX.

Obtención de datos

Para obtener datos de la celda, crearemos un método que generaliza las funciones del tipo sqlite3_column_хх, y que comprobará el tipo de datos, su tamaño y los grabará en CSQLite3Cell.

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool CSQLite3Base::ReadStatement(sqlite3_stmt_p64 stmt,int column,CSQLite3Cell &cell)
  {
   cell.Clear();
   if(!stmt || column<0)
      return(false);
   int bytes=::sqlite3_column_bytes(stmt,column);
   int type=::sqlite3_column_type(stmt,column);
//---
   if(type==SQLITE_NULL)
      cell.type=CT_NULL;
   else if(type==SQLITE_INTEGER)
     {
      if(bytes<5)
         cell.Set(::sqlite3_column_int(stmt,column));
      else
         cell.Set(::sqlite3_column_int64(stmt,column));
     }
   else if(type==SQLITE_FLOAT)
      cell.Set(::sqlite3_column_double(stmt,column));
   else if(type==SQLITE_TEXT || type==SQLITE_BLOB)
     {
      uchar dst[];
      ArrayResize(dst,bytes);
      PTR64 ptr=0;
      if(type==SQLITE_TEXT)
         ptr=::sqlite3_column_text(stmt,column);
      else
         ptr=::sqlite3_column_blob(stmt,column);
      ::memcpy(dst,ptr,bytes);
      if(type==SQLITE_TEXT)
         cell.Set(CharArrayToString(dst));
      else
         cell.Set(dst);
     }
   return(true);
  }

La función nos ha salido bastante voluminosa, pero, en esencia, lo único que hace es leer los datos de la instrucción actual, almacenándolos en una celda.

Asímismo, recargamos la función CSQLite3Base::Query, añadiéndole como primer parámetro el recuadro contenedor CSQLite3Table para los datos que se reciban.

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
int CSQLite3Base::Query(CSQLite3Table &tbl,string query)
  {
   tbl.Clear();
//--- check connection
   if(!IsConnected())
      if(!Reconnect())
         return(SQLITE_ERROR);
//--- check query string
   if(StringLen(query)<=0)
      return(SQLITE_DONE);
//---
   sqlite3_stmt_p64 stmt=NULL;
   PTR64 pstmt=::memcpy(stmt,stmt,0);
   uchar str[]; StringToCharArray(query,str);
   int res=::sqlite3_prepare(m_db, str, -1, pstmt, NULL); if(res!=SQLITE_OK) return(res);
   int cols=::sqlite3_column_count(pstmt); // get column count
   bool b=true;
   while(::sqlite3_step(pstmt)==SQLITE_ROW) // in loop get row data
     {
      CSQLite3Row row; // row for table
      for(int i=0; i<cols; i++) // add cells to row
        {
         CSQLite3Cell cell;
         if(ReadStatement(pstmt,i,cell)) row.Add(cell); else { b=false; break; }
        }
      tbl.Add(row); // add row to table
      if(!b) break; // if error enabled
     }
// get column name
   for(int i=0; i<cols; i++)
     {
      PTR64 pstr=::sqlite3_column_name(pstmt,i); if(!pstr) { tbl.ColumnName(i,""); continue; }
      int len=::strlen(pstr);
      ArrayResize(str,len+1);
      ::strcpy(str,pstr);
      tbl.ColumnName(i,CharArrayToString(str));
     }
   ::sqlite3_finalize(stmt);  // clean
   return(b?SQLITE_DONE:res); // return result code
  }

Ya tenemos todas las funciones para la obtención de datos. Pasemos a ver sus ejemplos:

// Lectura de datos (SELECT)
CSQLite3Table tbl;
sql3.Query(tbl, "SELECT * FROM `Trades`")

Imprimimos el resultado de este requerimiento en el terminal con el comando Print(TablePrint(tbl)). En el registro veremos lo siguiente (en orden ascendente):

// Ejemplo de cálculo de la información  estadística del recuadro (COUNT, MAX, AVG ...)
sql3.Query(tbl, "SELECT COUNT(*) FROM `Trades` WHERE(`profit`>0)")   
sql3.Query(tbl, "SELECT MAX(`ticket`) FROM `Trades`")
sql3.Query(tbl, "SELECT SUM(`profit`) AS `sumprof`, AVG(`profit`) AS `avgprof` FROM `Trades`")

// Obtener los nombres de todos los recuadros en la base
sql3.Query(tbl, "SELECT `name` FROM `sqlite_master` WHERE `type`='table' ORDER BY `name`;");

Imprimimos el resultado a dicho requerimiento de manera análoga con el comando Print(TablePrint(tbl)). Veremos el siguiente recuadro:

Como podemos ver por los parámetros, los resultados de la ejecución de requerimientos se ubican en la variable tbl, desde la cual se pueden obtener y gestionar dichos resultados de modo sencillo.


2.4. Grabación de los datos paramétricos con unión

Otro tema que debe ver sin falta cualquier principiante es la grabación de información en una base de datos que tenga un formato "incómodo". Por supuesto, estamos hablando sobre datos binarios, que no se pueden transmitir directamente con las instrucciones de texto normales INSERT o UPDATE, ya que con el primer cero la línea se considerará finalizada. O, por ejemplo, hay casos cuando en la propia línea tenemos símbolos con una comilla sola'.

En algunas situaciones la unión tardía es cómoda, cuando tenemos un recuadro muy ancho y grabar todos los campos en una sola línea es complicado y poco fiable, ya que hay muchas posibilidades de que se nos pase o perdamos algo importante. Para esta operación de unión, es necesaria una función de la familia sqlite3_bind_хх.

Para aplicar la unión, es necesario poner una plantilla en lugar de los datos transmitidos. Yo mostraré una de las variantes, que es el signo "?". Es decir, el requerimiento UPDATE tendrá el aspecto siguiente:

UPDATE `Trades` SET `open_price`=?, `comment`=? WHERE(`ticket`=3)


Después, hay que ejecutar dos funciones seguidas: sqlite3_bind_double y sqlite3_bind_text, para que quepan los datos en open_price y comment. En general, el esquema de trabajo con las funciones bind-se podría mostrar bajo el aspecto siguiente:

  1. sqlite3_prepare
  2. Llamamos por orden sqlite3_bind_хх y grabamos los datos necesarios en la instrucción
  3. sqlite3_step
  4. sqlite3_finalize

Según la cantidad de los tipos, sqlite3_bind_xx repiten completamente las funciones de lectura ya vistas, por eso es fácil unirlas en el conector en CSQLite3Base::BindStatement:

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool CSQLite3Base::BindStatement(sqlite3_stmt_p64 stmt,int column,CSQLite3Cell &cell)
  {
   if(!stmt || column<0)
      return(false);
   int bytes=cell.buf.Len();
   enCellType type=cell.type;
//---
   if(type==CT_INT)        return(::sqlite3_bind_int(stmt, column+1, cell.buf.ViewInt())==SQLITE_OK);
   else if(type==CT_INT64) return(::sqlite3_bind_int64(stmt, column+1, cell.buf.ViewInt64())==SQLITE_OK);
   else if(type==CT_DBL)   return(::sqlite3_bind_double(stmt, column+1, cell.buf.ViewDouble())==SQLITE_OK);
   else if(type==CT_TEXT)  return(::sqlite3_bind_text(stmt, column+1, cell.buf.m_data, cell.buf.Len(), SQLITE_STATIC)==SQLITE_OK);
   else if(type==CT_BLOB)  return(::sqlite3_bind_blob(stmt, column+1, cell.buf.m_data, cell.buf.Len(), SQLITE_STATIC)==SQLITE_OK);
   else if(type==CT_NULL)  return(::sqlite3_bind_null(stmt, column+1)==SQLITE_OK);
   else                    return(::sqlite3_bind_null(stmt, column+1)==SQLITE_OK);
  }

La única tarea de este método es grabar en la instrucción el búfer de la celda transmitida.

De manera análoga añadimos el método CQLite3Table::QueryBind, cuyo primer argumento es la línea de los datos a grabar:

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
int CSQLite3Base::QueryBind(CSQLite3Row &row,string query) // UPDATE <table> SET <row>=?, <row2>=?  WHERE (cond)
  {
   if(!IsConnected())
      if(!Reconnect())
         return(SQLITE_ERROR);
//---
   if(StringLen(query)<=0 || ArraySize(row.m_data)<=0)
      return(SQLITE_DONE);
//---
   sqlite3_stmt_p64 stmt=NULL;
   PTR64 pstmt=::memcpy(stmt,stmt,0);
   uchar str[];
   StringToCharArray(query,str);
   int res=::sqlite3_prepare(m_db, str, -1, pstmt, NULL);
   if(res!=SQLITE_OK)
      return(res);
//---
   bool b=true;
   for(int i=0; i<ArraySize(row.m_data); i++)
     {
      if(!BindStatement(pstmt,i,row.m_data[i]))
        {
         b=false;
         break;
        }
     }
   if(b)
      res=::sqlite3_step(pstmt); // executed
   ::sqlite3_finalize(pstmt);    // clean
   return(b?res:SQLITE_ERROR);   // result
  }

Su misión es grabar la línea en los parámetros correspondientes.


2.5. Transacciones / Multirow inserts

Antes de pasar a ver este tema, usted deberá tener conocimientos de otra función SQLite API más. En el apartado anterior traté el tema del procesado en tres etapas de los requerimientos prepare+step+finalize. Una solución alternativa, en algunos casos sencilla, en otros incluso imprescindible, sería la función sqlite3_exec:

int sqlite3_exec(sqlite3_p64 ppDb, const char &sql[], PTR64 callback, PTR64 pvoid, PTRPTR64 errmsg);

ppDb [in] - handle de la base de datos
sql  [in] - requerimiento SQL
Por el momento, no estudiaremos la aplicación a MQL5 de los tres parámetros restantes.

Retorna SQLITE_OK al realizarse correctamente o al obtener un error de archivo.

Su esencia es realizar el requerimiento en una sola llamada, sin crear un construcción en tres etapas.

Añadimos su llamada al conector:

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
int CSQLite3Base::Exec(string query)
  {
   if(!IsConnected())
      if(!Reconnect())
         return(SQLITE_ERROR);
   if(StringLen(query)<=0)
      return(SQLITE_DONE);
   uchar str[];
   StringToCharArray(query,str);
   int res=::sqlite3_exec(m_db,str,NULL,NULL,NULL);
   return(res);
  }

Usar el método obtenido es muy sencillo. Por ejemplo, los comandos de eliminación del recuadro (DROP TABLE) o de compresión de la base (VACUUM) se pueden ejecutar así:

sql3.Exec("DROP TABLE `Trades`");

sql3.Exec("VACUUM");


Transacciones

Ahora presentamos la situación en la que es necesario añadir varios miles de líneas a un recuadro. Si lo introducimos todo en este ciclo:

for (int i=0; i<N; i++)
   sql3.Query("INSERT INTO `Table` VALUES(1, 2, 'text')");

entonces tendremos una ejecución mmmmmmuuuuuuyyyyyyyyyy lenta. Para pegar mil líneas se invertirán más de 10(!) segundos, así que no recomendamos hacer esto en SQLite. La salida a esta situación es el uso de transacciones: todas las instrucciones SQL se introducen en una lista común y después se llevan a cabo en un solo requerimiento.

Para grabar el principio y el final de la transacción se usan operadores SQL:

BEGIN
...
COMMIT

Todo lo que hay en el interior se ejecuta de manera atómica en el último operador COMMIT. Para las situaciones en que haya que interrumpir el ciclo, o que por algún motivo no haya que ejecutar instrucciones ya añadidas, tenemos el operador ROLLBACK.

Como ejemplo añadiremos todas las operaciones de la cuenta en un recuadro.

#include <MQH\Lib\SQLite3\SQLite3Base.mqh>
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void OnStart()
  {
   CSQLite3Base sql3;

//--- open database connection
   if(sql3.Connect("Deals.db3")!=SQLITE_OK) return;
//---
   if(sql3.Query("CREATE TABLE IF NOT EXISTS `Deals` (`ticket` INTEGER PRIMARY KEY, `open_price` DOUBLE, `profit` DOUBLE, `comment` TEXT)")!=SQLITE_DONE)
     {
      Print(sql3.ErrorMsg());
      return;
     }

//--- create transaction
   if(sql3.Exec("BEGIN")!=SQLITE_OK)
     {
      Print(sql3.ErrorMsg());
      return;
     }
   HistorySelect(0,TimeCurrent());
//--- dump all deals from terminal to table 
   for(int i=0; i<HistoryDealsTotal(); i++)
     {
      CSQLite3Row row;
      long ticket=(long)HistoryDealGetTicket(i);
      row.Add(ticket);
      row.Add(HistoryDealGetDouble(ticket, DEAL_PRICE));
      row.Add(HistoryDealGetDouble(ticket, DEAL_PROFIT));
      row.Add(HistoryDealGetString(ticket, DEAL_COMMENT));
      if(sql3.QueryBind(row,"REPLACE INTO `Deals` VALUES("+row.BindStr()+")")!=SQLITE_DONE)
        {
         sql3.Exec("ROLLBACK");
         Print(sql3.ErrorMsg());
         return;
        }
     }
//--- end transaction
   if(sql3.Exec("COMMIT")!=SQLITE_OK)
      return;

//--- get statistical information from table
   CSQLite3Table tbl;
   CSQLite3Cell cell;

   if(sql3.Query(tbl,"SELECT COUNT(*) FROM `Deals` WHERE(`profit`>0)")!=SQLITE_DONE)
     {
      Print(sql3.ErrorMsg());
      return;
     }
   tbl.Cell(0,0,cell);
   Print("Count(*)=",cell.GetInt64());
//---
   if(sql3.Query(tbl,"SELECT SUM(`profit`) AS `sumprof`, AVG(`profit`) AS `avgprof` FROM `Deals`")!=SQLITE_DONE)
     {
      Print(sql3.ErrorMsg());
      return;
     }
   tbl.Cell(0,0,cell);
   Print("SUM(`profit`)=",cell.GetDouble());
   tbl.Cell(0,1,cell);
   Print("AVG(`profit`)=",cell.GetDouble());
  }

Tras ejecutar este script en su cuenta, grabará al instante las operaciones de su cuenta en un recuadro.

Se ha mostrado información estadística en el registro del terminal

Experimente con este script: comente las líneas con BEGIN, ROLLBACK y COMMIT, y si tiene más de cien operaciones en su cuenta, entonces apreciará claramente la diferencia. A propósito, según ciertas pruebas, las transacciones SQLite funcionan más rápido que en MySQL o PostgreSQL.


3. Montaje de la versión de 64 bits sqlite3_64.dll

  1. Descargamos el código fuente de SQLite (amalgamation) y extraemos de él el archivo sqlite3.c.
  2. Descargamos DLL sqlite-dll-win32 y le extraemos el archivo sqlite3.dll.
  3. En la carpeta donde ha sido extraido el archivo dll, ejecutamos en la consola el comando LIB.EXE /DEF:sqlite3.def. Asegúrese de que las rutas hasta el archivo lib.exe están escritas con la variable sistémica PATH, o bien encuéntrela en Visual Studio.
  4. Creamos un proyecto DLL, elegiendo la configuración Release para plataformas de 64 bits.
  5. Añadimos al proyecto el archivo descargado sqlite3.c y el archivo obtenido sqlite3.def. Si en alguna función del archivo def el compilador se "enfada", en este caso, coméntelo, simplemente.
  6. En los ajustes del proyecto, establecemos los siguientes parámetros:
    C/C++ --> General --> Debug Information Format = Program Database (/Zi)
    C/C++ --> Precompiled Headers --> Create/Use Precompiled Header = Not Using Precompiled Headers (/Yu)
  7. Compilamos y obtenemos una dll de 64 bit.


Conclusión

Espero que este artículo se convierta en sus instrucciones de cabecera para la asimilación de SQLite. Es muy posible que lo utilice en sus futuros proyectos. Esta breve panorámica le dará una idea general de la funcionalidad de SQLite, como una solución ideal y fiable para las aplicaciones.

En el artículo hemos echado un vistazo a todos los ejemplos con los que puede usted encontrarse al procesar los datos informáticos. Como deberes para hacer en casa, le propongo crear un recopilador sencillo de ticks, que sea capaz de grabar los ticks en un recuadro para cada símbolo. El código fuente de la biblioteca de clases y los scripts de texto podrá encontrarlos en el anexo al artículo.

¡Le deseo que tenga suerte y beneficios!

Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/862

Archivos adjuntos |
MQL5.zip (790.12 KB)
Fundamentos de programación en MQL5 - Cadenas de caracteres Fundamentos de programación en MQL5 - Cadenas de caracteres
Este artículo se ocupa de todo lo que se puede hacer con las cadenas de caracteres en el lenguaje MQL5. El artículo puede ser interesante en primer lugar para los principiantes que se han puesto a estudiar la programación en MQL5. Mientras que los programadores experimentados tienen una buena oportunidad de generalizar y sistematizar sus conocimientos.
Creación de filtros digitales sin retardo temporal Creación de filtros digitales sin retardo temporal
En el presente artículo se estudia una de las aproximaciones para determinar la utilidad de una señal (tendencia) de flujo de datos. Algunos tests para el filtrado (suavización) de las cotizaciones de la bolsa, bastante útiles, demuestran la posibilidad potencial de crear filtros digitales (indicadores) que no sufran retrasos temporales y no se redibujen en las últimas barras.
Recetas MQL5 - Desarrollo de un indicador multidivisa para el análisis de la divergencia de precios Recetas MQL5 - Desarrollo de un indicador multidivisa para el análisis de la divergencia de precios
En este artículo veremos el desarrollo de un indicador multidivisa para el análisis de la divergencia de precios en un periodo de tiempo determinado. Ya hemos visto muchos momentos importantes en el anterior artículo sobre la programación de indicadores multidivisa: "Desarrollo de un indicador multidivisa de volatilidad en MQL5". Por eso, esta vez sólo nos detendremos en las funciones nuevas, o bien en aquellas funciones que hayan sufrido cambios significativos. Si es la primera vez que se encuentra con el tema de los indicadores multidivisa, entonces le recomendamos que lea en primer lugar el artículo anterior.
Pegado de contrato de futuros en MetaTrader 5 Pegado de contrato de futuros en MetaTrader 5
El análisis técnico de los contratos de futuros (futuros, en lo sucesivo) se ve dificultado por la breve duración de su circulación. En gráficos relativamente cortos resulta difícil llevar a cabo el análisis técnico, por ejemplo, la cantidad de barras en el gráfico diurno de futuros en el índice de la bolsa ucraniana UX-9.13 es de algo más de 100. Por eso al trader le surge la cuestión sobre la construcción de instrumentos sintéticos sobre los futuros. En el artículo veremos el tema del pegado de la historia de los contratos de futuros con diferentes fechas de duración en el terminal MetaTrader 5.