
SQL e MQL5: Lavorare con il database SQLite
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:
- puoi utilizzare il file DLL con un set completo di funzioni API;
- puoi usare i comandi della shell su un file EXE;
- 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:
- sqlite3_prepare - verifica e ricezione dell'elenco delle dichiarazioni;
- sqlite3_step - eseguire queste istruzioni;
- 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:
- sqlite3_prepare
- sqlite3_column_count - scopri il numero di colonne della tabella ottenuta
- Mentre il risultato del passaggio corrente sqlite3_step == SQLITE_ROW
- sqlite3_column_хх - leggi le celle della stringa
- 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:
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:
- sqlite3_prepare
- Chiama sqlite3_bind_хх uno dopo l'altro e scrivi i dati richiesti nell'istruzione
- sqlite3_step
- 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)
- Scarica Codice sorgente SQLite (amalgamazione) e trova il file sqlite3.c.
- Scarica sqlite-dll-win32 ed estrai il file sqlite3.dll da esso.
- 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.
- Crea un progetto DLL selezionando Rilascia configurazione per piattaforme a 64 bit.
- Aggiungere al progetto i file sqlite3.c scaricati e sqlite3.def ottenuti. Se il compilatore non accetta alcune funzioni dal file def, basta commentarle.
- 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)
- 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





- App di trading gratuite
- Oltre 8.000 segnali per il copy trading
- Notizie economiche per esplorare i mercati finanziari
Accetti la politica del sito e le condizioni d’uso