MQL5 中的数据库操作原理

数据库以表的形式存储信息。获取、修改和向其中添加新数据都是使用 SQL 语言查询完成的。我们将在以下章节中描述其具体细节。与此同时,我们借助与交易无关的 DatabaseRead.mq5 脚本,了解如何创建一个简单的数据库并从中获取信息。这里提到的所有函数稍后都会详细描述。现在一定要想象一下总体原则。

使用内置的 DatabaseOpen/DatabaseClose 函数创建和关闭数据库,类似于处理文件,因为我们也为数据库创建一个描述符,检查数据库,并在最后将其关闭。

void OnStart()
{
   string filename = "company.sqlite";
   // create or open a database
   int db = DatabaseOpen(filenameDATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE);
   if(db == INVALID_HANDLE)
   {
      Print("DB: "filename" open failed with code "_LastError);
      return;
   }
   ...// further work with the database
   // close the database
   DatabaseClose(db);
}

打开数据库后,我们将确保其中没有以我们所需名称命名的表。如果表已存在,那么当尝试像我们的示例中那样向其插入相同数据时,将会发生错误,因此我们使用 DatabaseTableExists 函数。

删除和创建表是通过查询完成的,这些查询通过两次调用 DatabaseExecute 函数发送到数据库,并伴有错误检查。

   ...
   // if the table COMPANY exists, then delete it
   if(DatabaseTableExists(db"COMPANY"))
   {
      if(!DatabaseExecute(db"DROP TABLE COMPANY"))
      {
         Print("Failed to drop table COMPANY with code "_LastError);
         DatabaseClose(db);
         return;
      }
   }
   // creating table COMPANY 
   if(!DatabaseExecute(db"CREATE TABLE COMPANY("
     "ID      INT     PRIMARY KEY NOT NULL,"
     "NAME    TEXT    NOT NULL,"
     "AGE     INT     NOT NULL,"
     "ADDRESS CHAR(50),"
     "SALARY  REAL );"))
   {
      Print("DB: "filename" create table failed with code "_LastError);
      DatabaseClose(db);
      return;
   }
   ...

我们来阐述一下 SQL 查询的本质。在 COMPANY 表中,我们只有 5 个字段:记录 ID、姓名、年龄、地址和薪水。这里的 ID 字段是一个键,即唯一索引。索引能够唯一标识每条记录,并可跨表用于将它们链接在一起。这类似于仓位 ID 链接属于特定仓位的所有交易和订单。

现在你需要用数据填充表,这是通过 "INSERT" 查询完成的:

   // insert data into table
   if(!DatabaseExecute(db,
      "INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY) VALUES (1,'Paul',32,'California',25000.00); "
      "INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY) VALUES (2,'Allen',25,'Texas',15000.00); "
      "INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY) VALUES (3,'Teddy',23,'Norway',20000.00);"
      "INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY) VALUES (4,'Mark',25,'Rich-Mond',65000.00);"))
   {
      Print("DB: "filename" insert failed with code "_LastError);
      DatabaseClose(db);
      return;
   }
   ...

在这里,向 COMPANY 表添加了 4 条记录,每条记录都有一个字段列表,并指明了将写入这些字段的值。记录是通过单独的 "INSERT..." 查询插入的,这些查询通过特殊的分隔符 ';' 组合成一行,但我们也可以通过单独的 DatabaseExecute 调用将每条记录插入表中。

由于脚本末尾数据库将保存到 "company.sqlite" 文件中,下次运行时,我们会尝试使用相同的 ID 将相同的数据写入 COMPANY 表。这将导致出现错误,因此我们之前删除了表,以便每次运行脚本时都从头开始。

现在,我们从 COMPANY 表中获取 SALARY > 15000 字段的所有记录。这是通过 DatabasePrepare 函数完成的,该函数会“编译”请求文本并返回其句柄,以供后续在 DatabaseReadDatabaseReadBind 函数的字符串参数传递给 SQLite 执行引擎。

   // prepare a request with a descriptor
   int request = DatabasePrepare(db"SELECT * FROM COMPANY WHERE SALARY>15000");
   if(request == INVALID_HANDLE)
   {
      Print("DB: "filename" request failed with code "_LastError);
      DatabaseClose(db);
      return;
   }
   ...

成功创建请求后,我们需要获取其执行结果。这可以使用 DatabaseRead 函数完成,该函数在第一次调用时将执行查询并跳转到结果中的第一条记录。在每次后续调用时,它将读取下一条记录,直到到达末尾。在这种情况下,它将返回 false,表示“没有更多记录了”。

   // printing all records with salary over 15000
   int idage;
   string nameaddress;
   double salary;
   Print("Persons with salary > 15000:");
   for(int i = 0DatabaseRead(request); i++)
   {
      // read the values of each field from the received record by its number
      if(DatabaseColumnInteger(request0id) && DatabaseColumnText(request1name) &&
         DatabaseColumnInteger(request2age) && DatabaseColumnText(request3address) &&
         DatabaseColumnDouble(request4salary))
         Print(i":  "id" "name" "age" "address" "salary);
      else
      {
         Print(i": DatabaseRead() failed with code "_LastError);
         DatabaseFinalize(request);
         DatabaseClose(db);
         return;
      }
   }
   // deleting handle after use
   DatabaseFinalize(request);

执行结果将是:

Persons with salary > 15000:
0:  1 Paul 32 California 25000.0
1:  3 Teddy 23 Norway 20000.0
2:  4 Mark 25 Rich-Mond  65000.0

DatabaseRead 函数允许遍历查询结果中的所有记录,然后通过 DatabaseColumn 函数获取结果表中每一列的完整信息。这些函数旨在以通用方式处理任何查询的结果,但代价是代码冗余。

如果预先知道查询结果的结构,最好使用 DatabaseReadBind 函数,它允许一次将整个记录读入一个结构体中。我们可以用这种方式重制前面的示例,并以新名称 DatabaseReadBind.mq5 呈现它。首先,我们声明 Person 结构体:

struct Person
{
   int    id;
   string name;
   int    age;
   string address;
   double salary;
};

然后,只要函数返回 true,我们就在循环中用 DatabaseReadBind(request, person) 从查询结果中减去(读取)每条记录:

   Person person;
   Print("Persons with salary > 15000:");
   for(int i = 0DatabaseReadBind(requestperson); i++)
      Print(i":  "person.id" "person.name" "person.age,
         " "person.address" "person.salary);
   DatabaseFinalize(request);

因此,我们立即从当前记录中获取所有字段的值,而无需单独读取它们。

这个入门示例摘录自文章 SQLite:在 MQL5 中本地处理 SQL 数据库,其中除了这个示例外,还考虑了交易者应用数据库的几种方案。具体来说,你可以在那里找到从交易中恢复仓位历史、从策略、交易品种或最优选交易时段等方面分析交易报告,以及处理优化结果的技术。

掌握这些材料可能需要一些 SQL 基础知识,因此我们将在后续章节中简要介绍。