执行没有 MQL5 数据绑定的查询
某些 SQL 查询是命令,你只需按原样将它们发送到引擎即可。它们既不需要变量输入也不需要结果。例如,如果我们的 MQL 程序需要在数据库中创建具有特定结构和名称的表、索引或视图,我们可以将其编写为带有 "CREATE ..." 语句的常量字符串。此外,使用此类查询进行记录的批量处理或其组合(合并、计算聚合指标和同类型修改)也很方便。也就是说,通过一个查询,可以转换整个表数据或基于它填充其他表。这些结果可以在后续查询中进行分析。
在所有这些情况下,最重要的是获得操作成功的确认。此类请求使用 DatabaseExecute 函数执行。
bool DatabaseExecute(int database, const string sql)
该函数在由 database 描述符指定的数据库中执行查询。请求本身作为准备好的 sql 字符串发送。
该函数返回操作状态标识:成功 (true) 或错误 (false)。
例如,我们可以用这个方法补充我们的 DBSQLite 类(描述符已经在对象内部)。
class DBSQLite
{
...
bool execute(const string sql)
{
return DatabaseExecute(handle, sql);
}
};
|
然后,创建新表(如果需要,事先创建数据库本身)的脚本可能如下所示 (DBcreateTable.mq5)。
input string Database = "MQL5Book/DB/Example1";
input string Table = "table1";
void OnStart()
{
DBSQLite db(Database);
if(db.isOpen())
{
PRTF(db.execute(StringFormat("CREATE TABLE %s (msg text)", Table))); // true
}
}
|
执行脚本后,尝试在 MetaEditor 中打开指定的数据库,并确保它包含一个带单个 "msg" 文本字段的空表。但这也可以通过编程方式完成(参见 下一节)。
如果我们使用相同的参数再次运行该脚本,将会遇到一个错误(尽管并非严重到强制程序关闭)。
database error, table table1 already exists
db.execute(StringFormat(CREATE TABLE %s (msg text),Table))=false / DATABASE_ERROR(5601)
|
这是因为不能重复创建已存在的表。但是,SQL 允许我们抑制这个错误,并且仅在表尚不存在时才创建它,否则几乎不执行任何操作并返回成功指示。为此,只需在查询中的表名前添加 "IF NOT EXISTS" 即可。
db.execute(StringFormat("CREATE TABLE IF NOT EXISTS %s (msg text)", Table));
|
在实践中,表用于存储关于应用领域中对象(如报价、交易、交易信号)的信息。因此,最好能根据 MQL5 中对象的描述来自动化表的创建过程。正如我们稍后将看到的,SQLite 函数能够将查询结果绑定到 MQL5 结构体(而非类)。有鉴于此,在 ORM 包装器的框架内,我们将开发一种机制,用于根据 MQL5 中特定类型的 struct 描述来生成 "CREATE TABLE" SQL 查询。
这需要在编译时以某种方式在通用列表中注册结构体字段的名称和类型,然后在程序执行阶段,就可以从这个列表中生成 SQL 查询。
MQL5 的编译阶段会解析几类实体,这些实体可用于识别类型和名称:
首先需要明确的是,收集到的字段描述是与特定结构体的上下文相关的,不应混淆,因为程序中可能包含许多具有潜在相同名称和类型的不同结构体。换句话说,最好是为每种类型的结构体在单独的列表中累积信息。模板类型非常适合此目的,其模板参数 (S) 将是应用程序结构体。我们将这个模板称为 DBEntity。
template<typename S>
struct DBEntity
{
static string prototype[][3]; // 0 - type, 1 - name, 2 - constraints
...
};
template<typename T>
static string DBEntity::prototype[][3];
|
在该模板内部,有一个多维数组 prototype,我们将在其中写入字段的描述。为了捕获所应用字段的类型和名称,需要在 DBEntity 内部声明另一个模板结构体 DBField:这次其参数 T 是字段本身的类型。在其构造函数中,我们拥有关于此类型的信息 (typename(T)),并且还通过参数获取字段的名称(以及可选的约束)。
template<typename S>
struct DBEntity
{
...
template<typename T>
struct DBField
{
T f;
DBField(const string name, const string constraints = "")
{
const int n = EXPAND(prototype);
prototype[n][0] = typename(T);
prototype[n][1] = name;
prototype[n][2] = constraints;
}
};
|
字段 f 虽未使用,但却是必需的,因为结构体不能为空。
假设我们有一个应用程序结构体 Data (DBmetaProgramming.mq5)。
struct Data
{
long id;
string name;
datetime timestamp;
double income;
};
|
我们可以创建其模拟结构体,该结构体继承自 DBEntity<DataDB>,但其字段基于 DBField 进行了替换,与原始集合相同。
struct DataDB: public DBEntity<DataDB>
{
DB_FIELD(long, id);
DB_FIELD(string, name);
DB_FIELD(datetime, timestamp);
DB_FIELD(double, income);
} proto;
|
通过将结构体的名称代入父模板参数,该结构体向程序提供了关于其自身特性的信息。
请注意与结构体声明一起进行的一次性 proto 变量定义。这是必要的,因为在模板中,只有在源代码中至少创建了一个此类型的对象时,每个特定的参数化类型才会被编译。重要的是,proto 对象的创建发生在程序启动的最开始,即全局变量初始化之时。
DB_FIELD 标识符下隐藏了一个宏:
#define DB_FIELD(T,N) struct T##_##N: DBField<T> { T##_##N() : DBField<T>(#N) { } } \
_##T##_##N;
|
以下是它针对单个字段展开的方式:
struct Type_Name: DBField<Type>
{
Type_Name() : DBField<Type>(Name) { }
} _Type_Name;
|
这里,该结构体不仅被定义,而且被立即创建:实际上,它替换了原始字段。
由于 DBField 结构体包含一个所需类型的单个变量 f,因此 Data 和 DataDB 的维度及内部二进制表示是相同的。通过运行 DBmetaProgramming.mq5 脚本,可以轻松验证这一点。
void OnStart()
{
PRTF(sizeof(Data));
PRTF(sizeof(DataDB));
ArrayPrint(DataDB::prototype);
}
|
它会向日志输出:
DBEntity<Data>::DBField<long>::DBField<long>(const string,const string)
long id
DBEntity<Data>::DBField<string>::DBField<string>(const string,const string)
string name
DBEntity<Data>::DBField<datetime>::DBField<datetime>(const string,const string)
datetime timestamp
DBEntity<Data>::DBField<double>::DBField<double>(const string,const string)
double income
sizeof(Data)=36 / ok
sizeof(DataDB)=36 / ok
[,0] [,1] [,2]
[0,] "long" "id" ""
[1,] "string" "name" ""
[2,] "datetime" "timestamp" ""
[3,] "double" "income" ""
|
然而,要访问这些字段,你需要编写一些不方便的代码:data._long_id.f、data._string_name.f、data._datetime_timestamp.f、data._double_income.f。
我们不会这样做,不仅(或者说不主要)因为不方便,而且因为这种构建元结构的方式与将数据绑定到 SQL 查询的原则不兼容。在接下来的章节中,我们将探讨允许在 MQL5 结构体中获取表记录和 SQL 查询结果的 database 函数。然而,只允许使用没有继承且不包含对象类型静态成员的简单结构体。因此,需要稍微改变揭示元信息的方式。
我们将必须保持结构体的原始类型不变,并实际上重复数据库的描述,确保没有差异(拼写错误)。这不是很方便,但目前没有其他方法。
我们将把 DBEntity 和 DBField 实例的声明移到应用程序结构体之外。在这种情况下,DB_FIELD 宏将接收一个额外的参数 (S),在该参数中需要传递应用程序结构体的类型(以前它是通过在结构体内部声明来隐式获取的)。
#define DB_FIELD(S,T,N) \
struct S##_##T##_##N: DBEntity<S>::DBField<T> \
{ \
S##_##T##_##N() : DBEntity<S>::DBField<T>(#N) {} \
}; \
const S##_##T##_##N _##S##_##T##_##N;
|
由于表列可以有约束,因此如果需要,也需要将它们传递给 DBField 构造函数。为此,我们添加几个带有相应参数的宏(理论上,一列可以有多个约束,但通常不超过两个)。
#define DB_FIELD_C1(S,T,N,C1) \
struct S##_##T##_##N: DBEntity<S>::DBField<T> \
{
S##_##T##_##N() : DBEntity<S>::DBField<T>(#N, C1) {} \
}; \
const S##_##T##_##N _##S##_##T##_##N;
#define DB_FIELD_C2(S,T,N,C1,C2) \
struct S##_##T##_##N: DBEntity<S>::DBField<T> \
{ \
S##_##T##_##N() : DBEntity<S>::DBField<T>(#N, C1 + " " + C2) {} \
}; \
const S##_##T##_##N _##S##_##T##_##N;
|
所有这三个宏以及后续的开发都添加到了头文件 DBSQLite.mqh 中。
务必注意,这种“自制”的对象到表的绑定方式仅用于向数据库输入数据,因为从表读取数据到对象的功能在 MQL5 中是使用 DatabaseReadBind 函数实现的。
我们还改进了 DBField 的实现。MQL5 类型与 SQL 存储类不完全对应,因此在填充 prototype[n][0] 元素时需要进行转换。这种转换是由静态方法 affinity 完成的。
template<typename T>
struct DBField
{
T f;
DBField(const string name, const string constraints = "")
{
const int n = EXPAND(prototype);
prototype[n][0] = affinity(typename(T));
...
}
static string affinity(const string type)
{
const static string ints[] =
{
"bool", "char", "short", "int", "long",
"uchar", "ushort", "uint", "ulong", "datetime",
"color", "enum"
};
for(int i = 0; i < ArraySize(ints); ++i)
{
if(type == ints[i]) return DB_TYPE::INTEGER;
}
if(type == "float" || type == "double") return DB_TYPE::REAL;
if(type == "string") return DB_TYPE::TEXT;
return DB_TYPE::BLOB;
}
};
|
此处使用的 SQL 通用类型的文本常量放置在一个单独的命名空间中:它们可能在 MQL 程序的某些不同地方需要用到,并且必须确保没有名称冲突。
namespace DB_TYPE
{
const string INTEGER = "INTEGER";
const string REAL = "REAL";
const string TEXT = "TEXT";
const string BLOB = "BLOB";
const string NONE = "NONE";
const string _NULL = "NULL";
}
|
为了方便,可能的约束预设也在其组中进行了描述(作为提示)。
namespace DB_CONSTRAINT
{
const string PRIMARY_KEY = "PRIMARY KEY";
const string UNIQUE = "UNIQUE";
const string NOT_NULL = "NOT NULL";
const string CHECK = "CHECK (%s)"; // requires an expression
const string CURRENT_TIME = "CURRENT_TIME";
const string CURRENT_DATE = "CURRENT_DATE";
const string CURRENT_TIMESTAMP = "CURRENT_TIMESTAMP";
const string AUTOINCREMENT = "AUTOINCREMENT";
const string DEFAULT = "DEFAULT (%s)"; // requires an expression (constants, functions)
}
|
由于某些约束需要参数(其位置用通常的 '%s' 格式修饰符标记),我们添加一项约束存在性检查。这是 DBField 构造函数的最终形式。
template<typename T>
struct DBField
{
T f;
DBField(const string name, const string constraints = "")
{
const int n = EXPAND(prototype);
prototype[n][0] = affinity(typename(T));
prototype[n][1] = name;
if(StringLen(constraints) > 0 // avoiding error STRING_SMALL_LEN(5035)
&& StringFind(constraints, "%") >= 0)
{
Print("Constraint requires an expression (skipped): ", constraints);
}
else
{
prototype[n][2] = constraints;
}
}
|
由于宏和辅助对象 DBEntity<S> 和 DBField<T> 的组合填充了一个原型数组,因此在 DBSQlite 类内部,可以实现自动生成用于创建结构体表的 SQL 查询。
createTable 方法使用应用程序结构体类型进行模板化,并包含一个查询存根 ("CREATE TABLE %s %s (%s);")。其第一个参数是可选指令 "IF NOT EXISTS"。第二个参数是表名,默认情况下取自模板参数的类型名称 typename(S),但如果需要,可以通过输入参数 name(如果不为 NULL)替换为其他名称。最后,括号中的第三个参数是表列的列表:它由辅助方法 columns 基于数组 DBEntity <S>::prototype 形成。
class DBSQLite
{
...
template<typename S>
bool createTable(const string name = NULL,
const bool not_exist = false, const string table_constraints = "") const
{
const static string query = "CREATE TABLE %s %s (%s);";
const string fields = columns<S>(table_constraints);
if(fields == NULL)
{
Print("Structure '", typename(S), "' with table fields is not initialized");
SetUserError(4);
return false;
}
// attempt to create an already existing table will give an error,
// if not using IF NOT EXISTS
const string sql = StringFormat(query,
(not_exist ? "IF NOT EXISTS" : ""),
StringLen(name) ? name : typename(S), fields);
PRTF(sql);
return DatabaseExecute(handle, sql);
}
template<typename S>
string columns(const string table_constraints = "") const
{
static const string continuation = ",\n";
string result = "";
const int n = ArrayRange(DBEntity<S>::prototype, 0);
if(!n) return NULL;
for(int i = 0; i < n; ++i)
{
result += StringFormat("%s%s %s %s",
i > 0 ? continuation : "",
DBEntity<S>::prototype[i][1], DBEntity<S>::prototype[i][0],
DBEntity<S>::prototype[i][2]);
}
if(StringLen(table_constraints))
{
result += continuation + table_constraints;
}
return result;
}
};
|
对于每一列,描述都包含名称、类型和可选的约束。此外,还可以传递一个针对表的通用约束 (table_constraints)。
在将生成的 SQL 查询发送给 DatabaseExecute 函数之前,createTable 方法会向日志输出查询文本的调试信息(ORM 类中所有此类输出都可以通过替换 PRTF 宏来集中禁用)。
现在一切就绪,可以编写测试脚本 DBcreateTableFromStruct.mq5 了,该脚本将通过结构体声明在 SQLite 中创建相应的表。在输入参数中,我们只设置数据库的名称,程序将根据结构体的类型自行选择表名。
#include <MQL5Book/DBSQLite.mqh>
input string Database = "MQL5Book/DB/Example1";
struct Struct
{
long id;
string name;
double income;
datetime time;
};
DB_FIELD_C1(Struct, long, id, DB_CONSTRAINT::PRIMARY_KEY);
DB_FIELD(Struct, string, name);
DB_FIELD(Struct, double, income);
DB_FIELD(Struct, string, time);
|
在主函数 OnStart 中,我们通过调用具有默认设置的 createTable 来创建表。如果我们不希望在下次尝试创建它时收到错误标志,则需要将 true 作为第一个参数 (db.createTable<Struct> (true)) 传递。
void OnStart()
{
DBSQLite db(Database);
if(db.isOpen())
{
PRTF(db.createTable<Struct>());
PRTF(db.hasTable(typename(Struct)));
}
}
|
hasTable 方法通过表名检查数据库中是否存在该表。我们将在 下一节中探讨此方法的实现。现在,我们运行该脚本。首次运行后,表成功创建,你可以在日志中看到 SQL 查询(它显示时有换行,与我们在代码中构建时的方式一致)。
sql=CREATE TABLE Struct (id INTEGER PRIMARY KEY,
name TEXT ,
income REAL ,
time TEXT ); / ok
db.createTable<Struct>()=true / ok
db.hasTable(typename(Struct))=true / ok
|
第二次运行将从 DatabaseExecute 调用中返回一个错误,因为该表已存在,hasTable 的结果也进一步证明了这一点。
sql=CREATE TABLE Struct (id INTEGER PRIMARY KEY,
name TEXT ,
income REAL ,
time TEXT ); / ok
database error, table Struct already exists
db.createTable<Struct>()=false / DATABASE_ERROR(5601)
db.hasTable(typename(Struct))=true / ok
|