SQL e MQL5: Trabalhando com Banco de Dados SQLite
Pequeno. Rápido. Confiável.
Escolha qualquer um dos três.
Introdução
Muitos desenvolvedores usam bancos de dados em seus projetos para fins de armazenamento de dados e ficam hesitantes sobre isso, preocupados com quanto tempo extra a instalação do servidor SQL exigirá. E se não é tão difícil para os programadores (se um sistema de gerenciamento de banco de dados (DBMS) já foi instalado para outros fins), a instalação certamente vai ser um problema para o usuário comum que pode eventualmente ser desencorajado a instalar o software completamente.
Assim muitos desenvolvedores optam por não lidar com DBMS ao perceberem que as soluções trabalhadas serão usadas por muito poucas pessoas. Como resultado, eles se voltam para trabalhar com arquivos (muitas vezes tendo que lidar com mais de um arquivo, devido a variedade de dados utilizados): CSV, menos frequentemente XML ou JSON, ou arquivos de dados binários com rigoroso tamanho da estrutura, etc.
Portanto, verifica-se que é uma ótima alternativa o servidor SQL! E você não precisa nem instalar software adicional, pois tudo é feito localmente em seu projeto, enquanto ainda for permitido que você use o poder SQL. Estamos falando sobre SQLite.
O objetivo deste artigo é que você possa rapidamente começar com SQLite. Por isso não vou entrar em sutilezas e em todos os conjuntos de parâmetros que se possa imaginar e flags de funções, mas em vez disso vamos criar uma suave conexão wrapper para executar comandos SQL e demonstrar seu uso.
Para prosseguir com o artigo, você precisa:
- Esteja de bom humor ;)
- Extraia os arquivos anexados ao artigo para a pasta do terminal do cliente MetaTrader 5
- Instale qualquer Visualizador SQLite (por exempo: SQLiteStudio)
- Adicionar aos Favoritos a documentação oficial sobre SQLite no link http://www.sqlite.org
Conteúdo
1. Princípios SQLite
2. API SQLite3
2.1. Abrindo e Fechando um Banco de Dados
2.2. Execução de Consultas SQL
2.3. Obtendo Dados de Tabelas
2.4. Escrevendo Dados de Parâmetros com Vinculação
2.5. Transações/Inserções Multi Linhas (Exemplo: criando a Tabela de Operações da Conta de Negociação)
3. Compilando Versão de 64 bits (sqlite3_64.dll)
1. Princípios SQLite
SQLite é um RDBMS (Sistema Gerenciador de Banco de Dados Relacional), cuja principal característica é a ausência de um servidor SQL instalado localmente. Sua aplicação é visto aqui como um servidor. O trabalho com banco de dados SQLite é basicamente trabalhar com um arquivo (em disco ou na memória). Todos os dados podem ser arquivados ou movidos para outro computador sem a necessidade de instalá-los de qualquer forma específica.
Com SQLite, desenvolvedores e usuários podem se beneficiar de várias vantagens inegáveis:
- não há necessidade de instalar software adicional;
- os dados são armazenados em arquivo local, oferecendo a transparência de gestão, ou seja, você pode visualizar e editá-los independente das suas aplicações;
- capacidade de importar e exportar tabelas para outros DBMS;
- o código usa consultas SQL amigáveis, permitindo forçar o aplicativo para trabalhar com outros DBMS a qualquer momento.
Há três maneiras de se trabalhar com SQLite:
- você pode usar o arquivo DLL com um conjunto completo de funções API;
- você pode usar console de comandos (shell) para um arquivo EXE;
- você pode compilar seu projeto, incluindo os códigos-fonte do API SQLite.
Neste artigo, vou descrever a primeira opção, pois é a mais habitual em MQL5.
2. SQLite3 API
O funcionamento do conector vai requerer a utilização das seguintes funções 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
Você também vai precisar das funções msvcrt.dll de baixo nível para trabalhar com ponteiros:
strlen strcpy memcpy
Se estou criando um conector para funcionar em terminais de 32 e 64 de bits, então é importante considerar o tamanho do ponteiro enviado para funções API. Vamos separar seus nomes:
// 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 necessário, todas as funções API serão sobrecarregadas para ponteiros de 32 e 64 bits. Por favor, note que todos os ponteiros do conector serão de 64 bits. Eles serão convertidos para 32 bits diretamente nas funções API sobrecarregadas. A importação da função API para o código fonte é fornecida pelo arquivo SQLite3Import.mqh
Tipos de dados SQLite
Existem cinco tipos de dados da Versão 3 SQLite
Tipo |
Descrição |
---|---|
NULL | Valor nulo. |
INTEGER | Integrais armazenadas em 1, 2, 3, 4, 6 ou 8 bytes, dependendo da magnitude do valor armazenado. |
REAL | Número real de 8-byte. |
TEXT | String de texto com o caractere final armazenado \0 usando codificação UTF-8 ou UTF-16. |
BLOB | Dados binários arbitrários |
Você também pode usar outros nomes de tipo, por exemplo, INT ou BIGINT são aceitos em vários DBMS para especificar o tipo de dados de um campo ao criar uma tabela a partir de uma consulta SQL. Neste caso, SQLite irá convertê-los a um de seus tipos intrínsecos, neste caso para INTEGER. Para mais informações sobre tipos de dados e suas relações, leia a documentação http://www.sqlite.org/datatype3.html
2.1. Abrindo e Fechando um Banco de Dados
Como você já sabe, um banco de dados SQLite3 é um arquivo regular. Assim, a abertura de um banco de dados é de fato igual a abertura de um arquivo que você consegue manipular.
Foi idealizado usando a função 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.
Um arquivo de banco de dados é fechado usando a função 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.
Vamos criar funções no conector para abrir e fechar um banco de dados.
//+------------------------------------------------------------------+ //| 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); }
O conector agora pode abrir e fechar um banco de dados. Agora verificar sua execução com um simples 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(); }
Execute o script no modo de depuração, respire fundo e verifique o funcionamento de cada string. Como resultado, um arquivo do banco de dados será exibido na pasta de instalação do terminal MetaTrader 5. Parabenize a si mesmo com este sucesso e prossiga para a próxima seção.
2.2. Execução de Consultas SQL
Qualquer consulta SQL no SQLite3 tem de passar por pelo menos em três fases:
- sqlite3_prepare - verificação e o recebimento da lista de declarações;
- sqlite3_step - execução destas declarações;
- sqlite3_finalize - finalizar e limpar a memória.
Esta estrutura é apropriada principalmente para criar ou excluir tabelas, assim como para escrever dados não-binários, ou seja, para os casos em que uma consulta SQL não implica no retorno de todos os dados.
Se a consulta envolve o recebimento de dados ou escrita de dados binários, as funções sqlite3_column_хх e sqlite3_bind_хх são respectivamente utilizadas na segunda fase. Estas funções são descritas em detalhes na próxima seção.
Vamos escrever o método CSQLite3Base::Query para executar uma consulta SQL simples:
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ 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 você pode ver, as funções sqlite3_prepare, sqlite3_step e sqlite3_finalize são sequenciais.
Considere a execução do métodoCSQLite3Base::Query ao trabalhar com tabelas em SQLite:
// Create the table (CREATE TABLE) sql3.Query("CREATE TABLE IF NOT EXISTS `TestQuery` (`ticket` INTEGER, `open_price` DOUBLE, `comment` TEXT)");
Depois de executar este comando, a tabela aparece no banco de dados:
// 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`");
Depois de executar estes comandos, recebemos a tabela com um novo nome e um campo adicional:
// 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)")
A entrada a seguir aparece na tabela após a nova linha ser adicionada e alterada:
Finalmente, os comandos seguintes devem ser executados um após o outro para limpar o banco de dados.
// 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");
Antes de passar para a próxima seção, precisamos do método que recebe uma descrição do erro. De minha própria experiência, posso dizer que o código de erro pode fornecer muitas informações, mas a descrição do erro mostra o local no texto da consulta SQL onde um erro apareceu, simplificando a sua detecção e correção.
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.
No conector, devemos adicionar o método para receber esta string a partir do ponteiro usando strcpy e o 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. Obtendo dados das Tabelas
Como já mencionado no início da seção 2.2, a leitura dos dados é realizada utilizando a função sqlite3_column_хх. Isto pode ser representado esquematicamente da seguinte forma:
- sqlite3_prepare
- sqlite3_column_count - encontrar o número obtido de colunas da tabela
- Enquanto o resultado do passo atual é sqlite3_step == SQLITE_ROW
- sqlite3_column_хх - ler as células na sequência
- sqlite3_finalize
Uma vez que estamos nos aproximando de uma extensa seção sobre leitura e escrita de dados, é um bom momento para descrever três classes de contêineres utilizadas em toda a troca de dados. O modelo de dados necessários depende da forma como os dados são armazenados no banco de dados:
Banco de Dados
|
Table é um array de linhas.
|
Row é um array de células.
|
Cell é um buffer de bytes de comprimento arbitrário.
//+------------------------------------------------------------------+ //| 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 você pode ver, as conexões CSQLite3Row e CSQLite3Table são primitivas - são array de dados convencionais. A classe cellCSQLite3Cell também tem array de dados uchar + tipo de campo de Dados. O array Byte é implementado na classe CByteImage (similar ao bem conhecido CFastFile).
Eu criei a seguinte enumeração para facilitar a operação do conector e gerenciar os tipos de dados Cell:
enum enCellType
{
CT_UNDEF,
CT_NULL,
CT_INT,
CT_INT64,
CT_DBL,
CT_TEXT,
CT_BLOB,
CT_LAST
};
Note que o tipo CT_UNDEF foi adicionado a cinco tipos básicos SQLite3 para identificar o estado inicial Cell. Todo o tipo INTEGER é dividido em CT_INT e CT_INT64, dividido similarmente de acordo com as funções sqlite3_bind_intXX e sqlite3_column_intXX.
Obtendo Dados
A fim de obter dados a partir do Cell, devemos criar o método gerado pelos tipos das funções sqlite3_column_хх. Serão verificados os tipos, o tamanho dos dados e escrevê-los para 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); }
A função é bastante grande, mas somente lê os dados a partir da declaração atual e os armazena no buffer Cell.
Também devemos sobrecarregar a função CSQLite3Base::Query pela adição da tabela de contêiner CSQLite3Table para os dados recebidos como primeiro parâmetro.
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ 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 }
Temos todas as funções necessárias para o recebimento de dados. Vamos passar para os exemplos:
// Read data (SELECT) CSQLite3Table tbl; sql3.Query(tbl, "SELECT * FROM `Trades`")
Imprima o resultado da consulta no terminal usando o seguinte comando Print(TablePrint(tbl)). Veremos as seguintes entradas no diário (a ordem é de baixo para cima):
// 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`;");
O resultado da consulta é impresso da mesma forma usando Print(TablePrint(tbl)). Podemos ver a tabela resultante:
Como pode ser visto a partir dos exemplos, os resultados da execução da consulta são colocados numa variável tbl. Depois disso, você pode facilmente obter e processá-los com seu critério.
2.4. Escrevendo Dados de Parâmetros com Vinculação
Outro tema importante para os recém-chegados é escrever o banco de dados com formato "inconveniente". Em tempo, queremos dizer dados binários aqui. Não pode ser colocado diretamente numa declaração texto comum INSERT ou UPDATE, uma string é considerada completa quando é encontrado o primeiro zero. O mesmo problema ocorre quando a string em si contém aspas simples '.
Vinculação tardia pode ser útil em alguns casos, especialmente quando a tabela é grande. Seria difícil e não confiável escrever todos os campos para uma única linha, pois você pode facilmente perder alguma coisa. As funções das séries sqlite3_bind_хх são necessárias para a operação de vinculação.
A fim de aplicar a vinculação, um modelo deve ser inserido em vez dos dados passados. Vou considerar um dos casos - o sinal "?". Em outras palavras, a consulta UPDATE ficará da seguinte forma:
Então, as funções sqlite3_bind_double e sqlite3_bind_text devem ser executadas uma após a outra para colocar os dados para open_price e comment. Geralmente, o trabalho com funções bind podem ser representadas da seguinte forma:
- sqlite3_prepare
- Chamar sqlite3_bind_хх uma após a outra e escrever os dados necessários para a declaração
- sqlite3_step
- sqlite3_finalize
Pelo número de tipos da sqlite3_bind_xx, repetir completamente as funções de leitura como descrito acima. Assim, você pode facilmente combiná-los no conector em 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); }
O único objetivo deste método é escrever o buffer Cell para a declaração.
Vamos adicionar o método CQLite3Table::QueryBind de uma forma semelhante. Seu primeiro argumento é uma string de dados para a escrita:
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ 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 }
Seu objetivo é escrever a string com os parâmetros apropriados.
2.5. Transações / Inserções Multi Linhas
Antes de prosseguir com este tema, você precisa saber mais uma função API SQLite. Na seção anterior, descrevi a manipulação de três estágios de pedidos: prepare+step+finalize. No entanto, existe uma solução alternativa (em alguns casos, simples ou mesmo crítica) - a função 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.
Seu principal objetivo é executar a consulta numa única chamada, sem criar construções de três estágios.
Vamos adicionar sua chamada ao 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); }
O método resultante é fácil de usar: Por exemplo, você pode executar a exclusão da tabela (DROP TABLE) ou o comando do banco de dados compacto (VACUUM) da seguinte forma:
sql3.Exec("DROP TABLE `Trades`"); sql3.Exec("VACUUM");
Transações
Agora, suponha que temos de adicionar milhares de linhas para a tabela. Inserindo tudo isso no loop:
for (int i=0; i<N; i++) sql3.Query("INSERT INTO `Table` VALUES(1, 2, 'text')");
a execução será muito lenta (mais de 10 segundos!). Assim, essa aplicação não é recomendada no SQLite. A solução mais adequada aqui para usar em transações: todas as instruções SQL são inseridas em uma lista comum e então passadas como uma única consulta.
As seguintes instruções SQL são usadas para escrever o início e o fim da transação:
BEGIN ... COMMIT
Todos os conteúdos são executados na última declaração COMMIT. A declaração ROLLBACK é utilizada no caso em que o loop deve ser interrompido ou já foi adicionado declarações que não devem ser executadas.
Como um exemplo, todas as operações da conta são adicionados na tabela.
#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()); }
Depois que o script é aplicado à conta, ele insere operações da conta para a tabela imediatamente.
A estatística são apresentadas no diário de terminal
Você pode brincar com o script: comente fora as linhas que contêm BEGIN, ROLLBACK e COMMIT. Se houver centenas de ofertas em sua conta, você vai ver a diferença imediatamente. Aliás, de acordo com alguns testes, o SQLite trabalha mais rápido do que MySQL ou PostgreSQL.
3. Compilando Versão de 64 bits (sqlite3_64.dll)
- Baixar o código fonte SQLite e procurar o arquivo sqlite3.c.
- Baixar o sqlite-dll-win32 e extrair o arquivo sqlite3.dll a partir do mesmo.
- Executar o comando de console LIB.EXE /DEF:sqlite3.def na pasta onde o arquivo dll foi extraído. Certificar se os caminhos para o arquivo lib.exe estão definidos na variável de sistema PATH ou encontrá-lo no seu Visual Studio.
- Criar projeto de DLL selecionando a configuração da Versão para plataformas de 64 bits
- Adicionar os arquivos baixados sqlite3.c e sqlite3.def ao projeto. Se o compilador não aceitar algumas funções do arquivo def, basta comentá-las fora.
- Os seguintes parâmetros devem ser definidos nas configurações do projeto:
C/C++ --> General --> Debug Information Format = Program Database (/Zi)
C/C++ --> Precompiled Headers --> Create/Use Precompiled Header = Not Using Precompiled Headers (/Yu)
- Compilar e obter dll de 64 bits.
Conclusão
Espero que o artigo seja o seu guia indispensável para dominar SQLite. Talvez você vai usá-lo em seus futuros projetos. Este breve resumo tem fornecido algumas dicas sobre a funcionalidade do SQLite como uma solução perfeita e confiável para aplicativos.
Neste artigo eu descrevi todos os casos que você pode enfrentar ao lidar com dados de negociação. Como lição de casa, eu recomendo que você desenvolva um simples coletor de ticks inserindo-os na tabela para cada símbolo (ativo). Você pode encontrar código-fonte da biblioteca das classes e scripts de testes no anexo abaixo.
Desejo-lhe boa sorte e grandes lucros!
Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/862
- Aplicativos de negociação gratuitos
- 8 000+ sinais para cópia
- Notícias econômicas para análise dos mercados financeiros
Você concorda com a política do site e com os termos de uso
Novo artigo SQL e MQL5: Trabalhando com Banco de Dados SQLite foi publicado:
Autor: o_O
Hey man, great article.
Novo artigo SQL e MQL5: Trabalhando com Banco de Dados SQLite foi publicado:
Autor: o_O
Hey!
Since build 2170, this library is giving scope errors. Could you please update it as to follow the new MQL5 scope 'guidelines'? Thanks!
Btw great work!