
ログレコードをマスターする(第6回):ログをデータベースに保存する
はじめに
デジタル取引と金融のオートメーションが飛び交う市場を想像してみてください。そこではすべての動きが追跡・記録され、成功に向けて綿密に分析されています。もし、エキスパートアドバイザー(EA)によるすべての判断やエラーの記録にアクセスできるだけでなく、それらをリアルタイムに最適化・改良するための強力なツールとして活用できたらどうでしょうか。本記事は、「ログレコードをマスターする(第1回):MQL5の基本概念と最初のステップ」の続編です。ここでは、MQL5開発向けに設計された高機能なログライブラリの構築を始めました。
私たちはまず、MetaTrader 5のデフォルトのログインターフェイスが持つ制約を乗り越え、柔軟かつ拡張可能なロギングソリューションの基礎を作りました。その設計には、安定したコード運用を支えるシングルトン構造、完全な監査ログのためのデータベース出力、複数の出力先への対応、ログレベルによる分類、プロジェクトに応じたフォーマットのカスタマイズといった要素が含まれています。
ここからさらに踏み込んで、ログデータを単なる記録ではなく、有益なインサイトとして活用する方法を解説していきます。EAの挙動をより深く理解し、制御し、パフォーマンスを向上させるためのアプローチです。
本記事では、ログハンドラの基本概念から、データベースに直接書き込み・クエリを行う実装までを扱います。
データベースとは何か
ログはシステムの鼓動のようなもので、内部で起こるすべての出来事を記録します。しかし、それを効率的に保存するとなると話が変わってきます。これまで私たちは、ログをテキストファイルに保存してきました。多くの場面ではシンプルで機能的な解決策です。しかし、問題はデータ量が増えてきたときに現れます。何千行もの中から特定の情報を探すのは、パフォーマンス面でも管理面でも悪夢のような作業と化します。
そこで登場するのがデータベースです。情報を構造化され、最適化された形で保存・検索・整理するための手段を提供してくれます。ファイルを一行ずつ手作業で確認する代わりに、クエリを実行するだけで、必要な情報にすぐアクセスできます。ではそもそも、データベースとは何でしょうか、そしてなぜそれがこれほど重要なのでしょうか。
データベースの構造
データベースは、検索や操作を容易にするために論理的に整理された、知的なストレージシステムとしての役割を果たします。情報がそれぞれ決まった場所に整理されている、よく分類されたファイルのようなものと考えてください。ログの文脈では、記録をバラバラのファイルに保存する代わりに、データベースに構造化して保存することで、日付、エラーの種類、その他の関連する条件で素早くフィルタリングできるようになります。
よりよく理解するために、データベースの構造を、テーブル(表)、カラム(列)、行(レコード)の3つの基本的な構成要素に分けて説明します。
-
テーブル:データベースの基盤。テーブルはスプレッドシートのようなもので、関連するデータをグループ化するために使用されます。たとえばログの場合、「logs」という名前のテーブルを作成し、その中にログの記録を専用に保存することができます。
各テーブルは特定の種類のデータのために設計されており、情報の整理と効率的なアクセスを可能にします。
列:テーブルのフィールド。テーブル内には複数の列があり、それぞれが保存される情報のカテゴリを表します。列はデータフィールドに相当し、特定の情報の種類を定義します。ログテーブルの例では、以下のような列があります。
- id:ログの一意の識別子
- timestamp:記録された日時
- level:ログレベル(DEBUG、INFO、ERRORなど)
- message:ログメッセージの内容
- source:ログの発生元(どのシステムやモジュールが記録を生成したか)
それぞれの列には明確な役割があります。たとえば、timestampは日時を格納し、messageはテキストを格納します。この構造により、冗長性が排除され、検索のパフォーマンスも向上します。
-
行:保存されたレコード。列が「どのような情報を保存するか」を定義するのに対し、行はテーブル内の個々の記録(レコード)を表します。各行は、対応するすべてのカラムに値を持つ、完全なデータセットです。実際のログテーブルの例を見てみましょう。
ID Timestamp Level Message Source 1 2025-02-12 10:15 DEBUG RSI indicator value calculated:72.56 Indicators 2 2025-02-12 10:16 INFO Buy order sent successfully Order Management 3 2025-02-12 10:17 ALERT Stop Loss adjusted to breakeven level Risk Management 4 2025-02-12 10:18 ERROR Failed to send sell order Order Management 5 2025-02-12 10:19 FATAL Failed to initialize EA:Invalid settings Initialization 各行は、特定のイベントを説明する単一のレコードです。
データベースの構造を理解したところで、次はそれをMQL5で実際にどのように活用できるかを見ていきましょう。ログを効率的に保存・検索するための方法を実践的に探っていきます。
MQL5におけるデータベース
MQL5では、データを構造化して保存・取得することが可能ですが、データベースに関するサポートには特有の制約があります。実装に進む前に、これらの点をよく理解しておく必要があります。
Webアプリケーションや業務システム向けの言語とは異なり、MQL5にはMySQLやPostgreSQLのような強力なリレーショナルデータベースをネイティブで扱う機能は備わっていません。とはいえ、テキストファイルだけに頼るしかないわけでもありません。MQL5では、以下の2つの方法でデータベースの制限を回避できます。
1つ目は、MQL5でネイティブにサポートされている(MQL5におけるデータベース関数参照)軽量なファイルベースのデータベースであるSQLiteを使用する方法で、2つ目はAPIを通じて外部接続を確立し、より強力なデータベースと統合する方法です。ログを効率的に保存・検索するという目的においては、SQLiteが理想的な選択です。シンプルで高速、専用のサーバーを必要とせず、私たちのニーズに最適です。実装に進む前に、.sqliteファイル形式のデータベースの特徴を理解しておきましょう。
- 利点
- サーバー不要:SQLiteは「組み込み型」データベースで、サーバーのインストールや設定が不要です。
- すぐに使用可能:.sqliteファイルを作成するだけで、すぐにデータの保存を開始できます。
- 高速な読み取り:1つのファイルに格納されているため、小〜中規模のデータに対する読み取りは非常に高速です。
- 低遅延:簡単なクエリであれば、従来のリレーショナルデータベースよりも高速に処理できます。
- 高い互換性:多くのプログラミング言語と互換性があります。
- 欠点
- ファイル破損のリスク:ファイルが破損すると、データの復旧が困難になる場合があります。
- 手動バックアップ:SQLiteには自動レプリケーション機能がないため、.sqliteファイルをコピーすることによって手動でバックアップする必要があります。
- スケーラビリティの限界:大量のデータや複数の同時アクセスには不向きです。ただし、今回の目的はローカルにログを保存することなので問題はありません。
SQLiteの可能性と制限を理解したところで、次はログを効果的に保存・取得するための基本的な操作(基本クエリ、テーブル作成、データ挿入など)に取り組んでいきましょう。
必要なデータベース操作の基本
ハンドラを実装する前に、データベース上で使用する基本的な操作を理解しておく必要があります。これらの操作には、テーブルの作成、新しいレコードの挿入、データの取得、必要に応じてログの削除や更新が含まれます。
ログの文脈では、通常、日時、ログレベル、メッセージ、そしてログを生成したファイル名やコンポーネント名などの情報を保存する必要があります。これを実現するには、高速かつ効率的なクエリが可能となるよう、テーブル構造を設計しなければなりません。
まずは、Experts/Logifyフォルダ内にDatabaseTest.mq5というテスト用のエキスパートアドバイザー(EA)を作成してみましょう。ファイルが作成されると、以下のような形になります。
//+------------------------------------------------------------------+ //| DatabaseTest.mq5 | //| joaopedrodev | //| https://www.mql5.com/en/users/joaopedrodev | //+------------------------------------------------------------------+ #property copyright "joaopedrodev" #property link "https://www.mql5.com/en/users/joaopedrodev" #property version "1.00" //+------------------------------------------------------------------+ //| Import CLogify | //+------------------------------------------------------------------+ #include <Logify/Logify.mqh> //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+
データベースの作成と接続
最初のステップは、データベースを作成して接続することです。これをおこなうにはDatabaseOpen()関数を使用します。この関数は2つのパラメータを取ります。
- filename:「MQL5\Files」フォルダを基準としたデータベースファイルの名前
- flags:ENUM_DATABASE_OPEN_FLAGS列挙体からのフラグの組み合わせ。これらのフラグは、データベースへのアクセス方法を決定します。使用可能なフラグは次のとおりです。
- DATABASE_OPEN_READONLY:読み取り専用アクセス
- DATABASE_OPEN_READWRITE:読み書き可能
- DATABASE_OPEN_CREATE:ファイルが存在しない場合にデータベースをディスク上に作成
- DATABASE_OPEN_MEMORY:一時的なメモリ上のデータベースを作成
- DATABASE_OPEN_COMMON:すべての端末で共通のフォルダにファイルを保存
この例では、「DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE」を使用します。これにより、データベースがまだ存在しない場合には自動的に作成されるため、手動での存在確認が不要になります。
DatabaseOpen関数はデータベースハンドルを返します。これを変数に格納して、後の操作で使用します。また、使用後には必ず接続を閉じる必要があります。そのためにDatabaseClose関数を使用します。
コードは次のようになります。
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Opening a database connection int dbHandle = DatabaseOpen(path,DATABASE_OPEN_READWRITE|DATABASE_OPEN_CREATE); if(dbHandle == INVALID_HANDLE) { Print("[ERROR] ["+TimeToString(TimeCurrent())+"] Database error (Code: "+IntegerToString(GetLastError())+")"); return(INIT_FAILED); } Print("Open database file"); //--- Closing database after use DatabaseClose(handle_db); Print("Closed database file"); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+
データベースの開閉ができるようになったので、次は保存するデータの構造を決めましょう。まずは最初のテーブル「logs」を作成します。
テーブルの作成
ただし、闇雲にテーブルを作る前に、そのテーブルがすでに存在しているかどうかを確認する必要があります。これにはDatabaseTableExists関数を使います。テーブルがデータベース内に存在しない場合は、シンプルなSQLコマンドで作成します。ここで少しSQL(Structured Query Language:構造化問い合わせ言語)について説明します。SQLはデータベースとやり取りをするための言語で、データの挿入、検索、変更、削除などをおこなうことができます。SQLは「レストランのメニュー」のようなもので、注文(SQLクエリ)を出せば、正しく注文ができていれば欲しいものが返ってくるイメージです。
それでは実際に、ログ用のテーブルを作成し、必要に応じて正しく生成されるように構造を整えていきましょう。
今回の目的に必要なSQLコマンドは限られていますが、そのうちの最初はテーブルを作成するためのコマンドです。
CREATE TABLE {table_name} ({column_name} {type_data}, …);
- {table_name}:作成するテーブルの名前
- {column_name} {type_data}:列の定義。{type_data}はデータ型(テキスト、数値、日付など)を示します。
次に、DatabaseExecute関数を使ってテーブル作成のコマンドを実行します。テーブル構造はMqlLogifyModelの構造に基づいており、以下のフィールドを含みます。
- id:行の一意識別子
- formated:フォーマット済みメッセージ
- levelname:ログレベルの名前
- msg:元のメッセージ
- args:メッセージの引数
- timestamp:数値形式の日付と時刻
- date_time:フォーマット済みの日付と時刻
- level:ログレベル(重要度)
- origin:ログの発生元
- filename:ソースファイル名
- function:ログが生成された関数名
- line:ログが生成されたコード行
コードは次のようになります。
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Open the database connection int dbHandle = DatabaseOpen("db\\logs.sqlite", DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE); if(dbHandle == INVALID_HANDLE) { Print("[ERROR] [" + TimeToString(TimeCurrent()) + "] Unable to open database (Error Code: " + IntegerToString(GetLastError()) + ")"); return(INIT_FAILED); } Print("[INFO] Database connection opened successfully"); //--- Create the 'logs' table if it does not exist if(!DatabaseTableExists(dbHandle, "logs")) { DatabaseExecute(dbHandle, "CREATE TABLE logs (" "id INTEGER PRIMARY KEY AUTOINCREMENT," // Auto-incrementing unique ID "formated TEXT," // Formatted log message "levelname TEXT," // Log level (INFO, ERROR, etc.) "msg TEXT," // Main log message "args TEXT," // Additional details "timestamp BIGINT," // Log event timestamp (Unix time) "date_time DATETIME,"// Human-readable date and time "level BIGINT," // Log level as an integer "origin TEXT," // Module or component name "filename TEXT," // Source file name "function TEXT," // Function where the log was recorded "line BIGINT);"); // Source code line number Print("[INFO] 'logs' table created successfully"); } //--- Close the database connection DatabaseClose(dbHandle); Print("[INFO] Database connection closed successfully"); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+
これで、データベースと「logs」テーブルの作成手順は完了です。テーブルを作成すると、ファイルエクスプローラーのFilesフォルダ内にデータベースファイルが表示されるはずです。
ファイルをクリックすると、MetaEditorはこのファイルをサポートしており、次のような画面が開きます。
こちらは、データベースのデータを閲覧したり、赤枠で示されたようにさまざまなSQLコマンドを実行できるインターフェイスです。エディター内でデータを確認する際に、この機能を頻繁に使います。
データベースにデータを挿入する方法
SQLでは、テーブルにデータを挿入するために使用されるコマンドは次のとおりです。
INSERT INTO {table_name} ({column}, ...) VALUES ({value}, ...)
MQL5の文脈では、この処理は特定の関数を使うことで簡略化でき、より直感的かつエラーの起こりにくい形で記述できます。主に使用する関数は以下のとおりです。
- DatabasePrepare:SQLクエリの識別子を作成し、後の実行に向けて準備します。クエリをデータベースが解釈可能な状態にする最初のステップです。
- DatabaseBind:クエリ内のパラメータに実際の値を関連付けます。SQL文の中では?1、?2のようなプレースホルダーで値を指定し、実行時にデータで置き換えられます。
- DatabaseRead:準備済みのクエリを実行します。INSERTのように結果を返さないコマンドの場合でも、この関数を使って命令を実行し、必要に応じて次のレコードへと進みます。
- DatabaseFinalize:使用後は、クエリに関連するリソースを解放することが重要です。この関数は、準備されたクエリを終了し、メモリリークを防ぎます。
データを挿入するクエリを作成する際には、後から値をバインドするためにプレースホルダーを使うことができます。以下は、先ほど作成したlogsテーブルに新しいレコードを挿入する例です。
INSERT INTO logs (formated, levelname, msg, args, timestamp, date_time, level, origin, filename, function, line) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11);
テーブル内のすべてのフィールドが列挙されている点に注目してください。ただし、idフィールドだけは除かれています。これは、データベースによって自動的に生成されるためです。また、挿入される値は?1、?2などの形で示されています。これらのプレースホルダーは、後でDatabaseBind関数を使って実際の値と関連付ける際に使用されるインデックスに対応しています。
//--- Prepare SQL statement for inserting a log entry string sql = "INSERT INTO logs (formated, levelname, msg, args, timestamp, date_time, level, origin, filename, function, line) " "VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11);"; int sqlRequest = DatabasePrepare(dbHandle, sql); if(sqlRequest == INVALID_HANDLE) { Print("[ERROR] Failed to prepare SQL statement for log insertion"); } //--- Bind values to the SQL statement DatabaseBind(sqlRequest, 0, "06:24:00 [INFO] Buy order sent successfully"); // Formatted log message DatabaseBind(sqlRequest, 1, "INFO"); // Log level name DatabaseBind(sqlRequest, 2, "Buy order sent successfully"); // Main log message DatabaseBind(sqlRequest, 3, "Symbol: EURUSD, Volume: 0.1"); // Additional details DatabaseBind(sqlRequest, 4, 1739471040); // Unix timestamp DatabaseBind(sqlRequest, 5, "2025.02.13 18:24:00"); // Readable date and time DatabaseBind(sqlRequest, 6, 1); // Log level as integer DatabaseBind(sqlRequest, 7, "Order Management"); // Module or component name DatabaseBind(sqlRequest, 8, "File.mq5"); // Source file name DatabaseBind(sqlRequest, 9, "OnInit"); // Function name DatabaseBind(sqlRequest, 10, 100); // Line numberすべての値をバインドした後、DatabaseRead関数を使用して準備済みのクエリを実行します。実行が成功した場合は、確認メッセージが出力され、失敗した場合はエラーメッセージが表示されます。
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Open the database connection int dbHandle = DatabaseOpen("db\\logs.sqlite", DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE); if(dbHandle == INVALID_HANDLE) { Print("[ERROR] [" + TimeToString(TimeCurrent()) + "] Unable to open database (Error Code: " + IntegerToString(GetLastError()) + ")"); return(INIT_FAILED); } Print("[INFO] Database connection opened successfully"); //--- Create the 'logs' table if it does not exist if(!DatabaseTableExists(dbHandle, "logs")) { DatabaseExecute(dbHandle, "CREATE TABLE logs (" "id INTEGER PRIMARY KEY AUTOINCREMENT," // Auto-incrementing unique ID "formated TEXT," // Formatted log message "levelname TEXT," // Log level (INFO, ERROR, etc.) "msg TEXT," // Main log message "args TEXT," // Additional details "timestamp BIGINT," // Log event timestamp (Unix time) "date_time DATETIME,"// Human-readable date and time "level BIGINT," // Log level as an integer "origin TEXT," // Module or component name "filename TEXT," // Source file name "function TEXT," // Function where the log was recorded "line BIGINT);"); // Source code line number Print("[INFO] 'logs' table created successfully"); } //--- Prepare SQL statement for inserting a log entry string sql = "INSERT INTO logs (formated, levelname, msg, args, timestamp, date_time, level, origin, filename, function, line) " "VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11);"; int sqlRequest = DatabasePrepare(dbHandle, sql); if(sqlRequest == INVALID_HANDLE) { Print("[ERROR] Failed to prepare SQL statement for log insertion"); } //--- Bind values to the SQL statement DatabaseBind(sqlRequest, 0, "06:24:00 [INFO] Buy order sent successfully"); // Formatted log message DatabaseBind(sqlRequest, 1, "INFO"); // Log level name DatabaseBind(sqlRequest, 2, "Buy order sent successfully"); // Main log message DatabaseBind(sqlRequest, 3, "Symbol: EURUSD, Volume: 0.1"); // Additional details DatabaseBind(sqlRequest, 4, 1739471040); // Unix timestamp DatabaseBind(sqlRequest, 5, "2025.02.13 18:24:00"); // Readable date and time DatabaseBind(sqlRequest, 6, 1); // Log level as integer DatabaseBind(sqlRequest, 7, "Order Management"); // Module or component name DatabaseBind(sqlRequest, 8, "File.mq5"); // Source file name DatabaseBind(sqlRequest, 9, "OnInit"); // Function name DatabaseBind(sqlRequest, 10, 100); // Line number //--- Execute the SQL statement if(!DatabaseRead(sqlRequest)) { Print("[ERROR] SQL insertion request failed"); } else { Print("[INFO] Log entry inserted successfully"); } //--- Close the database connection DatabaseClose(dbHandle); Print("[INFO] Database connection closed successfully"); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+このEAを実行すると、コンソールに次のメッセージが表示されます。
[INFO] Database file opened successfully [INFO] Table 'logs' created successfully [INFO] Log entry inserted successfully [INFO] Database file closed successfully
また、エディターでデータベースを開くと、以下の画像のように、入力されたすべてのデータを含むlogsテーブルを確認することができます。
データベースからデータを読み取る方法
データベースからデータを読み取る処理は、レコードを挿入する処理と非常に似ていますが、目的はすでに保存されている情報を取得することにあります。MQL5では、データを読み取る基本的な流れは以下の通りです。
- SQLクエリを準備:DatabasePrepare関数を使用して、実行するクエリの識別子を作成します。
- クエリを実行:準備された識別子を使用し、DatabaseRead関数でクエリを実行し、結果セットの最初のレコードにカーソルを移動します。
- データを抽出:現在のレコードから、各カラムの値をデータ型に応じて取得します。以下のような関数を使用します。
- DatabaseColumnText:現在のレコードのフィールドを文字列として取得
- DatabaseColumnInteger:現在のレコードからint値を取得
- DatabaseColumnLong:現在のレコードからlong値を取得
- DatabaseColumnDouble:現在のレコードからdouble値を取得
- DatabaseColumnBlob:現在のレコードのフィールドを配列として取得
これらの手順により、必要な情報をアプリケーション内で簡単かつ効果的に取得して利用できます。
たとえば、logsテーブルのすべてのレコードを取得したい場合、SQLクエリは非常にシンプルです。
SELECT * FROM logs
このクエリは、テーブル内のすべてのレコードからすべてのカラムを選択します。MQL5では、データを挿入したときと同様に、DatabasePrepare関数を使ってクエリ識別子を作成します。
最終的なコードは以下のようになります。
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Open the database connection int dbHandle = DatabaseOpen("db\\logs.sqlite", DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE); if(dbHandle == INVALID_HANDLE) { Print("[ERROR] [" + TimeToString(TimeCurrent()) + "] Unable to open database (Error Code: " + IntegerToString(GetLastError()) + ")"); return INIT_FAILED; } Print("[INFO] Database connection opened successfully."); //--- Create the 'logs' table if it doesn't exist if(!DatabaseTableExists(dbHandle, "logs")) { string createTableSQL = "CREATE TABLE logs (" "id INTEGER PRIMARY KEY AUTOINCREMENT," // Auto-incrementing unique ID "formated TEXT," // Formatted log message "levelname TEXT," // Log level name (INFO, ERROR, etc.) "msg TEXT," // Main log message "args TEXT," // Additional arguments/details "timestamp BIGINT," // Timestamp of the log event "date_time DATETIME," // Human-readable date and time "level BIGINT," // Log level as an integer "origin TEXT," // Module or component name "filename TEXT," // Source file name "function TEXT," // Function where the log was recorded "line BIGINT);"; // Line number in the source code DatabaseExecute(dbHandle, createTableSQL); Print("[INFO] 'logs' table created successfully."); } //--- Prepare SQL statement to retrieve log entries string sqlQuery = "SELECT * FROM logs"; int sqlRequest = DatabasePrepare(dbHandle, sqlQuery); if(sqlRequest == INVALID_HANDLE) { Print("[ERROR] Failed to prepare SQL statement."); } //--- Execute the SQL statement if(!DatabaseRead(sqlRequest)) { Print("[ERROR] SQL query execution failed."); } else { Print("[INFO] SQL query executed successfully."); //--- Bind SQL query results to the log data model MqlLogifyModel logData; DatabaseColumnText(sqlRequest, 1, logData.formated); DatabaseColumnText(sqlRequest, 2, logData.levelname); DatabaseColumnText(sqlRequest, 3, logData.msg); DatabaseColumnText(sqlRequest, 4, logData.args); DatabaseColumnLong(sqlRequest, 5, logData.timestamp); string dateTimeStr; DatabaseColumnText(sqlRequest, 6, dateTimeStr); logData.date_time = StringToTime(dateTimeStr); DatabaseColumnInteger(sqlRequest, 7, logData.level); DatabaseColumnText(sqlRequest, 8, logData.origin); DatabaseColumnText(sqlRequest, 9, logData.filename); DatabaseColumnText(sqlRequest, 10, logData.function); DatabaseColumnLong(sqlRequest, 11, logData.line); Print("[INFO] Data retrieved: Formatted = ", logData.formated, " | Level = ", logData.level, " | Origin = ", logData.origin); } //--- Close the database connection DatabaseClose(dbHandle); Print("[INFO] Database connection closed successfully."); return INIT_SUCCEEDED; } //+------------------------------------------------------------------+
これでコードを実行すると、以下のような結果が得られます。
[INFO] Database file opened successfully [INFO] SQL request successfully [INFO] Data read! | Formated: 06:24:00 [INFO] Buy order sent successfully | Level: 1 | Origin: Order Management [INFO] Database file closed successfully
これらの基本的な操作を踏まえて、いよいよデータベースハンドラの構成に入ります。ログライブラリとデータベースを統合するために必要な環境を整えていきましょう。
データベースハンドラの構成
ログをデータベースに保存するためには、ハンドラを適切に設定する必要があります。これは、ファイルハンドラでおこなったように、設定構造体の属性を定義する作業を含みます。ここでは、MqlLogifyHandleDatabaseConfigという名前の構成構造体を作成し、それを元に必要な変更を加えたコピーを作っていきます。
struct MqlLogifyHandleDatabaseConfig { string directory; // Directory for log files string base_filename; // Base file name ENUM_LOG_FILE_EXTENSION file_extension; // File extension type ENUM_LOG_ROTATION_MODE rotation_mode; // Rotation mode int messages_per_flush; // Messages before flushing uint codepage; // Encoding (e.g., UTF-8, ANSI) ulong max_file_size_mb; // Max file size in MB for rotation int max_file_count; // Max number of files before deletion //--- Default constructor MqlLogifyHandleDatabaseConfig(void) { directory = "logs"; // Default directory base_filename = "expert"; // Default base name file_extension = LOG_FILE_EXTENSION_LOG;// Default to .log extension rotation_mode = LOG_ROTATION_MODE_SIZE;// Default size-based rotation messages_per_flush = 100; // Default flush threshold codepage = CP_UTF8; // Default UTF-8 encoding max_file_size_mb = 5; // Default max file size in MB max_file_count = 10; // Default max file count } };
赤でマークしたように、ローテーション、ファイルタイプ、最大ファイル数、エンコーディングモードなどの属性は、データベースの文脈では意味を持たないため削除します。属性を定義し直したら、ValidityConfigメソッドもそれに合わせて調整します。最終的に、コードは以下のようになります。
//+------------------------------------------------------------------+ //| Struct: MqlLogifyHandleDatabaseConfig | //+------------------------------------------------------------------+ struct MqlLogifyHandleDatabaseConfig { string directory; // Directory for log files string base_filename; // Base file name int messages_per_flush; // Messages before flushing //--- Default constructor MqlLogifyHandleDatabaseConfig(void) { directory = "logs"; // Default directory base_filename = "expert"; // Default base name messages_per_flush = 100; // Default flush threshold } //--- Destructor ~MqlLogifyHandleDatabaseConfig(void) { } //--- Validate configuration bool ValidateConfig(string &error_message) { //--- Saves the return value bool is_valid = true; //--- Check if the directory is not empty if(directory == "") { directory = "logs"; error_message = "The directory cannot be empty."; is_valid = false; } //--- Check if the base filename is not empty if(base_filename == "") { base_filename = "expert"; error_message = "The base filename cannot be empty."; is_valid = false; } //--- Check if the number of messages per flush is positive if(messages_per_flush <= 0) { messages_per_flush = 100; error_message = "The number of messages per flush must be greater than zero."; is_valid = false; } //--- No errors found return(is_valid); } };
構成が整ったところで、いよいよハンドラの実装に入ります。
データベースハンドラの実装
設定構造が完成したので、ここからは実践的な部分、つまりデータベースハンドラの実装に進みます。ここでは、各部分を詳細に説明しながら、今後の拡張にも対応できるよう柔軟なハンドラを構築していきます。
まずは、CLogifyHandlerを継承するCLogifyHandlerDatabaseクラスを定義します。このクラスでは、ハンドラの設定情報、時間制御ユーティリティ(CIntervalWatcher)、ログメッセージのキャッシュの要素を保持する必要があります。このキャッシュは、データベースへの過剰な書き込みを避けるための一時的なバッファとして機能し、一定のタイミングでまとめて書き込むために使用されます。
class CLogifyHandlerDatabase : public CLogifyHandler { private: //--- Config MqlLogifyHandleDatabaseConfig m_config; //--- Update utilities CIntervalWatcher m_interval_watcher; //--- Cache data MqlLogifyModel m_cache[]; int m_index_cache; public: CLogifyHandlerDatabase(void); ~CLogifyHandlerDatabase(void); //--- Configuration management void SetConfig(MqlLogifyHandleDatabaseConfig &config); MqlLogifyHandleDatabaseConfig GetConfig(void); virtual void Emit(MqlLogifyModel &data); // Processes a log message and sends it to the specified destination virtual void Flush(void); // Clears or completes any pending operations virtual void Close(void); // Closes the handler and releases any resources };
コンストラクタでは属性を初期化し、ハンドラ名を「database」に設定、m_interval_watcherのインターバルを指定し、キャッシュをクリアします。デストラクタではCloseメソッドを呼び出し、オブジェクト終了前に保留中のログがすべて書き込まれるようにしています。
もう一つ重要なメソッドがSetConfigで、これはハンドラの設定を受け取り保存し、エラーがないか検証します。GetConfigメソッドは、現在の設定を単純に返します。
CLogifyHandlerDatabase::CLogifyHandlerDatabase(void) { m_name = "database"; m_interval_watcher.SetInterval(PERIOD_D1); ArrayFree(m_cache); m_index_cache = 0; } CLogifyHandlerDatabase::~CLogifyHandlerDatabase(void) { this.Close(); } void CLogifyHandlerDatabase::SetConfig(MqlLogifyHandleDatabaseConfig &config) { m_config = config; string err_msg = ""; if(!m_config.ValidateConfig(err_msg)) { Print("[ERROR] ["+TimeToString(TimeCurrent())+"] Log system error: "+err_msg); } } MqlLogifyHandleDatabaseConfig CLogifyHandlerDatabase::GetConfig(void) { return(m_config); }
それでは、データベースハンドラの核心部分、つまりログレコードを直接保存する処理に入りましょう。これには、すべてのハンドラで必須となる3つの基本メソッドを実装します。
- Emit(MqlLogifyModel &data):ログメッセージを処理し、キャッシュに追加します。
- Flush():現在の操作を終了またはクリアし、情報を適切な宛先(ファイル、コンソール、データベースなど)に書き込みます。
- Close():ハンドラを閉じ、関連するリソースを解放します。
まずは、Emitメソッドから始めます。これは、受け取ったデータをキャッシュに追加し、キャッシュが定められた上限に達した場合にFlushを呼び出してまとめて書き込む役割を担います。
//+------------------------------------------------------------------+ //| Processes a log message and sends it to the specified destination| //+------------------------------------------------------------------+ void CLogifyHandlerDatabase::Emit(MqlLogifyModel &data) { //--- Checks if the configured level allows if(data.level >= this.GetLevel()) { //--- Resize cache if necessary int size = ArraySize(m_cache); if(size != m_config.messages_per_flush) { ArrayResize(m_cache, m_config.messages_per_flush); size = m_config.messages_per_flush; } //--- Add log to cache m_cache[m_index_cache++] = data; //--- Flush if cache limit is reached or update condition is met if(m_index_cache >= m_config.messages_per_flush || m_interval_watcher.Inspect()) { //--- Save cache Flush(); //--- Reset cache m_index_cache = 0; for(int i=0;i<size;i++) { m_cache[i].Reset(); } } } } //+------------------------------------------------------------------+
続いて、Flushメソッドではキャッシュからデータを読み取り、記事冒頭の「データベースへのデータ挿入方法」セクションで説明した通り、DatabasePrepare関数を使って同じ構造でデータベースに追加していきます。
//+------------------------------------------------------------------+ //| Clears or completes any pending operations | //+------------------------------------------------------------------+ void CLogifyHandlerDatabase::Flush(void) { //--- Get the full path of the file string path = m_config.directory+"\\"+m_config.base_filename+".sqlite"; //--- Open database ResetLastError(); int handle_db = DatabaseOpen(path,DATABASE_OPEN_CREATE|DATABASE_OPEN_READWRITE); if(handle_db == INVALID_HANDLE) { Print("[ERROR] ["+TimeToString(TimeCurrent())+"] Log system error: Unable to open log file '"+path+"'. Ensure the directory exists and is writable. (Code: "+IntegerToString(GetLastError())+")"); return; } if(!DatabaseTableExists(handle_db,"logs")) { DatabaseExecute(handle_db, "CREATE TABLE logs (" "id INTEGER PRIMARY KEY AUTOINCREMENT," "formated TEXT," "levelname TEXT," "msg TEXT," "args TEXT," "timestamp BIGINT," "date_time DATETIME," "level BIGINT," "origin TEXT," "filename TEXT," "function TEXT," "line BIGINT);"); } //--- string sql="INSERT INTO logs (formated, levelname, msg, args, timestamp, date_time, level, origin, filename, function, line) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11);"; // parâmetro de consulta int request = DatabasePrepare(handle_db,sql); if(request == INVALID_HANDLE) { Print("Erro"); } //--- Loop through all cached messages int size = ArraySize(m_cache); for(int i=0;i<size;i++) { if(m_cache[i].timestamp > 0) { DatabaseBind(request,0,m_cache[i].formated); DatabaseBind(request,1,m_cache[i].levelname); DatabaseBind(request,2,m_cache[i].msg); DatabaseBind(request,3,m_cache[i].args); DatabaseBind(request,4,m_cache[i].timestamp); DatabaseBind(request,5,TimeToString(m_cache[i].date_time,TIME_DATE|TIME_MINUTES|TIME_SECONDS)); DatabaseBind(request,6,(int)m_cache[i].level); DatabaseBind(request,7,m_cache[i].origin); DatabaseBind(request,8,m_cache[i].filename); DatabaseBind(request,9,m_cache[i].function); DatabaseBind(request,10,m_cache[i].line); DatabaseRead(request); DatabaseReset(request); } } //--- DatabaseFinalize(request); //--- Close database DatabaseClose(handle_db); } //+------------------------------------------------------------------+
最後に、Closeメソッドは、終了時に保留中のすべてのログが確実に書き込まれるように処理します。
void CLogifyHandlerDatabase::Close(void) { Flush(); }
これで堅牢なハンドラが実装でき、ログを効率的かつデータ損失なく保存できるようになりました。次のステップは、記録したログを効率的に検索するメソッドを作成することです。そのために、汎用的なベースメソッドとして、SQLコマンド(文字列形式)を受け取り、結果をMqlLogifyModel型の配列で返すQueryメソッドを用意します。これを基に、よく使う検索を簡単に行える専用メソッドを構築していきます。Queryメソッドは、データベースを開き、クエリを実行し、結果をログ構造体に格納する役割を担います。以下に実装を示します。
class CLogifyHandlerDatabase : public CLogifyHandler { public: //--- Query methods bool Query(string query, MqlLogifyModel &data[]); }; //+------------------------------------------------------------------+ //| Get data by sql command | //+------------------------------------------------------------------+ bool CLogifyHandlerDatabase::Query(string query, MqlLogifyModel &data[]) { //--- Get the full path of the file string path = m_config.directory+"\\"+m_config.base_filename+".sqlite"; //--- Open database ResetLastError(); int handle_db = DatabaseOpen(path,DATABASE_OPEN_READWRITE); if(handle_db == INVALID_HANDLE) { Print("[ERROR] ["+TimeToString(TimeCurrent())+"] Log system error: Unable to open log file '"+path+"'. Ensure the directory exists and is writable. (Code: "+IntegerToString(GetLastError())+")"); return(false); } //--- Prepare the SQL query int request = DatabasePrepare(handle_db,query); if(request == INVALID_HANDLE) { Print("Erro query"); return(false); } //--- Clears array before inserting new data ArrayFree(data); //--- Reads query results line by line for(int i=0;DatabaseRead(request);i++) { int size = ArraySize(data); ArrayResize(data,size+1,size); //--- Maps database data to the MqlLogifyModel model DatabaseColumnText(request,1,data[size].formated); DatabaseColumnText(request,2,data[size].levelname); DatabaseColumnText(request,3,data[size].msg); DatabaseColumnText(request,4,data[size].args); DatabaseColumnLong(request,5,data[size].timestamp); string value; DatabaseColumnText(request,6,value); data[size].date_time = StringToTime(value); DatabaseColumnInteger(request,7,data[size].level); DatabaseColumnText(request,8,data[size].origin); DatabaseColumnText(request,9,data[size].filename); DatabaseColumnText(request,10,data[size].function); DatabaseColumnLong(request,11,data[size].line); } //--- Ends the query and closes the database DatabaseFinalize(handle_db); DatabaseClose(handle_db); return(true); } //+------------------------------------------------------------------+
このメソッドにより、ログデータベース内であらゆるSQLクエリを自由に実行できる柔軟性が得られます。しかし、使いやすくするために、よく使われるクエリをまとめたヘルパーメソッドを作成します。
開発者が毎回SQLを書く手間を省くため、代表的なSQLコマンドをあらかじめ含むメソッドを用意しました。これらは、ログの絞り込み検索を重要度レベル、日付、発生元、メッセージ、引数、ファイル名、関数名で行うショートカットとして機能します。以下は、それぞれのフィルターに対応したSQLコマンド例です。
SELECT * FROM 'logs' WHERE level=1; SELECT * FROM 'logs' WHERE timestamp BETWEEN '{start_time}' AND '{stop_time}'; SELECT * FROM 'logs' WHERE origin LIKE '%{origin}%'; SELECT * FROM 'logs' WHERE msg LIKE '%{msg}%'; SELECT * FROM 'logs' WHERE args LIKE '%{args}%'; SELECT * FROM 'logs' WHERE filename LIKE '%{filename}%'; SELECT * FROM 'logs' WHERE function LIKE '%{function}%';
ここで、これらのコマンドを使用する特定のメソッドを実装します。
class CLogifyHandlerDatabase : public CLogifyHandler { public: //--- Query methods bool Query(string query, MqlLogifyModel &data[]); bool QueryByLevel(ENUM_LOG_LEVEL level, MqlLogifyModel &data[]); bool QueryByDate(datetime start_time, datetime stop_time, MqlLogifyModel &data[]); bool QueryByOrigin(string origin, MqlLogifyModel &data[]); bool QueryByMsg(string msg, MqlLogifyModel &data[]); bool QueryByArgs(string args, MqlLogifyModel &data[]); bool QueryByFile(string file, MqlLogifyModel &data[]); bool QueryByFunction(string function, MqlLogifyModel &data[]); }; //+------------------------------------------------------------------+ //| Get logs filtering by level | //+------------------------------------------------------------------+ bool CLogifyHandlerDatabase::QueryByLevel(ENUM_LOG_LEVEL level, MqlLogifyModel &data[]) { return(this.Query("SELECT * FROM 'logs' WHERE level="+IntegerToString(level)+";",data)); } //+------------------------------------------------------------------+ //| Get logs filtering by start end stop time | //+------------------------------------------------------------------+ bool CLogifyHandlerDatabase::QueryByDate(datetime start_time, datetime stop_time, MqlLogifyModel &data[]) { return(this.Query("SELECT * FROM 'logs' WHERE timestamp BETWEEN '"+IntegerToString((ulong)start_time)+"' AND '"+IntegerToString((ulong)stop_time)+"';",data)); } //+------------------------------------------------------------------+ //| Get logs filtering by origin | //+------------------------------------------------------------------+ bool CLogifyHandlerDatabase::QueryByOrigin(string origin, MqlLogifyModel &data[]) { return(this.Query("SELECT * FROM 'logs' WHERE origin LIKE '%"+origin+"%';",data)); } //+------------------------------------------------------------------+ //| Get logs filtering by message | //+------------------------------------------------------------------+ bool CLogifyHandlerDatabase::QueryByMsg(string msg, MqlLogifyModel &data[]) { return(this.Query("SELECT * FROM 'logs' WHERE msg LIKE '%"+msg+"%';",data)); } //+------------------------------------------------------------------+ //| Get logs filtering by args | //+------------------------------------------------------------------+ bool CLogifyHandlerDatabase::QueryByArgs(string args, MqlLogifyModel &data[]) { return(this.Query("SELECT * FROM 'logs' WHERE args LIKE '%"+args+"%';",data)); } //+------------------------------------------------------------------+ //| Get logs filtering by file name | //+------------------------------------------------------------------+ bool CLogifyHandlerDatabase::QueryByFile(string file, MqlLogifyModel &data[]) { return(this.Query("SELECT * FROM 'logs' WHERE filename LIKE '%"+file+"%';",data)); } //+------------------------------------------------------------------+ //| Get logs filtering by function name | //+------------------------------------------------------------------+ bool CLogifyHandlerDatabase::QueryByFunction(string function, MqlLogifyModel &data[]) { return(this.Query("SELECT * FROM 'logs' WHERE function LIKE '%"+function+"%';",data)); } //+------------------------------------------------------------------+
これで、データベースからログへ効率的かつ柔軟にアクセスできるメソッド群が揃いました。QueryメソッドはあらゆるSQLコマンドを実行可能で、必要に応じて複雑な条件を含むSQLを渡すこともできます。一方で、ヘルパーメソッドはよく使うクエリをカプセル化しているため、直感的に使え、ミスを減らすことができます。
ハンドラの実装が完了したので、次は動作確認をおこないましょう。結果を可視化し、ログが正しく保存・取得されているかを確かめていきます。
結果の可視化
ハンドラを実装したら、次に期待通りに動作しているかを確認します。ログの挿入テストを行い、レコードがデータベースに正しく保存されているかを検証し、クエリの速度と正確性も確認します。
テストには、同じLogifyTest.mq5ファイルを使い、冒頭にいくつかログメッセージを追加します。また、複雑な戦略は使わず、ポジションが開いていなければ新規にオープンし、テイクプロフィットとストップロスを設定して決済できるような簡単な動作を加えます。
//+------------------------------------------------------------------+ //| Import CLogify | //+------------------------------------------------------------------+ #include <Logify/Logify.mqh> #include <Trade/Trade.mqh> CLogify logify; CTrade trade; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Configs MqlLogifyHandleDatabaseConfig m_config; m_config.directory = "db"; m_config.base_filename = "logs"; m_config.messages_per_flush = 5; //--- Handler Database CLogifyHandlerDatabase *handler_database = new CLogifyHandlerDatabase(); handler_database.SetConfig(m_config); handler_database.SetLevel(LOG_LEVEL_DEBUG); handler_database.SetFormatter(new CLogifyFormatter("hh:mm:ss","{date_time} [{levelname}] {msg}")); //--- Add handler in base class logify.AddHandler(handler_database); //--- Using logs logify.Info("Expert starting successfully", "Boot", "",__FILE__,__FUNCTION__,__LINE__); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- No positions if(PositionsTotal() == 0) { double price_entry = SymbolInfoDouble(_Symbol,SYMBOL_ASK); double volume = 1; if(trade.Buy(volume,_Symbol,price_entry,price_entry - 100 * _Point, price_entry + 100 * _Point,"Buy at market")) { logify.Debug("Transaction data | Price: "+DoubleToString(price_entry,_Digits)+" | Symbol: "+_Symbol+" | Volume: "+DoubleToString(volume,2), "CTrade", "",__FILE__,__FUNCTION__,__LINE__); logify.Info("Purchase order sent successfully", "CTrade", "",__FILE__,__FUNCTION__,__LINE__); } else { logify.Debug("Error code: "+IntegerToString(trade.ResultRetcode(),_Digits)+" | Description: "+trade.ResultRetcodeDescription(), "CTrade", "",__FILE__,__FUNCTION__,__LINE__); logify.Error("Failed to send purchase order", "CTrade", "",__FILE__,__FUNCTION__,__LINE__); } } } //+------------------------------------------------------------------+
EURUSDの1日分のストラテジーテスターを実行したところ、909件のログレコードが生成されました。設定通り、これらは.sqliteファイルに保存されています。ログファイルにアクセスするには、端末フォルダを開くか、ショートカットの「Ctrl/Cmd + Shift + D」を押すとファイルブラウザが表示されます。パスは「MQL5/Files/db/logs.sqlite」です。ファイルを入手したら、先ほどと同様にMetaEditorで直接開いて内容を確認できます。
これでログライブラリのさらなる一歩を踏み出すことができました。ログはデータベースに効率よく保存・取得できるようになり、より高いスケーラビリティと整理性を実現しました。
結論
この記事では、ログライブラリへのデータベース統合について、基本的な概念から専用ハンドラの実装まで幅広く解説しました。まず、ログ保存における従来のテキストファイルよりもスケーラブルで構造化された代替手段としてのデータベースの重要性と利点を紹介しました。次に、MQL5の文脈におけるデータベース利用の特徴や制約、そしてそれらを克服するためのソリューションについて検討しました。
最後に、実装結果の分析を行い、ログが正しく保存され、迅速かつ効率的にアクセスできることを確認しました。また、データベースへの直接クエリやログ監視のための専用ツールを使ったログの閲覧方法についても触れました。この検証プロセスは、実際の運用環境において実装したソリューションが機能的かつ効果的であることを保証する上で非常に重要でした。
これにより、ログライブラリの開発における一つの段階を完了しました。ログ保存にデータベースを採用することで、ログ管理がより整理され、アクセスしやすく、スケーラブルになりました。この手法により、大量のデータをより効率的に扱えるだけでなく、システムが記録する情報の分析や監視も容易になります。
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/17709
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。





- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索