SQL und MQL5: Mit der SQLite Datenbank arbeiten

--- | 2 Juni, 2016

Klein. Schnell. Zuverlässig.
Wählen Sie.

 

Einleitung

Viele Entwickler überlegen sich in ihren Projekten mit Datenbanken zur Datenspeicherung zu arbeiten und sind dennoch zögerlich diese Überlegungen auch umzusetzen, da sie wissen, wie viel extra Zeit die Installierung des SQL-Servers womöglich in Anspruch nehmen kann. Und obwohl diese Installierung für Programmierer kein großes Problem darstellt (wenn aus anderen Gründen bereits ein Datenbank-Managementsystem (DBMS) installiert worden ist), ist sie für den gemeinen Anwender zweifellos ein Hindernis, das so groß sein kann, dass er letzten Endes davon absieht, diese Software überhaupt zu installieren.

Daher haben sich viele Entwickler entschieden, sich nicht mit einem DBMS abzugeben, da sie erkannt haben, dass die Lösungen, an denen sie derzeit arbeiten, von nur wenigen Menschen genutzt werden. Als Folge davon arbeiten sie lieber mit Dateien (und müssen dabei, angesichts der unterschiedlichen verwendeten Daten, oft mit mehr als einer Datei arbeiten): CSV, weniger häufig XML oder JSON oder binäre Datendateien mit strikter Strukturgröße, usw.

Doch die gute Nachricht ist: es gibt eine tolle Alternative zum SQL-Server! Und man muss dazu noch nicht einmal zusätzliche Software installieren, da alles lokal in Ihrem Projekt erledigt wird und Sie trotzdem noch die volle Leistung von SQL nutzen können. Wir sprechen von SQLite.

Zweck dieses Beitrags ist, Sie soweit vorzubereiten, dass Sie rasch mit SQLite zu arbeiten beginnen können. Daher werde ich nicht auf die Feinheiten und alle möglichen Parametersets und Funktionsmarker eingehen, sondern stattdessen ein leichtes Verbindungspaket anlegen, mit dem SQL-Befehle ausgeführt werden können, und seine Verwendung erklären.

Um diesen Beitrag weiter lesen zu können, müssen Sie:

Inhalt

1. SQLite Prinzipien
2. SQLite3 Programmierschnittstelle (API)
    2.1. Wie man eine Datenbank öffnet und schließt
    2.2. Ausführung von SQL-Anfragen
    2.3. Wie man Daten aus Tabellen erhält
    2.4. Wie man Parameterdaten mit Bindung schreibt
    2.5. Transaktionen / Einfügungen in mehrere Reihen (Beispiel des Anlegens der Tabelle der Abschlüsse in einem Handelskonto)
3. Erstellung der 64-Bit Version (sqlite3_64.dll)


1. SQLite Prinzipien

SQLite ist ein RDBMS, dessen wichtigstes Merkmal ist, dass man ohne lokal installiertem SQL-Server auskommt. Ihre Anwendung wird hier als Server betrachtet. Die Arbeit mit der SQLite-Datenbank geht als im Grunde wie die Arbeit mit einer Datei vonstatten (auf einem Laufwerk oder im Memory). Alle Daten können archiviert oder auf einen andern Computer verschoben werden, ohne sie dazu irgendwie spezielle installieren zu müssen.

Mit SQLite profitieren Entwickler und normale Anwender von einigen unbestreitbaren Vorteilen:

Man kann auf dreierlei Art mit SQLite arbeiten:

  1. Sie können die DLL-Datei mit einem kompletten Set an Programmierschnittstellenfunktionen verwenden;
  2. Sie können die Shell-Befehle an eine EXE-Datei verwenden;
  3. Sie können Ihr Projekt inkl. der Quellcodes der SQLite-Programmierschnittstelle erstellen.

In diesem Beitrag beschreibe ich die erste Option, da sie in MQL5 am gängigsten ist.

 

2. Die SQLite3 Programmierschnittstelle (API)

Damit der Connector funktioniert, ist die Verwendung der SQLite-Funktionen nötig:

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

Sie sehen auch Low-Level msvcrt.dll-Funktionen zur Arbeit mit Zeigern:

strlen
strcpy
memcpy

Da ich einen Connector erzeuge, der in 32- und 64-Bit Terminals funktionieren soll, ist es hier wichtig, die Größe des Zeigers, der an die API-Fnktionen gesendet werden soll, zu berücksichtigen. Teilen wir ihre Namen auf:

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

Falls erforderlich werden alle API-Funktionen für 32- und 64-Bit Zeiger überladen. Bitte beachten Sie: nicht alle Zeiger des Connectores sind 64-Bit. Sie werden direkt in den überladenen API-Funktionen zu 32-Bit umgewandelt. Der Quellcode zum Import der API-Funktion steht in SQLite3Import.mqh zur Verfügung


SQLite-Datentypen

Die SQLite Version3 besitzt fünf Datentypen

Typ
Beschreibung
NULL NULL-Wert.
ganze Zahl ganze Zahl, gespeichert in 1, 2, 3, 4, 6 oder 8 Bytes, je nach Umfang des gespeicherten Werts.
EACHT 8-Byte echte Ziffer.
TEXT Textstring mit dem Endzeichen \0, gespeichert mittels UTF-8- oder UTF-16-Kodierung.
BLOB Beliebige Binärdaten


Zur Spezifizierung des Datentyps eines Felds beim Anlegen einer Tabelle einer SQL-Anfrage können Sie auch andere Typenbezeichnungen verwenden, z.B. BIGINT oder INT, die in verschiedenen DBMS zugelassen sind. In diesem Fall wandelt SQLite sie in eine der Typen um, die SQLite besitzt, in diesem Fall in eine GANZE ZAHL. Weitere Informationen zu Datentypen und ihre Beziehungen finden Sie in folgender Dokumentation: http://www.sqlite.org/datatype3.html


2.1 Wie man eine Datenbank öffnet und schließt

Wie Sie ja bereits wissen, ist eine Datenbank in SQLite3 eine normale Datei. Eine Datenbank zu öffnen geht daher in der Tat ganz genauso, wie eine Datei zu öffnen und ihren Handle zu erhalten, und zwar

mit Hilfe der sqlite3_open Funktion:

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.

Will man eine Datenbank schließen, geht das mit Hilfe der sqlite3_close Funktion

int sqlite3_close(sqlite3_p64 ppDb);

ppDb [in] - file handle

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


Legen wir doch im Connector Funktionen zum Öffnen und Schließen der Datenbank an.

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

Jetzt kann der Connector eine Datenbank öffnen und schließen. Jetzt prüfen wir mit einem einfachen Script seine Leistung:

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

Lassen Sie das Script im Debug-Modus laufen, atmen Sie tief durch und überprüfen die Arbeit jedes Strings. Als Ergebnis erscheint im Installierungsordner im MetaTrader 5 Terminal eine Datenbank-Datei. Sie dürfen sich nun beglückwünschen und können zur nächsten Etappe gehen.


2.2 Ausführung von SQL-Anfragen

In SQLite3 muss jede SQL-Anfrage mind. drei Phasen durchlaufen:

  1. sqlite3_prepare - Verifizierung und Erhalt der Liste mit Aussagen;
  2. sqlite3_step - Ausführung dieser Aussagen;
  3. sqlite3_finalize - Abschlussarbeiten und Leeren des Memory.

Diese Struktur ist primär geeignet zum Anlegen oder Löschen von Tabellen sowie zum Schreiben nicht-binärer Daten, also immer dann, wenn eine SQL-Anfrage nicht impliziert, dass irgendwelche Daten geliefert werden, außer für den Status einer erfolgreichen Ausführung.

Hat die Anfrage mit dem Erhalt von Daten oder dem Schreiben binärer Daten zu tun, wird in der zweiten Phase die sqlite3_column_хх bzw. sqlite3_bind_хх Funktion verwendet. Diese Funktionen werden im nächsten Abschnitt detailliert beschrieben.

Schreiben wir jetzt die CSQLite3Base::Query Methode zur Ausführung einer einfachen SQL-Anfrage:

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

Wie Sie sehen, stehen die Funktionen sqlite3_prepare, sqlite3_step und sqlite3_finalize schön ordentlich hintereinander.

Bei der Arbeit mit Tabellen in SQLite sollten Sie an die Ausführung vonCSQLite3Base::Query denken:

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

Nach Ausführen dieses Befehls erscheint die Tabelle in der Datenbank:

// 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`");

Nach Ausführen dieser Befehle erhalten wir eine Tabelle mit einem neuen Namen und einem zusätzlichen Feld:

// 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)")

Nachdem eine neue Zeile hinzugefügt und verändert wurde, erscheint in der Tabelle der folgende Eintrag:

Schließlich sollten noch die folgenden Befehle, einer nach dem anderen, ausgeführt werden, um die Datenbank aufzuräumen.

// 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");

Bevor wir zum nächsten Abschnitt weitergehen, brauchen wir noch die Methode für den Erhalt einer Fehlerbeschreibung. Aus eigener Erfahrung kann ich sagen, dass der Fehlercode jede Menge Informationen liefern kann, doch die Beschreibung des Fehlers zeigt die Stelle im SQL-Anfragetext, wo der Fehler aufgetreten ist und erleichtert Fehlersuche und Fehlerbehebung ganz enorm.

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.

Wir sollten die Methode für den Erhalt dieses Strings vom Zeiger mit Hilfe von strcpy und strlen im Connector hinzufügen.

//+------------------------------------------------------------------+
//| 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 Wie man Daten aus Tabellen erhält

Wie bereits zu Beginn von Abschnitt 2.2 erwähnt, werden die Daten mit Hilfe der sqlite3_column_хх Funktionen gelesen. Schematisch lässt sich dies wie folgt darstellen:

  1. sqlite3_prepare
  2. sqlite3_column_count - die Anzahl der Spalten der erhaltenen Tabelle ermitteln
  3. Aktueller Schritt führt dabei zu sqlite3_step == SQLITE_ROW
    1. sqlite3_column_хх - die Zellen des Strings lesen
  4. sqlite3_finalize

Da wir uns gerade mit dem umfangreichen Abschnitt beschäftigen, der mit dem Lesen und Schreiben zu tun hat, ist es sicherlich nicht schlecht, die drei Container-Klassen zu beschreiben, die während des gesamten Datenaustausches verwendet werden. Welches Datenmodell notwendig ist, hängt davon ab, wie die Daten in der Datenbank gespeichert sind:

Datenbank
|
Tabelle ist ein Zeilen-Array.
|
Zeile ist ein Zellen-Array.
|
Zelle ist ein Byte-Puffer beliebiger Länge.


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

Wie Sie sehen, sind die CSQLite3Row und CSQLite3Table Verbindungen primitiv - das sind ganz gewöhnliche Datenarrays. Die CSQLite3Cell Zellklasse hat zudem ein uchar Datenarray + ein Feld für den Datentyp. Das Byte-Array wird in derCByteImage Klasse implementiert (ähnlich wie die bekannte CFastFile).

Zur Vereinfachung der Arbeit des Connectors und zur Verwaltung der Zelldatentypen habe ich die folgende Aufzählung angelegt:

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

Beachten Sie: derCT_UNDEF -Typ wurde fünf grundlegenden SQLite3-Typen hinzugefügt, um den anfänglichen Zellenstatus ermitteln zu können Der komplette GANZEZAHL-Typ wurde in CT_INT und CT_INT64 unterteilt, gemäß den ganz ähnlich unterteiltensqlite3_bind_intXX und sqlite3_column_intXX Funktionen.

Daten erhalten

Um Daten aus der Zelle zu erhalten, sollten wir die Methode zur Verallgemeinerung der Funktionen vom Typ sqlite3_column_хх erzeugen. Sie prüft den Datentyp und die Größe und schreibt sie in 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);
  }

Die Funktion ist ziemlich groß, doch sie liest nur Daten aus der aktuellen Aussage und legt sie in einer Zelle ab.

Wir sollten auch die CSQLite3Base::Query Funktion durch Hinzufügen der CSQLite3Table Container-Tabelle für erhaltene Daten als den ersten Parameter überladen.

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

Jetzt haben wir alle Funktionen, die wir zum Erhalt von Daten brauchen. Gehen wir zu ihren Beispielen weiter:

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

Drucken Sie mit Hilfe des folgenden Befehls Print(TablePrint(tbl)) das Ergebnis der Anfrage im Terminal aus. Im Logbuch finden wir die folgenden Einträge (Reihenfolge von unten nach oben):

// 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`;");

Das Ergebnis der Anfrage wird ganz genauso, nämlich mittels Print(TablePrint(tbl)), gedruckt. Wir sehen die bestehende Tabelle:

Diese Beispiele zeigen zudem, dass die Ergebnisse der Ausführung einer Anfrage in der tbl-Variable abgelegt werden. Anschließend haben Sie leicht darauf Zugriff und können Sie ganz nach Belieben bearbeiten.


2.4 Wie man Parameterdaten mit Bindung schreibt

Ein weiterer Punkt, der für Neulinge vielleicht interessant sein könnte, ist, wie man Daten mit einem "unbequemen" Format in die Datenbank schreibt - nämlich binäre Daten. Sie können nicht direkt in eine gemeinsame INSERT oder UPDATE-Textaussage übertragen werden, da ein String dann als abgeschlossen gilt, wenn man auf die erste "0" trifft. Das gleiche Problem tritt auf, wenn der String selbst einzelne Notierungen ' enthält.

Spätes Binden kann in manchen Fällen nützlich sein, vor allem wenn die Tabelle breit ist. Alle Felder in eine einzige Zeile zu schreiben, wäre schwierig und viel zu unzuverlässig, da man dabei leicht etwas übersehen könnte. Für die Durchführung einer Bindung sind die Funktionen der sqlite3_bind_хх Reihe notwendig.

Um Bindung anzuwenden, sollte anstelle der übertragenen Daten ein Template eingefügt werden. Ich betrachte hier mal einen Fall - das Zeichen "?". Also wird die UPDATE-Anfrage hierfür folgendermaßen aussehen:

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


Danach sollten die sqlite3_bind_double und sqlite3_bind_text -Funktionen hintereinander ausgeführt werden, um Daten in open_price und comment zu platzieren. Die Arbeit mit bind -Funktionen kann im Allgemeinen folgendermaßen dargestellt werden:

  1. sqlite3_prepare
  2. sqlite3_bind_хх hintereinander aufrufen und die erforderlichen Daten in die Aussage schreiben
  3. sqlite3_step
  4. sqlite3_finalize

sqlite3_bind_xx wiederholt per Anzahl der Typen vollständig die oben beschriebenen Lese-Funktionen. Somit können Sie sie leicht im Connector in CSQLite3Base::BindStatement kombinieren.

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

Das einzige Ziel dieser Methode besteht darin, den Puffer der übertragenen Zelle in die Aussage zu schreiben.

Die CQLite3Table::QueryBind Methode fügen wir jetzt auf ganz ähnliche Weise hinzu. Ihre erste Begründung ist der Datenstring zum Schreiben:

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

Ihr Ziel besteht darin, die String in die entsprechenden Parameter zu schreiben.


2.5 Transaktionen / Einfügungen in mehrere Reihen

Bevor wir weitermachen, müssen Sie noch eine weitere SQLite-API-Funktion kennen. Im vorangegangenen Abschnitt habe ich den dreistufigen Umgang mit Anfragen beschrieben: prepare+step+finalize. Doch da gibt es jedoch noch eine Alternativlösung (in manchen Fällen einfach oder sogar entscheidend) – die sog. sqlite3_exec Funktion:

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.

Ihr primäres Ziel ist die Ausführung der Anfrage in einem einzigen Aufruf ohne Erzeugung der dreistufigen Konstrukte.

Fügen wir diesen Aufruf unserem Connector hinzu:

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

Die so entstandene Methode ist kinderleicht zu verwenden Sie können nun z.B. die Befehle "Löschen der Tabelle" (DROP TABLE) oder "Datenbankverdichtung" (VACUUM) folgendermaßen ausführen:

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

sql3.Exec("VACUUM");


Transaktionen

Nehmen wir jetzt mal an, wir müssen die Tabelle um mehrere tausend Zeilen ergänzen. Wenn wir all das in die Schleife einfügen:

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

dann ist die Ausführung sehr langsam (mehr als 10(!) Sekunden). Daher empfiehlt sich diese Implementierung für SQLite nicht. Die beste Lösung hier ist, mit Transaktionen zu arbeiten: alle SQL-Aussagen werde in eine gemeinsame Liste eingetragen und dann als eine einzige Anfrage übertragen.

Zum Schreiben des Beginns und Endes der Transaktion werden die folgenden SQL-Aussagen verwendet:

BEGIN
...
COMMIT

Der komplette Inhalt wird bei der letzten COMMIT Aussage ausgeführt. Die ROLLBACK-Aussage wird verwendet, sollte die Schleife unterbrochen werden oder wenn bereits hinzugefügte Aussagen nicht ausgeführt werden sollen.

Als Beispiel sind alle Abschlüsse des Kontos nun dieser Tabelle hinzugefügt.

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

Nachdem da Script auf das Konto angewendet wird, fügt es die Abschlüsse des Kontos unverzüglich in die Tabelle ein.

Im Logbuch des Terminal wird die entsprechende Statistik angezeigt

Spielen Sie ruhig mit dem Script herum und versehen die Zeilen, die BEGIN, ROLLBACK und COMMIT enthalten, mit Kommentaren. Wenn auf Ihren Konto mehr als hundert Abschlüsse liegen, dann werden Sie sofort den Unterschied sehen. Übrigens haben einige Tests ergeben, dass SQLite-Transaktionen schneller in MySQL oder PostgreSQL arbeiten.


3. Erstellen der 64-Bit Version (sqlite3_64.dll)

  1. Laden Sie den SQLite Quellecode (Zusammenfügung) herunter und finden die sqlite3.c-Datei.
  2. Laden Sie sqlite-dll-win32 herunter und extrahieren daraus die sqlite3.dll-Datei.
  3. Führen Sie den LIB.EXE /DEF:sqlite3.def Konsolenbefehl in dem Ordner aus, in den die dll-Datei extrahiert wurde. Vergewissern Sie sich, dass die Pfade für die lib.exe-Datei in der Systemvariable PFAD angeben sind oder finden Sie sie in Ihrem Visuellen Studio.
  4. Legen Sie das DLL-Projekt an und wählen dabei die Ausgabekonfiguration für 64-Bit Plattformen aus.
  5. Ergänzen Sie das Projekt mit den heruntergeladenen sqlite3.c und erhaltenen sqlite3.def Dateien. Sollte der Compiler einige Funktionen der def-Datei nicht akzeptieren, versehen Sie sie einfach mit Kommentaren.
  6. In den Einstellungen für das Projekt sollten die folgenden Parameter eingerichtet werden:
    C/C++ --> Allgemein --> Format Debug-Information = Programm-Datenbank (/Zi)
    C/C++ --> Vorerstellte Kopfzeilen --> Vorerstellte Kopfzeile Erzeugen/Anwenden = Vorerstellte Kopfzeilen Nicht Anwenden (/Yu)
  7. Erstellen und die 64 bit dll-Datei bekommen.


Fazit

Ich hoffe, dieser Beitrag wird zu Ihrem ständigen Begleiter auf Ihrem Weg, SQLite komplett zu beherrschen. Vielleicht verwenden Sie ihn ja auch in Ihren zukünftigen Projekten. Dieser kurze Überblick hat einige Einblicke in die Funktionalitäten von SQLite als perfekte und zuverlässige Lösung für Anwendungen gegeben.

Ich habe in diesem Beitrag alle Situationen beschreiben, auf die Sie im Umgang mit Handelsdaten stoßen könnten. Als Hausaufgabe für Sie, empfehle ich Ihnen einen einfachen Sammler von Kursschwankungen zu entwickeln, der für jedes Kürzel die Kursschwankungen in die Tabelle einträgt. In den Anhängen hier unten finden Sie den Quellcode der Klassen-Library und die Test-Scripts.

Viel Spaß und tolle Gewinne!