市场模拟:MQL5 中的 SQL 入门(五)
引言
大家好,欢迎阅读本系列关于如何创建复制/模拟系统的又一篇文章。
在上一篇文章 “市场模拟:MQL5 中的 SQL 入门(四)” 中,我们介绍了一个在 MetaTrader 5 中使用 SQLite 时应当注意的小细节。尽管这一行为在未来可能会有所改变,但作为预防措施,您在 MetaTrader 5 环境中使用 SQLite 之前,应始终先进行测试。总之,我们在文中展示了如何添加查询机制,以便在 MQL5 代码中充分利用 SQL,并使用 SQL SELECT 查询来检索结果。
但是我们还需要讨论和实现最后一个函数,即 DatabaseReadBind 函数。由于要正确理解这个函数需要稍作详细解释,因此决定不在上一篇文章中介绍,而是在今天的文章中详述。这个话题内容较多,下面我们直接进入下一部分。
实现读取查询结果的函数
实现负责读取 SQL 查询返回数据的函数相当简单,但有一个细节绝不能忘记。读取函数返回的结果正是上一次 ExecRequestOfData 调用执行的结果。因此,在读取返回的数据之前,必须始终先调用 ExecRequestOfData。
不过,这里我们需要先做一个简短的插述。为了更好地理解这一点,我们首先需要查看一下类代码。完整的 C_DB_SQL 类代码如下所示。请记住,此代码是为使用 MetaTrader 5 内置的 SQL 支持而设计的。对于其他 SQL 使用场景,此代码需要进行修改以添加所需的功能。
001. //+------------------------------------------------------------------+ 002. #property copyright "Daniel Jose" 003. //+------------------------------------------------------------------+ 004. #include "..\Service Graphics\Support\C_Array.mqh" 005. //+------------------------------------------------------------------+ 006. class C_DB_SQL 007. { 008. private : 009. C_Array m_Arr; 010. int m_handleDB, 011. m_Request; 012. //+------------------------------------------------------------------+ 013. void Convert(const char &buff[], const int size) 014. { 015. string sz0 = ""; 016. bool b0, b1, bs1, bs2, bc0, bc1, bc; 017. int nLine = 1; 018. 019. b0 = b1 = bs1 = bs2 = bc0 = bc1 = bc = false; 020. for (int count = 0, nC0 = nLine; count < size; count++) 021. { 022. switch (buff[count]) 023. { 024. case '\t': 025. sz0 += (bs1 || bs2 ? "\t" : ""); 026. break; 027. case '\n': 028. nC0++; 029. case '\r': 030. bc0 = false; 031. break; 032. case ';': 033. b0 = (bs1 || bs2 || bc0 || bc1 ? b0 : true); 034. default: 035. switch (buff[count]) 036. { 037. case '"': 038. bs1 = (bs2 || bc0 || bc1 ? bs1 : !bs1); 039. break; 040. case '\'': 041. bs2 = (bs1 || bc0 || bc1 ? bs2 : !bs2); 042. break; 043. } 044. if (((count + 1) < size) && (!bs1) && (!bs2)) 045. { 046. if (bc = ((buff[count] == '-') && (buff[count + 1] == '-'))) bc0 = true; 047. if (bc = ((buff[count] == '/') && (buff[count + 1] == '*'))) bc1 = true; 048. if (bc = ((buff[count] == '*') && (buff[count + 1] == '/'))) bc1 = false; 049. if (bc) 050. { 051. count += 1; 052. bc = false; 053. continue; 054. } 055. } 056. if (!(bc0 || bc1)) 057. { 058. if ((!b1) && (buff[count] > ' ')) 059. { 060. b1 = true; 061. nLine = nC0; 062. } 063. sz0 += (b1 ? StringFormat("%c", buff[count]) : ""); 064. } 065. } 066. if (b0) 067. { 068. m_Arr.Add(sz0, nLine); 069. sz0 = ""; 070. b0 = b1 = false; 071. } 072. } 073. } 074. //+------------------------------------------------------------------+ 075. const string ExecSQL(void) 076. { 077. string szCmd; 078. 079. for (int count = 0, nLine; count >= 0; count++) 080. { 081. szCmd = m_Arr.At(count, nLine); 082. if (nLine < 0) break; 083. if (!ExecCommandSQL(szCmd)) 084. return StringFormat("Execution of line %d of the SQL script failed...", nLine); 085. } 086. 087. return NULL; 088. } 089. //+------------------------------------------------------------------+ 090. public : 091. //+------------------------------------------------------------------+ 092. C_DB_SQL(const string szFileName = ":memory:") 093. { 094. m_handleDB = DatabaseOpen(szFileName, DATABASE_OPEN_CREATE | DATABASE_OPEN_READWRITE); 095. m_Request = INVALID_HANDLE; 096. } 097. //+------------------------------------------------------------------+ 098. ~C_DB_SQL() 099. { 100. DatabaseClose(m_handleDB); 101. } 102. //+------------------------------------------------------------------+ 103. const string ExecResourceSQL(const string szResource) 104. { 105. char buff[]; 106. int size; 107. 108. ArrayResize(buff, size = StringLen(szResource)); 109. StringToCharArray(szResource, buff); 110. Convert(buff, size); 111. ArrayFree(buff); 112. 113. return ExecSQL(); 114. } 115. //+------------------------------------------------------------------+ 116. const string ExecScriptSQL(const string szFileName) 117. { 118. int file, size; 119. char buff[]; 120. 121. if ((file = FileOpen(szFileName, FILE_READ | FILE_BIN)) == INVALID_HANDLE) 122. return StringFormat("Unable to open script file: %s", szFileName); 123. ArrayResize(buff, size = (int) FileSize(file)); 124. FileReadArray(file, buff); 125. FileClose(file); 126. Convert(buff, size); 127. ArrayFree(buff); 128. 129. return ExecSQL(); 130. } 131. //+------------------------------------------------------------------+ 132. bool ExecCommandSQL(const string szCmd) 133. { 134. return (m_handleDB == INVALID_HANDLE ? false : DatabaseExecute(m_handleDB, szCmd)); 135. } 136. //+------------------------------------------------------------------+ 137. bool ExecRequestOfData(const string szCmd) 138. { 139. if (m_Request != INVALID_HANDLE) DatabaseFinalize(m_Request); 140. return ((m_Request = DatabasePrepare(m_handleDB, szCmd)) != INVALID_HANDLE); 141. } 142. //+------------------------------------------------------------------+ 143. template <typename T> bool GetRegisterOfRequest(T &stObject, const bool Finish = true) 144. { 145. if (!DatabaseReadBind(m_Request, stObject)) 146. { 147. if (Finish) 148. { 149. DatabaseFinalize(m_Request); 150. m_Request = INVALID_HANDLE; 151. } 152. return false; 153. } 154. 155. return true; 156. } 157. //+------------------------------------------------------------------+ 158. }; 159. //+------------------------------------------------------------------+
C_DB_SQL.mqh 代码
这就是完整的类代码。但由于只有一小部分尚未讨论,或者更准确地说,之前尚未展示过,因此我们不会检查其他文章中已经涉及的函数或要点。在此,我们将仅关注第 143 行的函数,即 GetRegisterOfRequest 函数。第一个也是最合理的问题是:为什么这个函数是以这种方式声明的?仅使用 MQL5 语言很难解释这一点。
为了详细理解这一点,我们需要解释一个存在于 C/C++ 中的非常复杂的概念。这个概念与泛型对象引用有关,在 C/C++ 中,它与通常与空指针一起解释的机制有关。是的,在 C/C++ 中,我们可以使用泛型引用和类似指针的机制,而不仅仅是许多人想象的具体类型。但是,由于在一篇文章中解释这个概念极其困难,亲爱的读者,我们将尽可能避免讨论 C/C++,而是试图向你展示为什么这个函数是以这种方式声明的。
首先,我们要面对的是类型依赖的问题。这个问题并非出现在第 143 行的函数声明中,而是出现在 DatabaseReadBind 函数中。这种依赖性迫使我们使用特定的数据类型,以避免在尝试编译代码时,MQL5 编译器报出一连串错误。许多程序员在打算使用 DatabaseReadBind 或尝试使用时,并没有像我们在这里一样将其封装在一个调用中。原因正是他们不了解此处产生的类型依赖。
那么,他们通常是怎么做的呢?他们会创建一个程序或函数来执行查询,分析 SQL 返回的结果,最后以常规方式将值返回给调用者。这种方法在非常特定的场景下是有效的,并且相当适用。在其他情况下,程序员只需在需要时直接在主代码中使用 DatabaseReadBind。
但是他们为何采取这种方式呢?再次强调,原因在于类型约束。在 MQL5中,处理起来并不那么容易。然而,当使用 C/C++时,情况会有所改变,因为 C/C++ 提供了处理通用对象引用的低级机制,包括空指针。我想你可能还没有完全理解这一点。那么,让我们来看看在 MQL5 中是如何声明 DatabaseReadBind 函数的。您可以在下面看到:
bool DatabaseReadBind( int request, // handle of the query created in DatabasePrepare void& struct_object // reference to the structure for reading the record );
我们在 MQL5 函数文档中可以找到相关内容。您可能对此不太清楚,但对于 C/C++ 程序员来说,这种声明是完全合理的。在不深入 C/C++ 细节的情况下,其基本思想如下:该函数必须接受两个参数。第一个参数是一个整数值,表示 DatabasePrepare 返回的句柄。我们在上一篇文章中已经讨论过这一点。然而,这里的关键角色是第二个参数。第二个参数的类型为 void,这意味着该函数接受一个对象的引用,而无需在签名中明确指定具体类型。
这里传递的并不是通常所说的“地址”本身,而是对内存中对象的引用。这是因为指针通常指向内存中的某个地址。然而,当我们在一个名称前使用与号(&)时,我们表示我们将引用一个变量,而不是一个位置。如果你真的想理解这一点,可以尝试学习如何在 C/C++ 中使用指针,因为这一主题在专门教授 C/C++ 的书籍中往往占据数章篇幅。这是一个相当复杂且容易让人困惑的主题。
由于 MQL5 不允许我们像 C/C++ 那样使用指针,我们需要一个小技巧,才能像 C_DB_SQL 类代码中那样正确封装并调用 DatabaseReadBind 函数。这个技巧正是使用模板类型机制,让编译器推断出适当的类型。乍一看,这可能看起来非常复杂且难以解决。幸运的是,MQL5 使我们能够使用 C/C++ 中也存在的东西,即声明:
template <typename T >
这里有一个注意事项。T 可以是任何类型,但在编程中我们通常按照约定使用 T。关键在于,这个声明放在函数名之前,告诉编译器必须执行正确的类型转换来解决类型问题。现在请注意以下几点:GetRegisterOfRequest 的第一个参数正是 T。由于我们不能使用 void 类型,这个声明实际上让我们能够泛化参数类型,而不是像低级 C/C++ 模式那样依赖于 void。
“好吧。但我仍然无法理解这种逻辑。在研究了 MQL5 文档中关于 DatabaseReadBind 的部分后,我们发现它指出第二个参数必须是一个结构。即使在演示此函数用法的示例中,第二个参数也是使用一个结构。因此,我无法理解为什么使用 template <typename T > 会如此复杂,以及第 143 行这个奇怪的声明。为什么不直接将结构体传递给这个函数呢?为什么要搞得这么复杂?”
亲爱的读者,我完全同意您的观点。你是对的。我们可以直接将结构体作为函数参数传递,这样就能大大简化第 143 行的声明。但现在我要问你们:这个结构应该有多少个字段,这些字段应该是什么类型?如果你能以一种允许逻辑普遍化的方式来回答这个问题,我肯定会以不同的方式实现第 143 行的调用。
但是,这种结构永远无法以一种能够泛化所有情况的方式构建。这是因为,即使我们知道数据库中字段的数量,这个数量迟早也会发生变化。如果发生这种情况,MQL5 代码中的整个实现都必须重新设计。因此,许多人在使用 DatabaseReadBind 时,采用的方式与此处所示有所不同。
我们展示的这个函数是一个通用函数,其目的是将SQL返回的结果返回给我们的 MQL5 代码。你很快就会明白为什么了。总之,在第 145 行,我们将进行调用,根据返回值,我们将面临两种完全不同的情况。第一种情况是 DatabaseReadBind成功 读取了 SQL 返回的一些记录。在这种情况下,将执行第 155 行。但也可能出现这种情况:这个块中已经没有剩余的记录了。那么我们将面临两种不同的情况。然而,这两种情况都是在 DatabaseReadBind 返回 false 时触发的分支中处理的。
请注意,默认情况下,GetRegisterOfRequest 函数的第二个参数的值为 true。当第 145 行的检查失败时,我们会在第 147 行检查第二个参数是否为 true。如果调用者未对其进行更改,则在第 149 行调用 DatabaseFinalize 函数。最后,在第 150 行,我们会更改 m_Request 变量的值,表示没有更多可用记录,需要向数据库发起新的查询。但是,如果调用者指定 Finish 参数为 false,则第 147 行的检查将失败,一切将保持原样。这在后续的主代码中会有用。无论如何,返回值都将是 false,这会在第 152 行进行处理。
至此,我们已完全完成了 C_DB_SQL 类的主要部分。从现在起,任何代码都可以在我们的回放和模拟系统中使用它。但在使用之前,我们需要确保一切正常工作。为此,让我们继续讨论一个新的话题,以便我们能更清晰地划分问题。
在 MetaTrader 5 中测试用于 SQLite 的 C_DB_SQL 类
我们的 C_DB_SQL 类大部分已经过测试。现在剩下的就是测试与查询相关的部分,并弄清楚执行查询时 SQL 将返回什么。
为了准确解释这一点,同时让每个人都能轻松理解,尤其是亲爱的读者你,我们将展示一段主代码,演示得到的结果,并解释发生了什么以及我们为什么会得到这样的结果。在文章的最后,我们将提供完整的代码,以便您可以在本地测试我们将要执行的操作。
处理完这部分后,接下来我们先做第一个测试。我们将使用以下代码片段来实现这一点:
18. //+------------------------------------------------------------------+ 19. void SELECT_Type_01(void) 20. { 21. struct st 22. { 23. int id; 24. string symbol; 25. }stLocal; 26. 27. Print("Executing type 1 data request..."); 28. if ((*SQL).ExecRequestOfData("SELECT * FROM tb_Symbols")) 29. { 30. Print("Request: OK..."); 31. for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal, false); c++) 32. Print(" Resq ", c, " ==> ", stLocal.id, " - ", stLocal.symbol); 33. Print("Reload..."); 34. for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal); c++) 35. Print(" Resq ", c, " ==> ", stLocal.id, " - ", stLocal.symbol); 36. }; 37. Print("Finish type 1..."); 38. } 39. //+------------------------------------------------------------------+
主文件中的片段
行号与完整代码中的行号完全一致。这意味着您可以按照自己的节奏逐段阅读。该代码片段的执行结果可以在以下动画中看到:

请注意,这非常简单明了。第 21 行我们声明了一个结构体。请记住,在这个结构中我们将有两个变量。你不应该这样看待它们。不要把它们当作变量,因为那样会让你感到困惑,并使你以后更难理解其他内容。它们应该被视为 SQL 返回的响应中的字段或列。因此,我们有一个结构体,在 SQL 结果集中应该被视为一个行。这个结果行中预期有两个字段。
在第 28 行,我们尝试使用 SQL 从数据库中查询数据。请密切注意双引号(")内的内容,因为这是 SQL 将执行的命令。你必须准确写出你想传递给 SQL 执行的内容。在这个最简单的例子中,我们只需要 tb_Symbols 表中所有的值。虽然这种创建查询的方式不适合实际使用,但对于我们的例子来说完全足够,因为我们的表很小,包含的记录很少。
如果我们的查询成功,我们将首先执行第 31 行的 for 循环。现在请注意,循环结束的原因正是没有新的返回记录。还要注意,在第 31 行的这些调用中,我们告诉 GetRegisterOfRequest 我们还不想关闭或完成当前的结果块。换句话说,当没有更多可用记录时,我们将保持当前结果集的开放和可用状态。
然后,在第 32 行,我们将返回的结果打印到终端。在第 33 行,我们在终端中指示我们将再次遍历返回的数据。请注意,我们所做的几乎与第 31 行相同。然而,这次我们不会在 GetRegisterOfRequest 调用的第二个参数中传递任何内容。因此,一旦块被完全读取,SQL 查询返回的所有内容都将被丢弃,因此我们必须执行一个新的查询。
接下来的实现方法见以下片段:
39. //+------------------------------------------------------------------+ 40. void SELECT_Type_02(void) 41. { 42. struct st 43. { 44. string of_day; 45. double price; 46. string symbol; 47. }stLocal; 48. 49. Print("Executing type 2 data request..."); 50. if ((*SQL).ExecRequestOfData("SELECT tq.of_day, tq.price, ts.symbol FROM tb_Quotes AS tq, tb_Symbols AS ts WHERE tq.fk_id = ts.id ORDER BY price DESC;")) 51. { 52. Print("Request: OK..."); 53. for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal); c++) 54. Print(" Resq ", c, " ==> ", stLocal.of_day, " { ", stLocal.price, " } ", stLocal.symbol); 55. } 56. Print("Finish type 2..."); 57. } 58. //+------------------------------------------------------------------+
主代码片段
结果可以在以下动画中看到:

请注意,这里的情况与前一个示例略有不同。首先,在第 42 行,我们声明一个具有三个字段的结构。在前一种情况下,每一行都有两个字段。但是请注意,尽管字段数量增加了,但我们仍然使用与以前非常相似的代码。但这一次,我希望你们仔细查看第 50 行使用的 SQL 命令。在这里,我们将 tb_Quotes 表和 tb_Symbols 表连接起来,以便以某种方式呈现数据库中实际存在的内容。
现在请注意 SQL 查询中声明字段的顺序。这一点很重要,因为 DatabaseReadBind 在读取 SQL 响应数据时也会遵循相同的顺序。因此,如果我们更改 SQL 命令或第 42 行结构中的字段声明的顺序,我们可能会在返回的数据中看到完全意想不到的内容。所以不要忽略这些细节。
现在我们遇到一个乍一看似乎更复杂的情况。但不要被第一印象所误导:它其实和其他事情一样简单。我们将更详细地使用 MQL5 中提供的函数,这些函数的目的是允许在 MQL5 中使用 SQL。下面可以看到片段。
58. //+------------------------------------------------------------------+ 59. void SELECT_Type_03(void) 60. { 61. struct st0 62. { 63. double price; 64. string symbol; 65. }stLocal; 66. struct st1 67. { 68. string tmp1, 69. tmp2, 70. date; 71. }std; 72. 73. Print("Executing type 3 data request..."); 74. if ((*SQL).ExecRequestOfData("SELECT tq.price, ts.symbol, tq.of_day FROM tb_Quotes AS tq, tb_Symbols AS ts WHERE tq.fk_id = ts.id ORDER BY price DESC;")) 75. { 76. Print("Request: OK..."); 77. for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal, false); c++) 78. Print(" Resq ", c, " ==> { ", stLocal.price, " } ", stLocal.symbol); 79. Print("Reload..."); 80. for (int c = 0; (*SQL).GetRegisterOfRequest(std); c++) 81. Print(" Resq ", c, " ==> < ", std.date, " > "); 82. 83. } 84. Print("Finish type 3..."); 85. } 86. //+------------------------------------------------------------------+
主代码片段
下面可以看到这段代码片段的执行结果。

到目前为止,我们使用的结构对应于一个包含两到三个字段的结果行。但正如我们在上一主题中所解释的,我们还可以做得更多,即让 SQL 返回超出我们所需数量的字段。而且,通过正确分析我们实际需要的字段,我们可以利用 SQL 已经返回的数据。这样,我们就不必创建新的查询来查找所需数据,因为这些数据已经包含在前一个查询中了。
这里有一个必须理解的重要细节。关键点在于,SQL 返回的每一行都包含对应字段的值。但是,正如本例所示,我们将执行与前一个片段相同的查询。然而,现在查询返回的字段顺序不同。这一点非常重要,请务必注意。原因在于,第 61 行声明的结构包含两个字段。第 66 行声明的结构包含三个字段。SQL 将返回三个字段。这是因为查询命令指示 SQL 返回这三个字段。
现在请注意,我们从结构 st0 切换到了结构 st1。然而,结构 st0 中缺少名为 date 的字段,因此当我们继续读取 SQL 返回的数据时,会忽略这个字段。这一情况出现在第 77 行,我们在那里创建了一个循环来读取 SQL 返回的数据并将其打印到终端。
但是,我们在第 80 行做了同样的事情。但这次我们忽略 SQL 查询返回的其余数据,只显示名为 date 的字段。这里没有什么复杂的问题。关键在于理解我们正在做什么以及 SQL 返回的结果。
你一定已经在想,我们可以为所欲为,而且无论我们采取什么行动,都会得到一个结果。然而,在现实中,这并不完全正确。要明白这一点,请看以下摘录:
086. //+------------------------------------------------------------------+ 087. void SELECT_Type_04(void) 088. { 089. struct st 090. { 091. string of_day; 092. double price; 093. string symbol; 094. }stLocal; 095. 096. Print("Executing type 4 data request..."); 097. if ((*SQL).ExecRequestOfData("SELECT tq.price, ts.symbol FROM tb_Quotes AS tq, tb_Symbols AS ts WHERE tq.fk_id = ts.id ORDER BY price DESC;")) 098. { 099. Print("Request: OK..."); 100. for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal); c++) 101. Print(" Resq ", c, " ==> ", stLocal.of_day, " { ", stLocal.price, " } ", stLocal.symbol); 102. } 103. Print("Finish type 4..."); 104. } 105. //+------------------------------------------------------------------+
主代码片段
下面显示了该片段的执行结果,乍一看似乎有些奇怪,因为我们得到了一个完全出乎意料的结果。

这对你来说可能看起来相当奇怪。但其实这并没有什么不寻常的:只需看看第 89 行的声明,我们就在那里明确指出了我们期望在 SQL 查询中接收到的字段。现在看看第 97 行,以及我们向 SQL 提出的请求。请注意,我们期望接收到的结果与我们要求 SQL 返回的信息之间并不一致。
这里出现了一个问题,迫使我们再次查阅 MQL5 文档,以理解为何结果会如动画所示。因此,当我们查阅 GetRegisterOfRequest 所使用的底层函数 DatabaseReadBind 的文档时,会看到以下高亮显示的片段:
请注意
struct_object 结构中的字段数不能超过 DatabaseColumnsCount() 的值。如果 struct_object 中的字段数小于记录中的字段数,则会执行部分读取。其余数据可以通过相应的 DatabaseColumnText()、DatabaseColumnInteger() 等函数显式获取。
“但是等等,我们并没有使用 DatabaseColumnsCount。因此,这个结果没有意义。”其实,这个结果是有意义的,因为在 SQL 查询中,我们明确指定了要接收两个字段。注意,在第 97 行,我们请求了价格字段和交易品种字段,这两个字段分别来自不同的表。GetRegisterOfRequest 并没有额外的机制去重新对齐查询结果与结构体字段,因此当返回列与结构定义不匹配时,DatabaseReadBind 只会按既有规则尝试绑定。因此,结果与动画中看到的一模一样。
我们可以改进这一点,但目前将保持现状,因为我们还需要考虑另一种情况。从以下片段中可以看出这一点。
105. //+------------------------------------------------------------------+ 106. void SELECT_Type_05(void) 107. { 108. struct st 109. { 110. double price; 111. }stLocal; 112. 113. Print("Executing type 5 data request..."); 114. if ((*SQL).ExecRequestOfData("SELECT tq.price FROM tb_Quotes AS tq, tb_Symbols AS ts WHERE tq.fk_id = (SELECT ts.id FROM tb_Symbols WHERE ts.symbol = 'PETR4') AND tq.of_day = '2023-07-11';")) 115. { 116. Print("Request: OK..."); 117. for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal); c++) 118. Print(" Resq ", c, " ==> { ", stLocal.price, " } "); 119. } 120. Print("Finish type 5..."); 121. } 122. //+------------------------------------------------------------------+
主代码片段
执行结果正是如此:

这是一个使用 SQL 的稍微极端一点的例子,我们让它执行了你可能认为在 SQL 之外应该完成的操作。但在这里,我们只是执行了一个非常具体的查询,从数据库中检索特定的记录。现在请注意第 114 行。它包含了 SQL 必须执行的命令。请注意,我们正在请求 PETR4 工具的报价,特别是针对 2023 年 7 月 11 日的报价。当然,日期遵循了 SQL 所期望的格式。但我想现在你已经了解 SQL 是如何工作的了。
如果我们查看数据库,执行 SQL 查询时,会得到与终端相同的结果。在这种情况下,只需要返回一个字段。注意第 108 行是如何声明的。
但这里可能会出现一个非常合理的问题:我们能否直接使用第 110 行,而不声明第 108 行所示的结构?很遗憾,亲爱的读者,答案是否定的。根据 GetRegisterOfRequest 的声明方式来看,并非如此。关键在于,如果我们尝试仅使用第 110行 代码而不创建结构,那么我们实际上并不会调用通过 DatabaseReadBind 执行的 read 函数。我们会使用其他方法。然而,作为程序员,我们可以编写能够处理这种情况以及前面片段中描述的情况的代码。
总之,您可以直接在 MetaEditor 中测试这些查询,或者更准确地说,测试我们在这里研究的 SQL 命令,以便进一步尝试创建旨在获得类似最后一个片段中结果的查询。但不要忘记在最终将用作 MetaTrader 5 应用程序的代码中测试相同的内容。完整的测试代码如下所示:
001. //+------------------------------------------------------------------+ 002. #property copyright "Daniel Jose" 003. #property description "Basic script for SQL database written in MQL5" 004. #property version "1.00" 005. #property script_show_inputs 006. //+------------------------------------------------------------------+ 007. #resource "\\Files\\Script 01.sql" as string SQL_Create 008. #resource "\\Files\\Script 02.sql" as string SQL_Insert 009. //+------------------------------------------------------------------+ 010. #include <Market Replay\SQL\C_DB_SQL.mqh> 011. //+------------------------------------------------------------------+ 012. enum eCall {eType_00, eType_01, eType_02, eType_03, eType_04, eType_05}; 013. //+------------------------------------------------------------------+ 014. input string user01 = "DataBase01"; //Database File Name 015. input ECall user02 = eType_01; //Search type 016. //+------------------------------------------------------------------+ 017. C_DB_SQL *SQL; 018. //+------------------------------------------------------------------+ 019. void SELECT_Type_01(void) 020. { 021. struct st 022. { 023. int id; 024. string symbol; 025. }stLocal; 026. 027. Print("Executing type 1 data request..."); 028. if ((*SQL).ExecRequestOfData("SELECT * FROM tb_Symbols")) 029. { 030. Print("Request: OK..."); 031. for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal, false); c++) 032. Print(" Resq ", c, " ==> ", stLocal.id, " - ", stLocal.symbol); 033. Print("Reload..."); 034. for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal); c++) 035. Print(" Resq ", c, " ==> ", stLocal.id, " - ", stLocal.symbol); 036. }; 037. Print("Finish type 1..."); 038. } 039. //+------------------------------------------------------------------+ 040. void SELECT_Type_02(void) 041. { 042. struct st 043. { 044. string of_day; 045. double price; 046. string symbol; 047. }stLocal; 048. 049. Print("Executing type 2 data request..."); 050. if ((*SQL).ExecRequestOfData("SELECT tq.of_day, tq.price, ts.symbol FROM tb_Quotes AS tq, tb_Symbols AS ts WHERE tq.fk_id = ts.id ORDER BY price DESC;")) 051. { 052. Print("Request: OK..."); 053. for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal); c++) 054. Print(" Resq ", c, " ==> ", stLocal.of_day, " { ", stLocal.price, " } ", stLocal.symbol); 055. } 056. Print("Finish type 2..."); 057. } 058. //+------------------------------------------------------------------+ 059. void SELECT_Type_03(void) 060. { 061. struct st0 062. { 063. double price; 064. string symbol; 065. }stLocal; 066. struct st1 067. { 068. string tmp1, 069. tmp2, 070. date; 071. }std; 072. 073. Print("Executing type 3 data request..."); 074. if ((*SQL).ExecRequestOfData("SELECT tq.price, ts.symbol, tq.of_day FROM tb_Quotes AS tq, tb_Symbols AS ts WHERE tq.fk_id = ts.id ORDER BY price DESC;")) 075. { 076. Print("Request: OK..."); 077. for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal, false); c++) 078. Print(" Resq ", c, " ==> { ", stLocal.price, " } ", stLocal.symbol); 079. Print("Reload..."); 080. for (int c = 0; (*SQL).GetRegisterOfRequest(std); c++) 081. Print(" Resq ", c, " ==> < ", std.date, " > "); 082. 083. } 084. Print("Finish type 3..."); 085. } 086. //+------------------------------------------------------------------+ 087. void SELECT_Type_04(void) 088. { 089. struct st 090. { 091. string of_day; 092. double price; 093. string symbol; 094. }stLocal; 095. 096. Print("Executing type 4 data request..."); 097. if ((*SQL).ExecRequestOfData("SELECT tq.price, ts.symbol FROM tb_Quotes AS tq, tb_Symbols AS ts WHERE tq.fk_id = ts.id ORDER BY price DESC;")) 098. { 099. Print("Request: OK..."); 100. for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal); c++) 101. Print(" Resq ", c, " ==> ", stLocal.of_day, " { ", stLocal.price, " } ", stLocal.symbol); 102. } 103. Print("Finish type 4..."); 104. } 105. //+------------------------------------------------------------------+ 106. void SELECT_Type_05(void) 107. { 108. struct st 109. { 110. double price; 111. }stLocal; 112. 113. Print("Executing type 5 data request..."); 114. if ((*SQL).ExecRequestOfData("SELECT tq.price FROM tb_Quotes AS tq, tb_Symbols AS ts WHERE tq.fk_id = (SELECT ts.id FROM tb_Symbols WHERE ts.symbol = 'PETR4') AND tq.of_day = '2023-07-11';")) 115. { 116. Print("Request: OK..."); 117. for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal); c++) 118. Print(" Resq ", c, " ==> { ", stLocal.price, " } "); 119. } 120. Print("Finish type 5..."); 121. } 122. //+------------------------------------------------------------------+ 123. const string ExecScripts(void) 124. { 125. string szMsg = (*SQL).ExecResourceSQL(SQL_Create); 126. if (szMsg != NULL) return szMsg; 127. return (*SQL).ExecResourceSQL(SQL_Insert); 128. }; 129. //+------------------------------------------------------------------+ 130. void OnStart() 131. { 132. string szMsg = NULL; 133. 134. SQL = new C_DB_SQL(user01); 135. 136. if (user02 == eType_00) szMsg = ExecScripts(); 137. Print(szMsg == NULL ? "Result of executing the SQL script: Success" : szMsg); 138. 139. if (szMsg == NULL) switch (user02) 140. { 141. case eType_01: SELECT_Type_01(); break; 142. case eType_02: SELECT_Type_02(); break; 143. case eType_03: SELECT_Type_03(); break; 144. case eType_04: SELECT_Type_04(); break; 145. case eType_05: SELECT_Type_05(); break; 146. } 147. 148. delete SQL; 149. } 150. //+------------------------------------------------------------------+
MQL5 主代码
请注意,所有解释都包含在代码中,并且行号相同。与我们之前文章中看到的代码唯一的小区别在于第 139 行,从第 15 行声明的变量开始,我们选择类型,或者更准确地说,选择将执行哪个片段。这样你就能看到每个动画中的相同内容。
最后的想法
在本文中,我们展示了在使用 MetaTrader 5 SQLite 时,如何读取 SQL 命令返回的数据。尽管在阅读本文时您可能已经注意到,我们仍然可以在保持 C_DB_SQL 类专注于处理 MetaTrader 5 内置 SQLite 的同时,对其进行改进。我们可以做的主要改进如下:
- 即使我们执行的查询包含的列较少,也允许填充 MQL5 结构中定义的字段。这将避免出现 GetRegisterOfRequest 调用返回无信息的不愉快结果。
- 另一个改进之处在于,如果查询结果是我们感兴趣的单个记录,如 SELECT_Type_05 片段中所示,我们就不必在结构体内声明返回的变量,而是可以直接将其作为常规独立变量来使用。
尽管实施这些改进颇具吸引力,但我还不能确定是否会展示具体操作方法。这是因为,在 MQL5 中编程时,我们可以采取更为谨慎的方法。无论如何,如果决定展示如何进行这些更改以更好地支持片段中所示的案例,那么将在下一篇文章中完成。尽管如此,请务必关注本系列的下一篇文章,因为这个主题每天都在变得更加有趣。
| 文件 | 描述 |
|---|---|
| Experts\Expert Advisor.mq5 | 演示 Chart Trade 与 EA 之间的交互(交互需要 Mouse Study)。 |
| Indicators\Chart Trade.mq5 | 创建一个窗口,用于配置待发送的委托(需要 Mouse Study 才能进行交互) |
| Indicators\Market Replay.mq5 | 创建用于与复制/模拟服务交互的控件(交互需要 Mouse Study)。 |
| Indicators\Mouse Study.mq5 | 提供图形控件与用户之间的交互(复制/模拟系统和真实市场都需要)。 |
| Services\Market Replay.mq5 | 创建并维护市场复制/模拟服务(整个系统的主文件)。 |
| Code VS C++\Servidor.cpp | 创建并维护一个用 C++ 开发的套接字服务器(迷你聊天版本)。 |
| Code in Python\Server.py | 创建并维护用于 MetaTrader 5 和 Excel 之间通信的 Python 套接字服务器。 |
| Indicators\Mini Chat.mq5 | 允许通过指标实现迷你聊天(需要使用服务器)。 |
| Experts\Mini Chat.mq5 | 允许使用 EA 实现迷你聊天功能(需要服务器)。 |
| Scripts\SQLite.mq5 | 演示如何通过 MQL5 使用 SQL 脚本。 |
| Files\Script 01.sql | 演示如何创建带有外键的简单表。 |
| Files\Script 02.sql | 演示如何向表中插入值。 |
本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/13117
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。
MQL5自优化智能交易系统(第十三部分):基于矩阵分解浅谈控制理论
基于协整股票的统计套利(第三部分):数据库搭建
MQL5交易策略自动化(第二十八部分):构建带可视化反馈的价格行为蝙蝠谐波形态
神经网络在交易中的应用:概率时间序列预测(K²VAE)