SQL と MQL5:SQLite データベースとの連携

--- | 23 12月, 2015

小型迅速信頼性
この3つのどれでも選んでください。


はじめに

開発者の多くがデータ保管のためにデータベースを利用することを考えますが、躊躇したままです。 SQL サーバーのインストールに余計な時間がどれほど取られるかわかっているからです。そして一方プログラマーにとってそれはさほど難しいことではなく(データベース管理システム(DBMS)がすでに別の目的でインストールされていれば)、それは明らかに、ソフトウェアを一緒にインストールすることに最終的に消極的な一般ユーザーにとっての問題なのです。

現在取り組んでいるソリューションがほとんど利用されないことがわかっているため、開発者の多くは DBMS に取り組もうとしません。結果、彼らはファイルの操作を来ないます(多様な使用データを与えられ、複数のファイルに対処する必要があることがよくあります。):CSV、頻度は低いものの XML や JSON、厳格なストラクチャサイズのバイナリファイルなど。

ただ、 SQL サーバーのすばらしい代替があることがわかりました。そしてすべてをみなさんのプロジェクト内で行われるため、追加のソフトウェアをインストールする必要はありません。それでも SQLのフル性能を利用できるのです。これから SQLiteについてお話していきたいと思います。

本稿のめざすところはみなさんに速くSQLite をスタートしてもらうことです。そのため、細かい部分や考えられるパラメータセットすべてや関数フラグについては述べませんが、その代り、SQL コマンドを実行するための軽い接続ラッパを作成し、使用を実地で見ていきます。

本稿を読み進むにあたりみなさんには以下が必要となります。

コンテンツ

1. SQLite 原理
2. SQLite3 API
2.1. データベースのオープンとクローズ
2.2. SQL クエリの実行
2.3. テーブルからのデータ取得
2.4. バインディングによるパラメータデータ書き込み
2.5. トランザクション/複数行挿入(トレード赤運とのディールテーブル作成例)
3. Compiling 64 バージョンのコンパイル (sqlite3_64.dll)


1. SQLite 原理

SQLite はキー機能がローカルでインストールされ SQL たサーバーにない RDBMS です。みなさんのアプリケーションはここではサーバーとして見られています。SQLite データベースとの連携は基本的にファイル(ディスクドライバー上またはメモリ内)との連携です。データはすべてアーカイブされるか特別な方法でインストールする必要はなく別のコンピュータに移動されます。

SQLite により開発者とユーザーがまぎれもないメリットのかずかずの恩恵を受けることができるのです。

SQLiteと連携する方法は3とおりあります。

  1. API 関数の完全セットを持つDLL ファイルを使用する。
  2. EXE ファイルに対するシェルコマンドを使用する。
  3. SQLite API のソースコードをインクルードしてプロジェクトをコンパイルする。

本稿では最初の選択肢について説明します。MQL5でもっとも通例なものです。


2. SQLite3 API

コネクタ操作のために以下の SQLite 関数を使用する必要があります。

//--- 一般的関数
sqlite3_open
sqlite3_prepare
sqlite3_step
sqlite3_finalize
sqlite3_close
sqlite3_exec

//--- エラー説明取得のための関数
sqlite3_errcode
sqlite3_extended_errcode
sqlite3_errmsg

//--- データ保存のための関数
sqlite3_bind_null
sqlite3_bind_int
sqlite3_bind_int64
sqlite3_bind_double
sqlite3_bind_text
sqlite3_bind_blob

//--- データ取得のための関数
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 関数に送信されるポインタサイズに配慮することは重要です。名前を分けます。

// 32 ビットターミナル用
#define PTR32                int
#define sqlite3_stmt_p32     PTR32
#define sqlite3_p32          PTR32
#define PTRPTR32             PTR32

// 64 ビットターミナル用
#define PTR64                long
#define sqlite3_stmt_p64     PTR64
#define sqlite3_p64          PTR64
#define PTRPTR64             PTR64

必要に応じて、API 関数はすべて32 ビットまたは 64 ビットポインタにオーバーロードします。コネクタのポインターはすべて64ビットであることに注意してください。それらは32ビットから直接オーバーロードされたAPI 関数内で変換されます。関数インポートソースコードは SQLite3Import.mqhに提供されています。

SQLite データタイプ

SQLite バージョン 3 にはデータが5タイプあります。

タイプ 説明
NULL NULL 値
INTEGER 格納される値の大きさに応じて 1, 2, 3, 4, 6, または 8 バイトで格納される整数
REAL 8 バイト実数
TEXT UTF-8 または UTF-16 エンコーディングによって格納される終了文字 \0 を持つテキスト文字列
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] -ファイルが現在の位置で開いていればパス名またはファイル名
pDb     [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 クエリはすべて最低3段階を経る必要があります。

  1. sqlite3_prepare-検証とステートメントリストの受け取り
  2. sqlite3_step-これらステートメントの実行
  3. sqlite3_finalize -完了とメモリクリア

このストラクチャは主にテーブルの作成と削除に適しています。また非バイナリデータを書くのにも適しています。すなわち、SQL クエリがそれ自体の実行が正常であるステートメント以外はデータを返さないことを示しています。

クエリがデータ受け取りやバイナリデータ書き込みをしたら、第二段階で関数 sqlite3_column_хх またはsqlite3_bind_хх がそれぞれ受け取りと書き込みに使用されます。これら関数は次のセクションで細かく説明します。

シンプルな SQLクエリを実行するための CSQLite3Base::Query メソッドを書きます。

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
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_preparesqlite3_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] - 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. While current step result sqlite3_step == SQLITE_ROW
    1. sqlite3_column_хх-文字列セルを読みます。
  4. sqlite3_finalize

データの読み出しと書き込みに関する大きなセクションに近づきつつあるので、データ交換全体で使用される3つのコンテナクラスについて説明するにはよいタイミングです。必要なデータモデルはデータがデータベース内にどのように格納されるかによります。
データベース
|
Tableは列の配列です。
|
Row はセルの配列です。
|
Cell は任意長のバイトバッファです。

//+------------------------------------------------------------------+
//| 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 タイプは5つの基本的な SQLite3 タイプに追加され、最初のセル状態を特定できるようにしていることに注意が必要です。同様に分割された関数sqlite3_bind_intXXsqlite3_column_intXX とに従い INTEGER タイプ全体を CT_INTCT_INT64 に分割します。

データ取得

セルからデータを得るには、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);
  }

この関数はひじょうに大型ですが、現在のステートメントからデータを読みそれをセルに格納するだけです。

また第一パラメータとして受け取ったデータに対しCSQLite3Table コンテナテーブルを追加することで CSQLite3Base::Query 関数をオーバーロードします。

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

次のコマンド PrintTablePrint(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`;");

クエリ結果は PrintTablePrint(tbl))によって同様にプリントアウトされます。既存テーブルを確認することができます。

例からわかるように、クエリ実行結果は tbl 変数に入れられます。その後、みなさんの記述で簡単にそれらを取得、処理することができます。

2.4. バインディングによるパラメータデータ書き込み

初心者の方に重要なもう一つ別の話題は『便利な』フォーマットによりデータベースにデータを書き込むことです。もちろんここではバイナリデータです。一般的な INSERT や UPDATE テキストステートメントでは直接渡すことができません。文字列は最初のゼロが検出されるとき簡潔しているとみなされるためです。文字列そのものが単一クオートを持つ場合も同じ問題が発生します '

遅いバインディングが便利が場合もります。とくにテーブルの幅が広い場合です。すべてのフィールドを1行に書くことは難しく信頼性がありません。何かを見落としやすいからです。sqlite3_bind_хх シリーズの関数が処理をバインディングするのに必要です。

バインディングを取り入れるには、渡されたデータの代わりにテンプレートを挿入します。一例を考察します。 "?"記号です。別の言い方をすると、UPDATE クエリは次のように記述されます。

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


それから関数 sqlite3_bind_double および sqlite3_bind_textopen_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. Transactions/複数行挿入

この話題を進める前印、Multirow Insert関数をもう一つ知る必要があります。前のセクションで、リクエストの三段階処理を説明しました。:準備+ステップ+完成です。ただし、代替(ある場合、シンプルあるいは重要な)ソリューションがあります。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 を返し、それ以外はエラーコードを返します。

その主な目的は三段階の構築をせず、1度の呼び出しでクエリを実行することです。

コネクタにその呼び出しを追加します。

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

Transactions

ここでテーブルに何千行と追加する必要があるとします。これをすべてループに挿入する場合、

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

実行速度はひじょうに遅いものとなります(10秒以上!)。そのような実装はSQLiteではお薦めしません 。ここでのもっとも適切な解決方法は transactionsを使用することです。: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());
  }

スクリプトがアカウントに適用されたあと、それはすぐにアカウントディールをテーブルに挿入します。

ターミナルのジャーナルに統計が表示されます。

スクリプトでいろいろすることができます。行にはBEGINROLLBACK COMMITでコメントします。みなさんのアカウントに100以上のディールがあれば、すぐに違いがわかります。ところで、いくらかの テストによると、SQLite のトランザクションは MySQL や PostgreSQLより速いのです。


3. 64 ビットバージョンのコンパイル (sqlite3_64.dll)

  1. SQLite ソースコード(融合)をダウンロードし、sqlite3.cファイルを検索します。
  2. sqlite-dll-win32 をダウンロードしそこからsqlite3.dllファイルを抽出します。
  3. LIB.EXE /DEF:sqlite3.def コンソールコマンドを dll ファイルが抽出されたフォルダで実行します。lib.exe ファイルへのパスが PATH システム変数に設定されているかお手持ちの Visual Studioにあることを確認します。
  4. 64ビットプラットフォーム用コンフィギュレーションのリリースを選択し、DLL プロジェクトを作成します。
  5. ダウンロードされた DLL と、取得したsqlite3.def ファイルをプロジェクトに追加します。コンパイラが defファイルからの関数のなかのいくつかを受け付けなければ、それにコメントを付けます。
  6. 次のパラメータをプロジェクト設定に設定します。
    C/C++ --> General --> Debug Information Format = Program Database (/Zi)
    C/C++ --> Precompiled Headers --> Create/Use Precompiled Header = Not Using Precompiled Headers (/Yu)
  7. コンパイルし64ビットの dll を取得します。


おわりに

本稿がSQLiteを習得するのに欠くことのできないガイドとなるよう願っています。おそらく将来のプロジェクトで利用することになるでしょう。この概要はアプリケーションにとって完璧で信頼できるソリューションとしてのSQLite の機能性についていくらかの洞察を提供しました。

本稿で私はトレードデータを処理するときに出会う可能性のある場合をすべて説明しました。宿題として、各シンボルのテーブルにティックを挿入してシンプルなティックコレクタを作成することをお薦めします。クラスライブラリのソースコードとテストスクリプトは下の添付にあります。

グッドラック!そしてビッグな利益を!