English Русский 中文 Español Deutsch 日本語 Português 한국어 Français Türkçe
SQL e MQL5: Lavorare con il database SQLite

SQL e MQL5: Lavorare con il database SQLite

MetaTrader 5Esempi | 12 gennaio 2022, 10:08
208 0
---
---

Piccolo Veloce. Affidabile.
Scegli uno dei tre.

 

Introduzione

Molti sviluppatori considerano l'utilizzo di database nei loro progetti per scopi di archiviazione dei dati e tuttavia rimangono titubanti piuttosto titubanti, sapendo quanto tempo extra potrebbe richiedere l'installazione del server SQL. E mentre potrebbe non essere così difficile per i programmatori (se un sistema di gestione di database (DBMS) è già stato installato per altri scopi), sarà sicuramente un problema per un utente comune che potrebbe eventualmente essere scoraggiato a installare il software.

Così tanti sviluppatori scelgono di non occuparsi di DBMS rendendosi conto che le soluzioni su cui stanno attualmente lavorando verranno utilizzate da pochissime persone. Di conseguenza, si rivolgono a lavorare con i file (spesso avendo a che fare con più di un file, data la varietà di dati utilizzati): CSV, meno spesso XML o JSON o file di dati binari con dimensioni della struttura rigorose, ecc.

Tuttavia, si scopre che esiste un'ottima alternativa al server SQL! E non hai nemmeno bisogno di installare software aggiuntivo poiché tutto viene fatto localmente nel tuo progetto, pur consentendoti di utilizzare tutta la potenza di SQL. Stiamo parlando di SQLite.

Lo scopo di questo articolo è iniziare rapidamente con SQLite. Pertanto non entrerò nelle sottigliezze e in tutti i set di parametri e flag di funzione immaginabili, ma creerò invece un wrapper di connessione leggero per eseguire comandi SQL e ne dimostrerò l'uso.

Per procedere con l'articolo, è necessario:

  • Sii di buon umore ;)
  • Estrai i file di archivio allegati all'articolo nella cartella del terminale del cliente MetaTrader 5
  • Installa qualsiasi visualizzatore SQLite conveniente (ad esempio SQLiteStudio)
  • Aggiungi la documentazione ufficiale su SQLite http://www.sqlite.org ai preferiti

Contenuti

1. Principi SQLite
2. SQLite3 API
    2.1. Apertura e chiusura di un database
    2.2. Esecuzione di query SQL
    2.3. Ottenere dati dalle tabelle
    2.4. Scrittura dei dati dei parametri con l'associazione
    2.5. Transazioni/Inserti multiriga (esempio di creazione della tabella delle operazioni di un conto di trading)
3. Compilazione della versione a 64 bit (sqlite3_64.dll)


1. Principi SQLite

SQLite è un RDBMS la cui caratteristica fondamentale è l'assenza di un server SQL installato localmente. La tua applicazione è vista qui come un server. Lavorare con il database SQLite è fondamentalmente lavorare con un file (su un'unità disco o in memoria). Tutti i dati possono essere archiviati o spostati su un altro computer senza bisogno di installarli in modo specifico.

Con SQLite, sviluppatori e utenti possono beneficiare di numerosi innegabili vantaggi:

  • non è necessario installare software aggiuntivo;
  • i dati sono memorizzati in un file locale, offrendo così trasparenza di gestione, ovvero puoi visualizzarli e modificarli, indipendentemente dalla tua applicazione;
  • possibilità di importare ed esportare tabelle su altri DBMS;
  • il codice utilizza query SQL familiari, che consentono di forzare l'applicazione a funzionare con altri DBMS in qualsiasi momento.

Esistono tre modi per lavorare con SQLite:

  1. puoi utilizzare il file DLL con un set completo di funzioni API;
  2. puoi usare i comandi della shell su un file EXE;
  3. puoi compilare il tuo progetto includendo i codici sorgente dell'API SQLite.

In questo articolo, descriverò la prima opzione, essendo la più consueta in MQL5.

 

2. SQLite3 API

L'operazione del connettore richiederà l'uso delle seguenti funzioni SQLite:

//--- general functions
sqlite3_open
sqlite3_prepare
sqlite3_step
sqlite3_finalize
sqlite3_close
sqlite3_exec

//--- functions for getting error descriptions
sqlite3_errcode
sqlite3_extended_errcode
sqlite3_errmsg

//--- functions for saving data
sqlite3_bind_null
sqlite3_bind_int
sqlite3_bind_int64
sqlite3_bind_double
sqlite3_bind_text
sqlite3_bind_blob

//--- functions for getting data
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

Avrai anche bisogno di funzioni msvcrt.dll di basso livello per lavorare con i puntatori:

strlen
strcpy
memcpy

Poiché sto creando un connettore che dovrebbe funzionare in terminali a 32 e 64 bit, è importante considerare la dimensione del puntatore inviato alle funzioni API. Separiamo i loro nomi:

// for a 32 bit terminal
#define PTR32                int
#define sqlite3_stmt_p32     PTR32
#define sqlite3_p32          PTR32
#define PTRPTR32             PTR32

// for a 64 bit terminal
#define PTR64                long
#define sqlite3_stmt_p64     PTR64
#define sqlite3_p64          PTR64
#define PTRPTR64             PTR64

Se necessario, tutte le funzioni API verranno sovraccaricate per i puntatori a 32 e 64 bit. Si prega di notare che tutti i puntatori del connettore saranno a 64 bit. Verranno convertiti a 32 bit direttamente nelle funzioni API sovraccaricate. Il codice sorgente di importazione della funzione API è fornito in SQLite3Import.mqh


Tipi di dati SQLite

Ci sono cinque tipi di dati in SQLite versione 3

Tipo
Descrizione
NULL valore NULLO.
INTEGER Intero memorizzato in 1, 2, 3, 4, 6 o 8 byte a seconda della grandezza del valore memorizzato.
REAL Numero reale a 8 byte.
TESTO Stringa di testo con il carattere finale \0 memorizzata utilizzando la codifica UTF-8 o UTF-16.
BLOB Dati binari arbitrari


È inoltre possibile utilizzare altri nomi di tipo, ad esempio BIGINT o INT accettati in vari DBMS per specificare il tipo di dati di un campo durante la creazione di una tabella da una query SQL. In questo caso, SQLite li convertirà in uno dei suoi tipi intrinseci, in questo caso in INTEGER. Per ulteriori informazioni sui tipi di dati e sulle loro relazioni, leggere la documentazione http://www.sqlite.org/datatype3.html


2.1. Apertura e chiusura di un database

Come già sai, un database in SQLite3 è un file normale. Quindi aprire un database è in effetti uguale ad aprire un file e ottenere il suo handle.

Viene eseguito utilizzando la funzione sqlite3_open:

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

filename [in]  - a pathname or file name if the file is being opened at the current location.
ppDb     [out] - variable that will store the file handle address.

The function returns SQLITE_OK in case of success or else an error code.

Un file di database viene chiuso utilizzando la funzione sqlite3_close:

int sqlite3_close(sqlite3_p64 ppDb);

ppDb [in] - file handle

The function returns SQLITE_OK in case of success or else an error code.


Creiamo le funzioni di apertura e chiusura del database nel connettore.

//+------------------------------------------------------------------+
//| 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);
  }

Il connettore può ora aprire e chiudere un database. Ora controlla le sue prestazioni con un semplice script:

#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();
}

Esegui lo script in modalità debug, fai un respiro profondo e controlla il funzionamento di ogni stringa. Di conseguenza, un file di database apparirà nella cartella di installazione del terminale MetaTrader 5. Congratulazioni con te stesso per questo successo e passa alla sezione successiva.


2.2. Esecuzione di query SQL

Qualsiasi query SQL in SQLite3 deve passare attraverso almeno tre fasi:

  1. sqlite3_prepare - verifica e ricezione dell'elenco delle dichiarazioni;
  2. sqlite3_step - eseguire queste istruzioni;
  3. sqlite3_finalize - finalizzazione e cancellazione della memoria.

Questa struttura è adatta principalmente alla creazione o cancellazione di tabelle, nonché alla scrittura di dati non binari, ovvero nelle caselle in cui una query SQL non implichi la restituzione di alcun dato se non per lo stato di successo dell'esecuzione.

Se la query prevede la ricezione di dati o la scrittura di dati binari, nella seconda fase viene utilizzata rispettivamente la funzione sqlite3_column_хх o sqlite3_bind_хх. Queste funzioni sono descritte in dettaglio nella sezione successiva.

Scriviamo il metodo CSQLite3Base::Query per eseguire una semplice query SQL:

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
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);
  }

Come puoi vedere, sqlite3_prepare, sqlite3_step e sqlite3_finalize si susseguono una dopo l'altra.

Considerare l'esecuzione di CSQLite3Base::Query quando si lavora con tabelle in SQLite:

// Create the table (CREATE TABLE)
sql3.Query("CREATE TABLE IF NOT EXISTS `TestQuery` (`ticket` INTEGER, `open_price` DOUBLE, `comment` TEXT)");

Dopo aver eseguito questo comando, nel database appare la tabella:

// Rename the table  (ALTER TABLE  RENAME)
sql3.Query("ALTER TABLE `TestQuery` RENAME TO `Trades`");

// Add the column (ALTER TABLE  ADD COLUMN)
sql3.Query("ALTER TABLE `Trades` ADD COLUMN `profit`");

Dopo aver eseguito questi comandi, riceviamo la tabella con un nuovo nome e un campo aggiuntivo:

// Add the row (INSERT INTO)
sql3.Query("INSERT INTO `Trades` VALUES(3, 1.212, 'info', 1)");

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

La seguente voce viene visualizzata nella tabella dopo l'aggiunta e la modifica della nuova riga:

Infine, i seguenti comandi dovrebbero essere eseguiti uno dopo l'altro per ripulire il database.

// Delete all rows from the table (DELETE FROM)
sql3.Query("DELETE FROM `Trades`")

// Delete the table (DROP TABLE)
sql3.Query("DROP TABLE IF EXISTS `Trades`");

// Compact database (VACUUM)
sql3.Query("VACUUM");

Prima di passare alla sezione successiva, abbiamo bisogno del metodo che riceve una descrizione dell'errore. Per esperienza personale posso dire che il codice di errore può fornire molte informazioni, ma la descrizione dell'errore mostra il punto nel testo della query SQL in cui è apparso un errore semplificandone il rilevamento e la correzione.

const PTR64 sqlite3_errmsg(sqlite3_p64 db);

db [in] - handle received by function sqlite3_open

The pointer is returned to the string containing the error description.

Nel connettore, dovremmo aggiungere il metodo per ricevere questa stringa dal puntatore usando strcpy e 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. Ottenere dati dalle tabelle

Come ho già detto all'inizio della sezione 2.2, la lettura dei dati viene eseguita utilizzando le funzioni sqlite3_column_хх. Questo può essere schematicamente mostrato come segue:

  1. sqlite3_prepare
  2. sqlite3_column_count - scopri il numero di colonne della tabella ottenuta
  3. Mentre il risultato del passaggio corrente sqlite3_step == SQLITE_ROW
    1. sqlite3_column_хх - leggi le celle della stringa
  4. sqlite3_finalize

Poiché ci stiamo avvicinando a un'ampia sezione relativa alla lettura e alla scrittura dei dati, è un buon momento per descrivere tre classi di contenitori utilizzate nell'intero scambio di dati. Il modello di dati necessario dipende da come i dati vengono archiviati nel database:

Database
|
La tabella è un array di righe.
|
La riga è un array di celle.
|
Cell è un buffer di byte di lunghezza arbitraria.


//+------------------------------------------------------------------+
//| 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;
//...
  };

Come puoi vedere, le connessioni CSQLite3Row e CSQLite3Table sono primitive: si tratta di array di dati convenzionali. La classe di celle CSQLite3Cell ha anche l'array di dati uchar + il campo Tipo di dati. L'array di byte è implementato nella classe CByteImage (simile al noto CFastFile).

Ho creato la seguente enumerazione per facilitare il funzionamento del connettore e gestire i tipi di dati delle celle:

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

Si noti che il tipo CT_UNDEF è stato aggiunto a cinque tipi SQLite3 di base per identificare lo stato iniziale della cella. L'intero tipo INTEGER diviso in CT_INT e CT_INT64 secondo le funzioni sqlite3_bind_intXX e sqlite3_column_intXX divise in modo simile.

Ottenere dati

Per ottenere i dati dalla cella, dovremmo creare il metodo che generalizza le funzioni di tipo sqlite3_column_хх. Controllerà il tipo e la dimensione dei dati e li scriverà su 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 funzione è piuttosto grande, ma legge solo i dati dall'istruzione corrente e li memorizza in una cella.

Dovremmo anche sovraccaricare la funzione CSQLite3Base::Query aggiungendo la tabella contenitore CSQLite3Table per i dati ricevuti come primo parametro.

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
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
  }

Abbiamo tutte le funzioni necessarie per la ricezione dei dati. Passiamo ai loro esempi:

// Read data (SELECT)
CSQLite3Table tbl;
sql3.Query(tbl, "SELECT * FROM `Trades`")

Stampa il risultato della query nel terminale usando il seguente comando Print(TablePrint(tbl)). Vedremo le seguenti voci nel journal (l'ordine è dal basso verso l'alto):

// Sample calculation of stat. data from the tables (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`")

// Get the names of all tables in the base
sql3.Query(tbl, "SELECT `name` FROM `sqlite_master` WHERE `type`='table' ORDER BY `name`;");

Il risultato della query viene stampato allo stesso modo utilizzando Print(TablePrint(tbl)). Possiamo vedere la tabella esistente:

Come si può vedere dagli esempi, i risultati dell'esecuzione della query vengono inseriti nella variabile tbl. Successivamente, puoi facilmente ottenerli ed elaborarli a tua discrezione.


2.4. Scrittura dei dati dei parametri con l'associazione

Un altro argomento che può essere importante per i nuovi arrivati è scrivere dati nel database con un formato "scomodo". Ovviamente qui intendiamo dati binari. Non può essere passato direttamente in un'istruzione di testo INSERT o UPDATE comune, poiché una stringa è considerata completa quando viene incontrato il primo zero. Lo stesso problema si verifica quando la stringa stessa contiene virgolette singole '.

La rilegatura tardiva può essere utile in alcune caselle, soprattutto quando la tabella è ampia. Sarebbe difficile e inaffidabile scrivere tutti i campi su una singola riga, poiché potresti facilmente perdere qualcosa. Le funzioni della serie sqlite3_bind_хх sono necessarie per l'operazione di associazione.

Per applicare l'associazione, è necessario inserire un modello anziché i dati passati. Prenderò in considerazione una delle caselle - "?" cartello. In altre parole, la query UPDATE avrà il seguente aspetto:

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


Quindi, le funzioni sqlite3_bind_double e sqlite3_bind_text devono essere eseguite una dopo l'altra per inserire i dati in open_price e comment. In genere, l'utilizzo delle funzioni di associazione può essere rappresentato nel modo seguente:

  1. sqlite3_prepare
  2. Chiama sqlite3_bind_хх uno dopo l'altro e scrivi i dati richiesti nell'istruzione
  3. sqlite3_step
  4. sqlite3_finalize

Per il numero di tipi sqlite3_bind_xx ripete completamente le funzioni di lettura sopra descritte. Pertanto, puoi facilmente combinarli nel connettore in 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);
  }

L'unico obiettivo di questo metodo è scrivere il buffer della cella passata nell'istruzione.

Aggiungiamo il metodo CQLite3Table::QueryBind in modo simile. Il suo primo argomento è la stringa di dati per la scrittura:

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
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
  }

Il suo obiettivo è scrivere la stringa nei parametri appropriati.


2.5. Transazioni/Inserti multiriga

Prima di procedere con questo argomento, è necessario conoscere un'altra funzione dell'API SQLite. Nella sezione precedente, ho descritto la gestione delle richieste in tre fasi: prepara+fase+finalizza. Tuttavia, esiste una soluzione alternativa (in alcune caselle, semplice o addirittura critica) - funzione sqlite3_exec:

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

ppDb [in] - database handle
sql  [in] - SQL query
The remaining three parameters are not considered yet in relation to MQL5.

It returns SQLITE_OK in case of success or else an error code.

Il suo obiettivo principale è eseguire la query in un'unica chiamata senza creare costruzioni in tre fasi.

Aggiungiamo la sua chiamata al connettore:

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
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);
  }

Il metodo risultante è facile da usare. Ad esempio, è possibile eseguire il comando di eliminazione della tabella (DROP TABLE) o di compattazione del database (VACUUM) nel modo seguente:

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

sql3.Exec("VACUUM");


Transazioni

Supponiamo ora di dover aggiungere diverse migliaia di righe alla tabella. Se inseriamo tutto questo nel ciclo:

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

l'esecuzione sarà molto lenta (più di 10(!) secondi). Pertanto, tale implementazione non è raccomandata in SQLite. La soluzione più appropriata qui è utilizzare transazioni: tutte le istruzioni SQL vengono inserite in un elenco comune e quindi passate come una singola query.

Le seguenti istruzioni SQL vengono utilizzate per scrivere l'inizio e la fine della transazione:

BEGIN
...
COMMIT

Tutto il contenuto viene eseguito all'ultima istruzione COMMIT. L'istruzione ROLLBACK viene utilizzata nel caso in cui il ciclo debba essere interrotto o le istruzioni già aggiunte non debbano essere eseguite.

Ad esempio, tutti i deal dell'account vengono aggiunti alla tabella.

#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());
  }

Dopo che lo script è stato applicato all'account, inserisce immediatamente le offerte dell'account nella tabella.

Le statistiche vengono visualizzate nel journal del terminale

Puoi giocare con lo script: commenta le righe contenenti BEGIN, ROLLBACK e COMMIT. Se ci sono più di centinaia di offerte sul tuo account, vedrai immediatamente la differenza. A proposito, secondo alcuni test, le transazioni SQLite funzionano più velocemente che in MySQL o PostgreSQL.


3. Compilazione della versione a 64 bit (sqlite3_64.dll)

  1. Scarica Codice sorgente SQLite (amalgamazione) e trova il file sqlite3.c.
  2. Scarica sqlite-dll-win32 ed estrai il file sqlite3.dll da esso.
  3. Eseguire il comando della console LIB.EXE /DEF:sqlite3.def nella cartella in cui è stato estratto il file dll. Assicurati che i percorsi del file lib.exe siano impostati nella variabile di sistema PATH o trovalo in Visual Studio.
  4. Crea un progetto DLL selezionando Rilascia configurazione per piattaforme a 64 bit.
  5. Aggiungere al progetto i file sqlite3.c scaricati e sqlite3.def ottenuti. Se il compilatore non accetta alcune funzioni dal file def, basta commentarle.
  6. I seguenti parametri dovrebbero essere impostati nelle impostazioni del progetto:
    C/C++ -> Generale -> Formato informazioni di debug = Database del programma (/Zi)
    C/C++ Intestazioni precompilate Crea/Usa intestazione precompilata = Non si utilizzano intestazioni precompilate (/Yu)
  7. Compila e ottieni una dll a 64 bit.


Conclusione

Spero che l'articolo diventi la tua guida indispensabile per padroneggiare SQLite. Forse lo utilizzerai nei tuoi progetti futuri. Questa breve panoramica ha fornito alcune informazioni sulle funzionalità di SQLite come soluzione perfetta e affidabile per le applicazioni.

In questo articolo ho descritto tutte le caselle che potresti incontrare quando gestisci i dati di trading. Come compito a casa, ti consiglio di sviluppare un semplice raccoglitore di zecche inserendo le zecche nella tabella per ogni simbolo. È possibile trovare il codice sorgente della libreria di classi e gli script di test nell'allegato sottostante.

Ti auguro buona fortuna e grandi profitti!

Tradotto dal russo da MetaQuotes Ltd.
Articolo originale: https://www.mql5.com/ru/articles/862

File allegati |
MQL5.zip (790.12 KB)
Indicatore per la costruzione di un grafico Three Line Break Indicatore per la costruzione di un grafico Three Line Break
Questo articolo è dedicato al grafico Three Line Break, suggerito da Steve Nison nel suo libro "Beyond Candlesticks". Il più grande vantaggio di questo grafico è che consente di filtrare le fluttuazioni minori di un prezzo rispetto al movimento precedente. Discuteremo il principio della costruzione del grafico, il codice dell'indicatore e alcuni esempi di strategie di trading basate su di esso.
Reti neurali economiche - Collega NeuroPro con MetaTrader 5 Reti neurali economiche - Collega NeuroPro con MetaTrader 5
Se specifici programmi di rete neurale per il trading sembrano costosi e complessi o, al contrario, troppo semplici, prova NeuroPro. È gratuito e contiene il set ottimale di funzionalità per i dilettanti. Questo articolo ti spiegherà come usarlo insieme a MetaTrader 5.
Racconti di robot di trading: Meno è veramente di più? Racconti di robot di trading: Meno è veramente di più?
Due anni fa in "The Last Crusade" abbiamo esaminato un metodo piuttosto interessante ma attualmente non ampiamente utilizzato per la visualizzazione di informazioni di mercato - grafici a punti e cifre. Ora ti suggerisco di provare a scrivere un robot di trading basato sui modelli rilevati sul grafico a punti e figure.
Video tutorial MetaTrader Signals Service Video tutorial MetaTrader Signals Service
In soli 15 minuti, questo video tutorial spiega cos'è il servizio di segnali MetaTrader e mostra in dettaglio come abbonarsi ai segnali di trading e come diventare un fornitore di segnali nel nostro servizio. Guardando questo tutorial, sarai in grado di iscriverti a qualsiasi segnale di trading o pubblicare e promuovere i tuoi segnali nel nostro servizio.