SQL e MQL5: Trabalhando com Banco de Dados SQLite

--- | 14 julho, 2014

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:

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:

Há três maneiras de se trabalhar com SQLite:

  1. você pode usar o arquivo DLL com um conjunto completo de funções API;
  2. você pode usar console de comandos (shell) para um arquivo EXE;
  3. 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:

  1. sqlite3_prepare - verificação e o recebimento da lista de declarações;
  2. sqlite3_step - execução destas declarações;
  3. 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:

  1. sqlite3_prepare
  2. sqlite3_column_count - encontrar o número obtido de colunas da tabela
  3. Enquanto o resultado do passo atual é sqlite3_step == SQLITE_ROW
    1. sqlite3_column_хх - ler as células na sequência
  4. 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:

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


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:

  1. sqlite3_prepare
  2. Chamar sqlite3_bind_хх uma após a outra e escrever os dados necessários para a declaração
  3. sqlite3_step
  4. 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)

  1. Baixar o código fonte SQLite e procurar o arquivo sqlite3.c.
  2. Baixar o sqlite-dll-win32 e extrair o arquivo sqlite3.dll a partir do mesmo.
  3. 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.
  4. Criar projeto de DLL selecionando a configuração da Versão para plataformas de 64 bits
  5. 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.
  6. 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)
  7. 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!