English Русский Español Deutsch 日本語 Português 한국어 Français Italiano Türkçe
SQL 与 MQL5: 与 SQLite 数据库集成

SQL 与 MQL5: 与 SQLite 数据库集成

MetaTrader 5示例 | 25 六月 2014, 09:02
3 965 1
---
---

小。快。灵活。
选择所有三个。

 

介绍

许多开发者考虑在他们的项目中使用数据库,目的是进行数据存储,但他们还在对此犹豫,知道安装 SQL 服务器需要多少额外时间。然而对程序员来讲这不是很难 (如果数据库管理系统 (DBMS) 已经因其它目的而安装), 它对一般用户确实是个问题,以至最终对此安装灰心。

许多开发者认识到这样做会导致他们的工作仅有少数人使用,所以他们选择不处理 DBMS。结果, 他们转而使用文件 (经常会处理多于一个文件, 使用多种数据变种): CSV, 较少的 XML 或 JSON, 或限定结构的尺寸的二进制文件, 等等。

然而,事实证明,有一个极好的 SQL 服务器替代者!而你甚至不需要安装额外的软件,因为您的项目一切都在本地完成,同时还允许您使用 SQL 的强大功能。我们来讨论 SQLite。

本文目的是令您快速开始应用 SQLite。因此我不再罗嗦参数集和函数标志等细微之处,而是创建一个轻量连接包装来执行 SQL 命令,并演示它的使用。

继续阅读本文,您需要:

  • 有一个好心情 ;)
  • 展开文章附件中的存档文件至 MetaTrader 5 客户端文件夹
  • 安装任何方便的 SQLite 的观察器 (如 SQLiteStudio)
  • 加入官方文档 SQLite http://www.sqlite.org 至收藏

目录

1. SQLite 原理
2. SQLite3 API
    2.1. 打开和关闭数据库
    2.2. 执行 SQL 查询
    2.3. 从表中获取数据
    2.4. 通过绑定写参数数据
    2.5. 事务 / 多行插入 (创建交易账户成交表例子)
3. 编译 64-位版本 (sqlite3_64.dll)


1. SQLite 原理

SQLite 是一款关系型数据库,其关键特征是无需安装本地 SQL 服务器。您的应用看上去像服务器。与 SQLite 数据库集成,基本上就是与一个文件打交道 (在磁盘上或 在内存中)。所有数据可以被存档或是移动到另外的电脑上而无需以任何特别方式安装它们。

协同 SQLite,开发者和用户可以从若干难以否认的优势中获益:

  • 无需安装附加软件;
  • 数据存储在本地文件中, 因此而管理透明, 即您可以独立于您的应用来观看并编辑它们;
  • 有能力导入和导出数据表至其它关系型数据库;
  • 代码使用熟悉的SQL查询,它允许您强制应用程序在任何时候与其他DBMS一起工作。

有三种方式与 SQLite 工作:

  1. 您可以使用含有完整 API 函数集的 DLL 文件;
  2. 您可以使用脚本命令来执行 EXE 文件;
  3. 您可以编译您的包含 SQLite API 源码的项目。

在本文中, 我将描述第一个选项, 用最习惯的 MQL5。

 

2. SQLite3 API

此连接器操作需要使用以下 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

您还需要底层 msvcrt.dll 函数来与指针工作:

strlen
strcpy
memcpy

因为我正在创建的连接器需要支持 32 位和64 位的客户端,考虑传递到 API 函数的指针尺寸就十分重要。让我们分离它们的名称:

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

如果有必要, 所有 API 函数将被针对 32 和 64 位重载指针。请注意所有连接器指针将是 64 位的。它们将在重载 API 函数中被直接转换到 32 位。此 API 函数导入的源代码在 SQLite3Import.mqh 中提供。


SQLite 数据类型

在 SQLite 版本 3 中有五种数据类型

类型
描述
NULL 空值。
INTEGER 整数值 1, 2, 3, 4, 6, 或 8 字节 (根据存储值的大小)。
REAL 8-字节实数。
TEXT 文本字符串。结尾字符为 \0 使用 UTF-8 或 UTF-16 编码存储。
BLOB 任意的二进制数据


当使用 SQL 查询创建一个表时,字段的数据类型您还可以使用其它类型名称指定, 如在其它多种 DBMS 中接受的 BIGINT 或 INT。在此情况下, SQLite 将转换它们至内在的类型,如 INTEGER。有关数据类型及其相互关系的进一步信息,请阅读文档 http://www.sqlite.org/datatype3.html


2.1. 打开与关闭数据库

正如您已经知道的, 在 SQLite3 中一个数据库就是一个正常文件。所以打开一个数据库事实上等同于打开一个文件并获取它的句柄。

它可以使用 sqlite3_open 函数完成:

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

filename [in]  - 路径,或当前位置打开的文件名。ppDb     [out] -保存文件句柄的变量。若成功函数返回 SQLITE_OK,或一个错误代码。

关闭一个数据库文件使用 sqlite3_close 函数:

int sqlite3_close(sqlite3_p64 ppDb);

ppDb [in] - 文件句柄

若成功函数返回 SQLITE_OK,或一个错误代码。


让我们在当前连接器中创建数据库打开和关闭函数。

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

此连接器现在可以打开和关闭一个数据库。现在通过一个简单脚本来检查它的性能:

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

在调试模式中运行此脚本, 做一次深呼吸并检查每个字符串的操作结果。作为结果, 一个数据库文件将出现在 MetaTrader 5 客户端安装文件夹中。祝贺您自己的成功并进入下一阶段。


2.2. 执行 SQL 查询

任何在 SQLite3 中的 SQL 查询都要经过三个阶段:

  1. sqlite3_prepare - 验证并接受语句列表;
  2. sqlite3_step - 执行这些语句;
  3. sqlite3_finalize - 完成并清理内存。

这种结构主要适用于创建或删除表,以及写非二进制数据,亦即,在那些 SQL 查询除了执行状态,并不返回其它数据时。

如果查询涉及接收数据或写二进制数据, sqlite3_column_ххsqlite3_bind_хх 函数分别用在第二个阶段。这些函数的详细描述在下一段。

让我们编写 CSQLite3Base::Query 方法来执行一个简单的 SQL 查询:

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
int CSQLite3Base::Query(string query)
  {
//--- check connection
   if(!IsConnected())
      if(!Reconnect())
         return(SQLITE_ERROR);
//--- check query string
   if(StringLen(query)<=0)
      return(SQLITE_DONE);
   sqlite3_stmt_p64 stmt=0; // variable for pointer
//--- get pointer
   PTR64 pstmt=::memcpy(stmt,stmt,0);
   uchar str[];
   StringToCharArray(query,str);
//--- prepare statement and check result
   int res=::sqlite3_prepare(m_db,str,-1,pstmt,NULL);
   if(res!=SQLITE_OK)
      return(res);
//--- execute
   res=::sqlite3_step(pstmt);
//--- clean
   ::sqlite3_finalize(pstmt);
//--- return result
   return(res);
  }

如您所见, sqlite3_prepare, sqlite3_stepsqlite3_finalize 函数是一个接一个的执行。

考虑在 SQLite 中对数据表执行 CSQLite3Base::Query :

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

在命令执行后, 数据表出现在数据库:

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

在执行这些命令后, 我们收到数据表的名称以及一个附加字段:

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

以下条目会在新数据行加入和修改后出现在数据表中:

最后, 以下命令将会在其它命令执行后清理数据库。

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

在移步到下一段之前, 我们需要接受错误描述的方法。从我自己的经验我可以说,在 SQL 查询中提供的错误码可以提供很丰富的信息,但错误描述对于检测和修复则过于简单。

const PTR64 sqlite3_errmsg(sqlite3_p64 db);

db [in] - handle received by function sqlite3_open

指针返回的字符串包含错误描述。

在连接器中, 我们应该加入从指针接收字符串的方法,这要使用 strcpy 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. 从表中获取数据

正如我已经提到的,在 2.2 段的开始, 数据读取要执行 sqlite3_column_хх 函数。示意如下:

  1. sqlite3_prepare
  2. sqlite3_column_count - 找到所得到的表中的列的数目
  3. 循环,当步长结果 sqlite3_step == SQLITE_ROW
    1. sqlite3_column_хх - 读字符串单元
  4. sqlite3_finalize

由于我们正在接近广泛的有关数据读写部分,这是一个很好的时机来描述整个数据交换中使用的三个容器类。必要的数据模型依赖于如何将数据存储在数据库中:

数据库
|
是数据行数组。
|
是单元数组。
|
单元 是一个任意长度的字节缓冲区。


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

如您所见, CSQLite3Row CSQLite3Table 连接是元数据 - 这些都是传统的数据数组。CSQLite3Cell 单元类也有 uchar 数组 + 数据类型域。字节数组在 CByteImage 类中实现 (类似著名的 CFastFile)。

我已经创建了以下枚举,以方便连接器的操作和管理单元中的数据类型:

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

注意这个 CT_UNDEF 类型已经被加到五个基础 SQLite3 类型中,来标识出示单元状态。完整的 INTEGER 分为 CT_INTCT_INT64,同理划分 sqlite3_bind_intXXsqlite3_column_intXX 函数。

获取数据

为了从单元中得到数据, 我们将创建方法生成 sqlite3_column_хх 类型函数。它将检查数据类型和尺寸并将之写入 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);
  }

这个函数十分巨大, 但它仅从当前语句中读数据并保存在一个单元中。

我们也重载 CSQLite3Base::Query 函数,添加 CSQLite3Table 容器表来接收数据作为第一个参数。

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

我们已经有了全部接收数据所必需的函数。让我们通过例子:

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

在客户端上打印出查询结果,使用以下命令 Print(TablePrint(tbl))。我们将在日志中看到以下条目 (顺序是从下到上):

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

查询结果的打印输出采用相同方式 Print(TablePrint(tbl))。我们可以看到存在的数据表:

正如从例子中看到的那样, 查询执行结果放在 tbl 变量中。在这之后, 您可以容易地获取并处理它们。


2.4. 写参数数据与绑定

另一个对初学者很重要的话题就是写 "不方便的" 格式的数据到数据库。当然, 此处我们的意思是指二进制数据。它不能被直接传递到通用的 INSERT 或 UPDATE 文本语句中, 当遇到第一个零时,它会被整个认作字符串。相同的问题发生在字符串里包含单引号的情况 '

后期绑定也许在有些情况下有用, 特别是当表很宽。将所有字段写入单行是困难的,并且不可靠,因为您会很容易的丢失一些东西。函数 sqlite3_bind_хх 系列对于绑定操作是必需的。

为了应用绑定, 插入模板来替代传递数据。我将考虑一种情况 - "?" 符号。换句话说, UPDATE 查询将看上去如下:

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


之后, sqlite3_bind_doublesqlite3_bind_text 函数一接一个执行来放置数据至 open_pricecomment。通常, 与 bind 函数工作可以用以下方式代表:

  1. sqlite3_prepare
  2. 调用 sqlite3_bind_хх 一个接一个并写所需数据至语句。
  3. sqlite3_step
  4. sqlite3_finalize

所有的类型数量 sqlite3_bind_xx ,都要完整重复以上描述的阅读功能。因此,您可以轻松地将它们结合在连接器 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);
  }

这个方法仅有的目的就是写传递单元缓存区至语句。

让我们用简单的方式加入 CQLite3Table::QueryBind 方法。它的第一个参数是写入的数据字符串:

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

它的目的是写字符串至适当的参数。


2.5. 事务 / 多行插入

在进入本话题之前, 您需要了解一些更多的 SQLite API 函数。在之前的段落, 我已经描述了三阶段请求处理: 准备+执行+完成。然而,还存在另一种 (在某些情况下,简单的或更严重的) 方案 – 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.

当成功时,它返回 SQLITE_OK,否则是一个错误代码。

它的主要目的是在单个单元中执行查询而无需创建三阶段构造。

让我们在连接器中加入它的调用:

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

结果方法很容易使用。例如, 您可以执行表删除 (DROP TABLE) 或数据库压缩 (VACUUM) 命令,如下方式:

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

sql3.Exec("VACUUM");


事务

现在, 假设我们不得不加入几千行数据到表中。如果我们在循环中插入所有这些:

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

执行将会很慢 (超过 10(!) 秒)。所以, 如此实现在 SQLite 中是 不建议的。在此,最合适的方案是使用 事务: 所有 SQL 语句的输入进入到通用列表,并传递到单一查询语句中。

以下 SQL 语句用于写事务开始和结束:

BEGIN
...
COMMIT

所有内容在最后的 COMMIT 语句执行。ROLLBACK 语句用于中断循环或停止已经加入的语句执行。

举例如下, 所有账户成交加入表。

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

当脚本在账户应用后, 它立即在表中插入账户成交记录。

在客户端日志中显示统计

您可以随意试验此脚本: 注释掉包含 BEGIN, ROLLBACK COMMIT 的语句。如果您的账户里有超过数百的成交记录, 您将立即看到差异。顺便说, 根据 一些 测试, SQLite 事务的工作速度比 MySQL 或 PostgreSQL 还快。


3. 编译 64-位版本 (sqlite3_64.dll)

  1. 下载 SQLite 源码 (amalgamation) 并查找 sqlite3.c 文件。
  2. 下载 sqlite-dll-win32 并从中展开 sqlite3.dll 文件。
  3. 在展开的 dll 文件夹里执行 LIB.EXE /DEF:sqlite3.def 控制台命令。确认 lib.exe 文件的路径已经设置在 PATH 系统环境变量里,或在您的 Visual Studio 中找到它。
  4. 创建 DLL 项目,选择针对 64 为平台的发行配置。
  5. 加入下载的 sqlite3.c 以及获取的 sqlite3.def 文件到项目中。如果编译器不能接受一些来自 def 文件的函数, 就注释掉它们。
  6. 以下参数要在项目设置中设置:
    C/C++ --> 常用 --> 调试信息格式 = 程序数据库 (/Zi)
    C/C++ --> 预编译头文件 --> 创建/使用预编译头文件 = 不使用预编译头文件 (/Yu)
  7. 编译并取得 64 位 dll。


结论

我希望本文能成为您掌握 SQLite 的不可缺少的指南。也许, 您会在将来的项目中用到它。这个简短的概述提供了一些对于 SQLite 功能的洞察,可作为完美并可靠的解决方案。

在本文中,我已经描述了所有当您处理交易数据时可能面对的情况。作为一门功课,我建议您开发一个简单的即时价格收集器,插入每个品种的即时价格到表中。您可以在下面的附件中找到类库源码以及测试脚本。

我希望您有好运,并且获利丰厚!

本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/862

附加的文件 |
MQL5.zip (790.12 KB)
最近评论 | 前往讨论 (1)
benqzyp
benqzyp | 26 3月 2022 在 11:46
ByteImg.mqh运行出错,请修正
自动交易的传说: 是少或多? 自动交易的传说: 是少或多?
两年前,在 "The Last Crusade - 最新的改革" 我们回顾了一个相当有趣但目前没有被广泛使用的显示市场信息的方法 - 点数图。现在,我建议您尝试写一个基于检测点数图范式的自动交易。
在 MetaTrader 5 中的连续期货合约 在 MetaTrader 5 中的连续期货合约
期货合约的生存跨度较短,它们的技术分析更复杂。对于短周期图表,技术分析是十分困难的。例如,UX-9.13 乌克兰股指期货的日线图上,有超过 100 根柱线。所以, 交易员会创建组合的长线期货合约。这篇文章解释了如何在 MetaTrader 5 客户端中拼接期货合约。
如何实现交易员的订单,并在 MQL5 自由职业者服务板块创造利润 如何实现交易员的订单,并在 MQL5 自由职业者服务板块创造利润
MQL5 自由职业者板块是一个在线服务,开发者可以从交易员发布的创建交易应用程序订单中获取报酬。现在, 交易者理解了 MQL5.com 网站上的所有服务的不同之处: 一款准备就绪的交易机器人可以从 MetaTrader 的市场中买到, 而基于用户定义的特殊策略的 EA, 可以在自由职业者板块定购。经验丰富的开发者争夺交易者的订单的实现,使交易者可以选择一个最佳时间和成本条件的候选者。
用于 Renko 制图的指标 用于 Renko 制图的指标
本篇举例描述一个用 MQL5 实现的 Renko 制图指标。这个指标的修改,将它从经典图表中区分开来。它在指标窗口或主窗口两者中都可以构建。此外,此处还有 ZigZag 指标。您可以找到一些图表实现的例子。