市場シミュレーション:MQL5でのSQL入門(V)
はじめに
こんにちは。リプレイ/シミュレーションシステムの構築方法を解説する連載の次の記事へようこそ。
前回の「市場シミュレーション:MQL5でのSQL入門(IV)」では、MetaTrader 5でSQLiteを使用する際に注意すべき点について説明しました。 この動作は将来的に変更される可能性もありますが、念のため、MetaTrader 5の環境でSQLiteを使用する際は、事前に十分なテストを行うことをお勧めします。 また、前回の記事では、MQL5のコードから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++ではvoidポインタと関連付けて説明されることの多い仕組みです。 そうです。多くの人が想像するような通常の型だけでなく、C/C++では汎用的な参照やポインタに類似した仕組みを使って処理をおこなうことができます。 しかし、こうした概念を記事の中で説明するのは非常に難しいため、今回はC/C++の詳しい解説にはできるだけ踏み込まずに、なぜこの関数がこのような宣言になっているのかを説明していきます。
まず、ここで問題となるのは型の依存関係です。 これは143行目の関数宣言そのものではなく、DatabaseReadBind関数に関係するものです。 この依存関係があるため、MQL5のコンパイラで大量のエラーが発生するのを防ぐには、特定のデータ型を使用する必要があります。 DatabaseReadBindを使用しようとする多くのプログラマーは、私たちがここで採用しているような呼び出し方をせずに、直接使用してしまいます。 その理由は、まさにここで発生する型の依存関係を理解していないことにあります。
では、彼らは通常どのような方法を取るのでしょうか。 まず、クエリを実行し、SQLから返された結果を解析したうえで、その値を通常の方法で呼び出し元に返す処理を、独立したプロシージャや関数として作成します。 この方法でも問題なく動作しますし、用途が限定されたケースであれば十分に適しています。 また、別の方法として、必要な箇所でDatabaseReadBindをメインコードから直接呼び出すプログラマーもいます。
では、なぜこのような方法を取るのでしょうか。 繰り返しになりますが、その理由は型の依存関係にあります。 MQL5では、これを簡単に扱うことができません。 一方、C/C++では状況が少し異なります。C/C++には、voidポインタを含め、汎用的なオブジェクト参照を扱うための、より低レベルな仕組みが用意されているからです。 まだ、この点が十分に理解できていないかもしれません。 そこで、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++の詳しい説明には立ち入りませんが、基本的な考え方は次のとおりです。この関数は、2つの引数を受け取ります。 1つ目の引数は整数型で、DatabasePrepareによって返されたクエリのハンドルを表します。 この点については、前回の記事ですでに説明しました。 しかし、ここで重要な役割を果たすのは2つ目の引数です。 この引数の型は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を使用する代わりに、引数の型を汎用化しているのです。
「なるほど。 しかし、まだこの仕組みがよく理解できません。 DatabaseReadBindのMQL5リファレンスを確認すると、2番目の引数には構造体を指定する必要があると書かれています。 実際、この関数の使用例でも、2番目の引数には構造体が使われています。 それなのに、なぜtemplate <typename T>を使ったり、143行目のような特殊な宣言にしたりする必要があるのでしょうか。 構造体をそのままこの関数に渡せばよいのではありませんか。 なぜ、わざわざ複雑にするのでしょうか。」
確かに、読者の皆さんの疑問はもっともです。 私もその意見には全面的に同意します。 おっしゃるとおり、関数の引数として構造体を直接渡すこともできますし、その方が143行目の宣言ははるかに簡潔になります。 しかし、ここで一つ質問させてください。その構造体には、いくつのフィールドを用意すればよいのでしょうか。また、それぞれのフィールドには、どのような型を指定すればよいのでしょうか。 この2つの質問に対して、あらゆるケースに対応できる汎用的な答えを出せるのであれば、私も143行目の呼び出し方を別の方法で実装するでしょう。
しかし、あらゆる状況に対応できるような構造体を、あらかじめ定義しておくことはできません。 なぜなら、データベースに存在するフィールドの数が分かっていたとしても、いずれその数が変更される可能性があるからです。 そのような変更が発生すれば、MQL5側の実装全体を作り直さなければならなくなります。 このため、多くのプログラマーは、ここで紹介している方法とは異なる形でDatabaseReadBindを使用しているのです。
今回紹介している関数は、SQLから返されたデータをMQL5側に渡すことを目的とした汎用関数です。 汎用関数と呼べる理由は、もうすぐ分かるでしょう。 いずれにしても、145行目で関数を呼び出し、その戻り値によって、まったく異なる2つの状況に分かれます。 1つ目は、DatabaseReadBindがSQLによって返されたレコードの読み取りに成功した場合です。 この場合、155行目の処理が実行されます。 一方、読み取るべきレコードが残っていない場合もあります。 この場合も含め、DatabaseReadBindがfalseを返したときには、2つの異なる状況が考えられます。 ただし、どちらの場合もfalseが返されたときの分岐処理で対応します。
ここで、GetRegisterOfRequestの2番目の引数には、デフォルトでtrueが設定されていることに注意してください。 145行目のチェックが失敗すると、147行目で2番目の引数が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行目では、構造体を宣言しています。 この構造体には2つの変数が含まれることを覚えておいてください。 ただし、これらを単なる変数として捉えてはいけません。 そのように考えると混乱し、後でほかの内容を理解するのが難しくなります。 これらは、SQLが返す結果の中に含まれるフィールド、または列として扱うべきものです。 つまり、この構造体はSQLの検索結果セットにおける1つの行として捉えます。 この結果の1行には、2つのフィールドが含まれることになります。
次に、28行目ではSQLを使ってデータベースのデータをクエリしようとしています。 ここで、二重引用符(")の中に書かれている内容に注目してください。これがSQLによって実行されるコマンドです。 SQLに実行させたい内容を、そのまま正確に記述する必要があります。 今回の最もシンプルな例では、テーブルtb_Symbolsに存在するすべての値を取得するだけです。 このようなクエリの作成方法は、実際の運用には適していません。しかし、今回の例ではテーブルが非常に小さく、レコード数も少ないため、十分に適切な方法です。
クエリが正常に実行された場合、まず31行目のforループが実行されます。 ここで注意してほしいのは、ループが終了する理由は、新たに取得できるレコードがなくなることだという点です。 また、31行目でのこれらの呼び出しでは、現在の結果セットをまだ閉じたり、終了したりしないように、GetRegisterOfRequestに指示していることにも注目してください。 つまり、取得可能なレコードがなくなっても、現在の結果セットは開いたまま保持され、引き続き利用できる状態になっています。
続いて32行目では、取得した結果をターミナルに出力します。 33行目では、取得したデータをもう一度読み取ることをターミナルに表示しています。 ここでは31行目とほぼ同じ処理を行っています。 ただし、今回は GetRegisterOfRequest の呼び出し時に、第2引数をまったく指定していません。 そのため、結果セットの読み取りがすべて完了すると、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行目では3つのフィールドを持つ構造体を宣言しています。 前の例では、各行に含まれるフィールドは2つでした。 しかし、フィールドの数が増えているにもかかわらず、今回も前の例と非常によく似たコードを使用していることに注目してください。 ただし、今回は50行目で使用しているSQLコマンドを注意深く見てほしいと思います。 ここでは、tb_Quotes テーブルと tb_Symbols テーブルを結合(JOIN)し、データベース内に実際に格納されている情報を、ある形で表示しようとしています。
次に、SQLクエリ内でフィールドが宣言されている順番に注目してください。 これは非常に重要です。なぜなら、DatabaseReadBind は、SQLの実行結果を読み取る際に、このフィールドの順番に従ってデータを格納するからです。 したがって、SQLコマンド内のフィールドの順番、あるいは42行目の構造体内でのフィールドの宣言順序を変更すると、取得されるデータが予想外のものになる可能性があります。 このような細かな点も見落とさないようにしてください。
さて、ここからは一見すると少し複雑そうに見えるケースに進みます。 しかし、第一印象に惑わされないでください。これまでの例と同じように、実際にはとてもシンプルです。 今回は、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. //+------------------------------------------------------------------+
主なコードの抜粋
このコードを実行した結果は、以下のとおりです。

ここまでの例では、2つまたは3つのフィールドを持つ結果行に対応した構造体を使用してきました。 しかし、前のトピックで説明したように、SQLではさらに多くのことができます。具体的には、必要な数よりも多くのフィールドをSQLに返させることができます。 そして、実際に必要なフィールドを適切に判断することで、SQLがすでに返しているデータを有効活用できます。 この方法なら、必要なデータを取得するために新しいクエリを実行する必要はありません。必要なデータは、すでに前のクエリの結果に含まれているからです。
ここで、理解しておかなければならない重要なポイントがあります。 SQLが返す各行には、それぞれ対応するフィールドの値が含まれています。 今回の例では、直前のコード断片と同じクエリを実行します。 ただし、今回はクエリが返すフィールドの順番が異なります。 この点には十分に注意してください。 その理由は、61行目で宣言している構造体st0には2つのフィールドがあり、 66行目で宣言している構造体st1には3つのフィールドがあるということです。 SQLは3つのフィールドを返します。 これは、クエリのコマンドでSQLに対して3つのフィールドを返すように指定しているからです。
次に、構造体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クエリの中で、取得したいフィールドを2つと明確に指定しているからです。 97行目では、異なるテーブルに属する price フィールドとsymbolフィールドを要求していることに注目してください。 GetRegisterOfRequestは、データを調整したり、構造体で指定された3つのフィールドのうち2つにデータを割り当てたりするための処理をおこないません。そのため、DatabaseReadBindは、SQLから返されたデータを構造体内のフィールドに対応させようとする処理を行わず、単純に順番に読み取ります。 したがって、得られる結果は、アニメーションで確認できるものと正確に一致するわけです。
この部分は改善することもできます。しかし、今のところはこのままにしておきましょう。というのも、もう1つ検討しなければならないケースがあるからです。 それについては、次のコードで確認できます。
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の外部でおこなうと思われる処理を、あえてSQLに実行させています。 ここでは、データベースから特定のレコードを取得するために、非常に具体的なクエリを実行します。 まず、114行目に注目してください。 ここには、SQLが実行するコマンドが記述されています。 今回は、銘柄 PETR4 の2023年7月11日の相場データを要求しています。 もちろん、日付はSQLが認識できる形式で指定しています。 しかし、ここまで読んでいただいた皆さんなら、SQLの仕組みについては、すでに十分理解されていると思います。
データベースの内容を確認すると、SQLクエリを実行した際にターミナルに表示されるものと同じ結果が得られます。 このようなケースでは、返す必要があるフィールドは1つだけです。 その宣言方法は108行目で確認できます。
108行目で示されている構造体を宣言せずに、110行目だけを使用することはできないのでしょうか。 残念ながら、できません。 少なくとも、現在のGetRegisterOfRequestの宣言方法では不可能です。 なぜなら、構造体を作成せずに110行目だけを使用しようとすると、DatabaseReadBindを通じて実行される読み取り関数を呼び出すことにはならないからです。 代わりに、別の処理が使用されることになります。 しかし、プログラマーとして、こうした問題や、前のコード断片で説明したケースにも対応できるコードを書くことは可能です。
いずれにしても、今回取り上げたクエリ、より正確にはSQLコマンドを、MetaEditor上で直接テストしてみることをお勧めします。さまざまなクエリを作成し、最後のコード断片のような結果を取得する方法を、さらに試してみてください。 ただし、MetaEditor上でのテストだけで終わらせず、最終的に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コマンドが返すデータをどのように読み取るかについて説明しました。 ただし、この記事を読んでお気づきのように、MetaTrader 5に組み込まれているSQLiteとの連携に特化したまま、C_DB_SQLクラスをさらに改善する余地はあります。 主な改善点は以下のとおりです。
- SQLクエリで指定した列数が少ない場合でも、MQL5の構造体で定義されているフィールドに値を格納できるようにすること。 これにより、GetRegisterOfRequestの呼び出しで情報が何も返されないという、望ましくない結果を避けることができます。
- もう1つの改善点は、SELECT_Type_05のコード断片のように、取得したい結果が1つのレコードである場合、返される値を構造体の中に宣言する必要をなくし、通常の独立した変数として直接使用できるようにすることです。
これらの改善を実装することには興味がありますが、その方法を今後の記事で紹介するかどうかは、まだ決めていません。 というのも、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は、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。