English Русский Deutsch 日本語
preview
简化MQL5中的数据库操作(第一部分):数据库与SQL简介

简化MQL5中的数据库操作(第一部分):数据库与SQL简介

MetaTrader 5 — 示例 |
22 4
joaopedrodev
joaopedrodev

引言

谈及MQL5,大多围绕指标、智能交易系统(EA)、交易策略以及回测展开讨论。但每一位认真做自动化交易的交易者或开发者,在开发到一定阶段后都会意识到,数据持久化十分关键,而数据库正是用来解决该问题的工具。您可能会产生疑惑:“我已经把运行结果保存在CSV或者TXT文本文件里了,何必再用数据库把事情变复杂?” 答案就在于数据库所具备的数据规整性、运行性能与可靠性,在处理海量信息或是复杂业务操作时,这些优势尤为突出。

在MQL5的场景下,刚接触时您会觉得讨论数据库有点小题大做。毕竟这门语言主要面向交易、指标以及自动交易机器人。然而,一旦您的策略需要处理大批量数据、复杂回测或者详尽的订单记录,普通文件这种简单方案很快就会暴露出能力不足。这时学会数据库的创建、访问与修改,会成为改善交易开发流程的有力工具。

刚开始接触MQL5数据库功能的开发者,会看到大量原生函数以及零散的示例代码。用这些函数保存和查询记录已经足够,但很快就会面临疑问:在实际项目当中,如何把这些代码整理成简洁、可复用的形式?本系列文章不止局限于官方文档内容。我们从SQLite基础函数入手,循序渐进,在MQL5中搭建一套**迷你ORM(TickORM)**。设计思路是把底层直接调用的语句封装为架构优良的程序分层,借鉴Java的JDBC与JPA设计思想,并适配MetaTrader软件的运行环境。

本文讲解MQL5环境下数据库与SQL的基础概念。把底层数据库函数封装到类、接口当中,最终完成ORM的开发,为后续文章做准备。


什么是数据库?有什么作用?

从技术上讲,数据库是一套将信息结构化存储的体系,可以对已存储的数据执行保存、查询和修改操作。它能够保障数据完整性,在不同数据集之间建立关联关系,同时保证各项操作的执行效率。

文件存储方案和数据库存储方案之间最实际的区别,在于数据组织形式与访问便捷程度。如CSV或TXT这类普通文件,每次读取或者修改数据,都需要遍历全部内容、处理字符串、应对各类格式错误,还得时刻提防文件损坏的风险。而数据库把这些底层细节做了封装抽象:您可以直接检索指定的单条记录、批量修改多项数据,还可以保障完整事务执行,避免发生数据丢失。

但在数据库内部是如何完成这种结构化管理的?您可以把数据库的基础结构类比为电子表格,示例如下:

  • 数据库中的每一张表,相当于电子表格中的一个标签页,专门存放一类信息(例如:交易记录表Trades)。
  • 每张表内部都设有列,和电子表格一样,列代表每条数据项的属性(例如id、交易品种、价格等)。
  • 而表中的每一行,则对应一条独立记录,也就是一笔具体交易或已成交订单。

这种由表、列、行构成的简单架构,是实现快速、结构化查询的基础。您可以把它想象成无数张可相互关联的电子表格;并且它支持交叉比对数据、使用高级筛选,轻松处理海量数据,不会像普通表格那样难以维护。

这正是我们要在MQL5中自研ORM的出发点。思路很简单:不再每次操作数据表(新建记录、查询数据、更新字段)时手动编写SQL语句。我们会封装一层中间层,把表映射为实体,把列映射为类属性。这就是本系列文章将要开发项目的核心,它将作为底层支撑,用来构建更健壮、可扩展的交易系统。将这套ORM命名为TickORM。

简言之,数据库是搭建可靠、灵活解决方案的基础,而我们开发的ORM,就是连接数据库底层与MQL5业务代码的桥梁。

为方便大家测试,所有示例代码都附在文末。您可以按需查阅、复制和改造,无需手动拼接零散代码片段。

注意:从本节开始,部分代码示例会向控制台输出日志记录。文中使用了我开发的Logify库,该库专门用于简化MQL5的日志追踪与调试工作。如果您想深入了解它的原理,或是跟着逐步实现,我写了一整个系列文章讲解该库的开发过程。


SQL与MQL5原生数据库函数

操作数据库并不复杂。幸运的是,MQL5提供了一套原生函数,可以直接在代码内部创建、访问和操作数据。这些函数充当代码与数据库之间的桥梁。

它们与SQL的关联非常直接:这些原生函数构成查询执行层。换句话说,您在MQL5代码里编写SQL语句,由原生函数完成语句预处理、执行以及读取返回结果。这就意味着无需依赖外部库,您就能执行各类复杂操作,如SELECT(查询)、INSERT(插入)、UPDATE(更新)、DELETE(删除),甚至创建数据表。

在进入实际代码实现之前,我们先了解每个函数及其作用。全部原生数据库函数如下,后续我们会逐一讲解每个函数的用法:

  • DatabaseOpen(string filename, uint flags):打开或创建指定文件形式的数据库
  • DatabaseClose(int database):关闭数据库
  • DatabaseImport(int database,const string table,const string filename,uint flags,const string separator,ulong skip_rows,const string skip_comments):将文件数据导入数据表
  • DatabaseExport(int database,const string table_or_sql,const string filename,uint flags,const string separator):将数据表或SQL查询结果导出为CSV文件
  • DatabasePrint(int database,const string table_or_sql,uint flags):在EA日志窗口打印数据表或SQL执行结果
  • DatabaseTableExists(int database,string table):检查数据库内是否存在指定数据表
  • DatabaseExecute(int database, string sql):在指定数据库中执行SQL语句
  • DatabasePrepare(int database, string sql):创建查询预编译语句句柄,后续可配合DatabaseRead()读取结果
  • DatabaseReset(int request):重置预编译查询,通常在调用DatabasePrepare()之后使用
  • DatabaseBind(int request,int index,T value):为预编译查询绑定参数值
  • DatabaseRead(int request):读取查询结果集的下一条记录
  • DatabaseFinalize(int request):释放DatabasePrepare()创建的预编译语句资源
  • DatabaseTransactionBegin(int database):开启执行事务
  • DatabaseTransactionCommit(int database):完成执行事务
  • DatabaseTransactionRollback(int database):回滚事务
  • DatabaseColumnsCount(int request):获取查询结果中的字段总数
  • DatabaseColumnName(int request,int column,string& name):根据索引获取字段名称
  • DatabaseColumnType(int request,int column):根据索引获取字段数据类型
  • DatabaseColumnSize(int request,int column):获取字段占用字节大小
  • DatabaseColumnText(int request,int column,string& value):读取当前记录中指定字段的字符串类型值
  • DatabaseColumnInteger(int request,int column,int& value):读取当前记录中指定字段的int整型值
  • DatabaseColumnLong(int request,int column,long& value):读取当前记录中指定字段的long长整型值
  • DatabaseColumnDouble(int request,int column,double& value):读取当前记录中指定字段的double浮点值

MetaQuotes官方文档《MQL5数据库函数》提供了全部函数、参数以及示例的完整说明。


1. 数据库的打开与关闭

我们从基础操作开始,建立数据库连接。该操作由DatabaseOpen函数实现,函数接收数据库文件名以及可选的标识位(flags)。标识位用于定义数据库的打开模式,可选取值如下:

值 描述
DATABASE_OPEN_READONLY
只读模式
DATABASE_OPEN_READWRITE 以读写模式打开
DATABASE_OPEN_CREATE 文件不存在时自动创建
DATABASE_OPEN_MEMORY 在RAM中创建数据库
DATABASE_OPEN_COMMON 文件存放于所有客户端终端共用的公共目录
int OnInit()
  {
   //--- Open connection
   int handler = DatabaseOpen("database.sqlite",DATABASE_OPEN_CREATE|DATABASE_OPEN_READWRITE);
   if(handler == INVALID_HANDLE)
     {
      logs.Error("Failed to open database","TickORM");
      return(INIT_FAILED);
     }
   logs.Info("Open database","TickORM");
   
   //--- Here you would perform queries, inserts or updates
   
   //--- Close connection
   logs.Info("Closed database","TickORM");
   DatabaseClose(handler);
   
   return(INIT_SUCCEEDED);
  }

第一个参数指定数据库名称,本例中我使用"database",并补上数据库文件的后缀名“.sqlite”,这就是数据库文件的扩展名。虽然它是一种基于文件的数据库,但使用方式完全不受影响,数据的读取与查询均遵循标准SQL。

本例中,DATABASE_OPEN_READWRITE开启读写权限,DATABASE_OPEN_CREATE保证当文件不存在时自动创建。所有操作完成后,调用DatabaseClose关闭数据库连接,释放系统资源。

运行这段代码后,会自动在<MQL5/Files>文件夹下生成数据库文件,您可以在MetaEditor中查看该目录。


2. 创建数据表

成功连接数据库后的第一步,就是定义用于存储数据的结构。在SQL中,通过CREATE TABLE语句完成该操作,该语句指定表名以及表内包含的字段。

在MQL5中,创建数据表会用到两个主要的函数:

  • DatabaseTableExists(handler, table_name):检查数据库中是否存在指定数据表,返回true或false。
  • DatabaseExecute(handler, sql):直接执行SQL语句。

最常用的执行流程:检查表是否存在 → 如果不存在,则新建数据表。实际示例如下:

//--- Create table
if(!DatabaseTableExists(handler,"trades"))
  {
   logs.Info("Creating user table...","TickORM");
   if(DatabaseExecute(handler,"CREATE TABLE trades (id INTEGER PRIMARY KEY AUTOINCREMENT,symbol TEXT, price FLOAT, takeprofit FLOAT, stoploss FLOAT, volume FLOAT);"))
     {
      logs.Info("'Trades' table created","TickORM");
     }
   else
     {
      logs.Error("Failed to create table",GetLastError(),"TickORM");
     }
  }

在此代码段中,我们使用CREATE TABLE trades(...)创建一张名为trades的数据表,用于存储交易基础信息:id(自动生成的唯一标识)、交易品种、开仓价格、止盈价、止损价以及交易量。

由于代码先用DatabaseTableExists判断表是否存在,因此数据表已存在时,建表语句不会被重复执行。请始终采用这种先判断表是否存在并且按需建表的开发范式。

当前数据库结构的可视化效果如下图所示:

MetaEditor支持直接查看该数据库,只需双击对应的数据库文件,编辑器会自动切换到对应的数据库浏览标签页:

  • 蓝色箭头标出的是数据库中的数据表列表;
  • 黄色区域是SQL命令输入区域(既可以手动在这里输入SQL,也可以在代码中执行,下一节会给出示例);
  • 紫色标注是"Database"浏览标签页,如果需要切回查看代码文件(.mqh或.mq5),切换到"MQL5"标签页即可。


3. 向数据表中插入数据

接下来,我们向刚刚建好的数据表中插入数据。在SQL中,使用INSERT INTO语句新增数据记录。其语法结构如下:

INSERT INTO table_name
(column1,column2,...)
VALUES
(value1,value2,...)

直接拼接数据时,字符串类型的值需要用单引号包裹(如 'example')。我们仍然使用DatabaseExecute函数执行语句,并校验执行结果是否成功。

//--- Insert data
ResetLastError();
if(DatabaseExecute(handler,"INSERT INTO trades (symbol,price,takeprofit,stoploss,volume) VALUES ('EURUSD',1.16110,1.15490,1.16570,0.01);"))
  {
   logs.Info("Data saved to the trades table","TickORM");
  }
else
  {
   logs.Error("Failed to save data to the trade table",GetLastError(),"TickORM");
  }

这里,我们向trades表中插入一条记录,为每个字段赋值:

  • id:由数据库自动生成
  • symbol:'EURUSD'
  • price:1.16110
  • takeprofit:1.15490
  • stoploss:1.16570
  • volume:0.01

当前阶段的可视化效果如下:


4. 从数据表中读取数据

把数据存进数据库只算完成了一半,数据库真正的优势在于可以按结构化方式查询和提取数据。在MQL5中,读取数据有一套固定的流程:

  • 使用DatabasePrepare预编译查询语句,传入SQL命令。
  • 使用DatabaseRead逐行遍历查询结果集。
  • 使用DatabaseFinalize释放查询句柄,回收内存资源。

基础示例如下:

//--- Data reading
int request = DatabasePrepare(handler,"SELECT * FROM trades;");

//--- Number of table columns that were read
int size_cols = DatabaseColumnsCount(request);

//--- While data is available for reading (reading each line)
while(DatabaseRead(request))
  {
   //--- Scan across all columns of the current line of reading
   for(int j=0;j<size_cols;j++)
     {
      string name = "";
      DatabaseColumnName(request,j,name);
      logs.Info(name,"TickORM");
     }
  }

//--- Reset query
DatabaseFinalize(request);

执行后,我们会在日志中得到如下输出:

2025.08.21 10:14:51 [INFO]: Open database
2025.08.21 10:14:51 [INFO]: id
2025.08.21 10:14:51 [INFO]: symbol
2025.08.21 10:14:51 [INFO]: price
2025.08.21 10:14:51 [INFO]: takeprofit
2025.08.21 10:14:51 [INFO]: stoploss
2025.08.21 10:14:51 [INFO]: volume
2025.08.21 10:14:51 [INFO]: Closed database

这里,使用查询语句SELECT * FROM trades,读取"trades"表中的全部记录。每次调用DatabaseRead(request),游标都会移动到下一行,内层循环遍历所有字段,仅打印字段名称。

可视化效果如下图所示:

  • 外层循环(while(DatabaseRead(request))) 逐行遍历(图中绿色方框)。
  • 内层循环(for(int j=0;j<size_cols;j++))在当前行内逐列(字段)遍历(图中红色方框)。

两者结合,可以逐个单元格访问数据库内容,操作方式和浏览电子表格十分相似。

继续往下读取字段值,前面的示例只列出了字段名称。我们需要拿到存储的数据值,这时要根据字段类型选用对应的读取函数:

  • DatabaseColumnType → 动态获取字段的数据类型;
  • DatabaseColumnText → 读取文本类型(例如交易品种或名称);
  • DatabaseColumnDouble → 读取浮点数(例如价格);
  • DatabaseColumnLong或DatabaseColumnInteger → 读取整型数值(例如ID)

完整示例如下:

//--- Data reading
int request = DatabasePrepare(handler,"SELECT * FROM trades;");

//--- Number of table columns that were read
int size_cols = DatabaseColumnsCount(request);

//--- While data is available for reading (reading each line)
while(DatabaseRead(request))
  {
   //--- Scan across all columns of the current line of reading
   for(int j=0;j<size_cols;j++)
     {
      string name = "";
      DatabaseColumnName(request,j,name);
      
      ENUM_DATABASE_FIELD_TYPE type = DatabaseColumnType(request,j);
      if(type == DATABASE_FIELD_TYPE_TEXT)
        {
         string data = "";
         DatabaseColumnText(request,j,data);
         logs.Info(name + " | "+data,"TickORM");
        }
      else if(type == DATABASE_FIELD_TYPE_FLOAT)
        {
         double data = 0;
         DatabaseColumnDouble(request,j,data);
         logs.Info(name + " | "+DoubleToString(data,5),"TickORM");
        }
      else if(type == DATABASE_FIELD_TYPE_INTEGER)
        {
         long id = 0;
         DatabaseColumnLong(request,j,id);
         logs.Info(name + " | "+IntegerToString(id),"TickORM");
        }
     }
  }

//--- Reset query
DatabaseFinalize(request);

执行后,我们会在日志中得到如下输出:

2025.08.21 10:18:04 [INFO]: Open database
2025.08.21 10:18:04 [INFO]: id | 1
2025.08.21 10:18:04 [INFO]: symbol | EURUSD
2025.08.21 10:18:04 [INFO]: price | 1.16110
2025.08.21 10:18:04 [INFO]: takeprofit | 1.15490
2025.08.21 10:18:04 [INFO]: stoploss | 1.16570
2025.08.21 10:18:04 [INFO]: volume | 0.01000
2025.08.21 10:18:04 [INFO]: Closed database

在本例中,除了遍历行与列,我们还可以读取每个单元格里的内容。返回示意图的类比:

  • 灰色方框代表整张数据表(trades表)。
  • 每个绿色方框是DatabaseRead读取出来的一行记录。
  • 每个红色方框代表一个字段,其值可以是文本(字符串)、小数(双精度)或者整数(整型)。
  • 调用DatabaseColumnType,就相当于在询问:“这个红色方框内的数据是什么类型?”
  • 接下来调用对应的读取函数(DatabaseColumnText、DatabaseColumnDouble、DatabaseColumnLong)获取字段值。


5. 更新表数据

在实际开发场景中,经常需要调整已存在的数据。这时就要用到UPDATE语句,它可以修改数据表中指定行的一个或多个字段的值。

SQL的基础语法结构如下:

UPDATE table_name
SET column1=value1, column2=value2, ...
WHERE condition;

在MQL5中,更新记录依靠DatabaseExecute函数,由它直接把SQL语句发送给数据库执行。示例如下:

//--- Update data
ResetLastError();
if(DatabaseExecute(handler,"UPDATE trades SET volume=0.1 WHERE id=1"))
  {
   logs.Info("Trades table data updated","TickORM");
  }
else
  {
   logs.Error("Failed to update trades table data",GetLastError(),"TickORM");
  }

这里,我们展示一个典型场景: trades表中id等于1的记录,找到之后,将volume字段值修改为0.1。

这种写法在交易开发中非常实用。设想您保存了一笔初始交易量为0.01手的交易记录,后续策略调整,需要把交易量修正为0.1手。只需要一条简单的UPDATE语句,数据库中的数据就能可靠同步,无需删除原有记录再重新新建。


6. 删除表数据

删除数据需要使用SQL的DELETE语句,用于删除表中的行记录。语句通用格式如下:

DELETE FROM table_name
WHERE condition;

实现方式与之前一致,使用DatabaseExecute函数提交这条SQL语句:

//--- Delete data
ResetLastError();
if(DatabaseExecute(handler,"DELETE FROM trades WHERE id=1"))
  {
   logs.Info("Deleted trades table data","TickORM");
  }
else
  {
   logs.Error("Failed to delete data to the trade table",GetLastError(),"TickORM");
  }

在本例中,我们让数据库从trades表删除id字段等于1的记录。这是一次精准、可控且安全的删除操作。

重要提醒:与UPDATE语句相似,WHERE子句必不可少,防止误删除整张表的数据。

到这里,我们就完成了基础的数据增删改查完整流程:建表、插入、更新和删除。掌握了这些内容,就为后续的学习打下了坚实的基础。


7. 删除数据表

如果后续不再需要某数据表,我们可以直接删除该表。

//--- Delete table
ResetLastError();
if(DatabaseExecute(handler,"DROP TABLE trades"))
  {
   logs.Info("Trades table deleted","TickORM");
  }
else
  {
   logs.Error("Failed to delete trades table",GetLastError(),"TickORM");
  }

需要重点强调的是,DROP TABLE操作不可撤销。DELETE FROM trades仅仅删除表内记录,数据表结构仍然保留;而DROP TABLE会同时删除数据表本身以及里面的全部数据。一旦执行该语句,想要写入新数据就必须重新创建数据表。


8. 事务与数据完整性

接下来是一个非常关键的知识点 —— 数据库的数据完整性。任何操作数据的应用程序中,数据完整性都是重中之重。设想这样一个场景:我们需要写入多条记录。如果其中一条插入失败,而其余记录已经写入了数据库,就会造成数据不一致。事务正是用来解决此类问题。

事务是一组被视为一个原子单元的SQL操作:

  • 要么内部所有语句全部执行生效(提交**commit**),
  • 要么所有修改全部作废(回滚**rollback**)。

MQL5提供原生函数来控制事务流程:

  • DatabaseTransactionBegin(handler) → 开启事务;
  • DatabaseTransactionCommit(handler) → 确认并提交所有修改;
  • DatabaseTransactionRollback(handler) → 撤销自事务开启以来的全部改动。

关于事务用法的示例如下:

//--- Insert with transaction
string sql="INSERT INTO trades (symbol,price,takeprofit,stoploss,volume) VALUES ('EURUSD',1.16110,1.15490,1.16570,0.01);";
DatabaseTransactionBegin(handler);
logs.Info("Inserting data, preparing transaction","TickORM");
if(!DatabaseExecute(handler,sql))
  {
   logs.Error("Transaction failed, reverting to previous state",GetLastError(),"TickORM");
   DatabaseTransactionRollback(handler);
  }
else
  {
   logs.Info("Transaction complete, changes saved","TickORM");
   DatabaseTransactionCommit(handler);
  }

在此代码段中:

  1. 通过DatabaseTransactionBegin开启事务;
  2. 尝试执行插入操作(DatabaseExecute);
  3. 如果出现异常,调用DatabaseTransactionRollback撤销全部改动;
  4. 如果一切正常,则通过DatabaseTransactionCommit确认提交所有变更。

该机制的价值在于保证不会留存不完整的数据。它提升了管控能力:您可以把多项操作打包在一起,只有确认全部执行无误后,才会真正写入数据库。这也提升了数据的安全性:一旦发生意外(网络中断、断电、代码异常),数据库会自动恢复到操作前的状态,数据不会受损。

设想一下需要在多张数据表中记录十笔不同操作的场景。如果第八笔操作失败,在不使用事务的情况下,会成功保存七笔,剩下三笔缺失,造成数据不一致。使用事务时,您只需执行回滚,数据库可回到操作前的状态。


9. 导入与导出数据

除了事务之外,数据导入导出功能也大幅提升了实用性和数据安全性。借助该功能,可以定期对数据库进行备份,不仅防止数据丢失,而且方便数据迁移与快速恢复。对应的函数是DatabaseExport和DatabaseImport,支持将数据导出为CSV文件,需要时再把CSV中的数据导回数据库。

以下示例展示将trades表导出为backup_trades.csv文件:

//--- Export table
ResetLastError();
long data = DatabaseExport(handler,"trades","backup_trades.csv",DATABASE_EXPORT_HEADER,";");
if(data > 0)
  {
   logs.Info("Backup of the 'trades' table created successfully","TickORM");
  }
else
  {
   logs.Error("Failed to create backup of table 'trades'",GetLastError(),"TickORM");
  }

这里有几个要点需要注意:

  • 文件名:路径是相对于MQL5\\Files文件夹的。也就是说,您可以按需在子文件夹中整理备份文件。

  • CSV格式:通用格式,可使用Excel、谷歌表格、Python、R等工具打开。

  • 标识位:本例使用了DATABASE_EXPORT_HEADER,作用是在第一行写入字段名称,方便后续有需要时解读数据。

    值 描述
    DATABASE_EXPORT_HEADER 在首行输出字段名称
    DATABASE_EXPORT_INDEX 输出行号
    DATABASE_EXPORT_NO_BOM 文件开头不添加BOM标记(默认会添加BOM)
    DATABASE_EXPORT_CRLF 使用CRLF换行(默认换行符为LF)
    DATABASE_EXPORT_APPEND 将数据追加写入已有文件(默认会覆盖文件),如果文件不存在,则会新建
    DATABASE_EXPORT_QUOTED_STRINGS 字符串值使用双引号包裹
    DATABASE_EXPORT_COMMON_FOLDER CSV文件将生成在所有终端共用目录:\Terminal\Common\File
  • 分隔符:示例中使用分号";" ,也可以选用逗号","、制表符"\\t"等,取决于您的偏好或者读取该文件的系统要求。

函数返回值代表成功导出的记录条数。如果返回负数,则代表执行出错,可以调用GetLastError()获取具体错误代码。

现在考虑反向操作:将数据恢复、载入到trades表:

//--- Import table
ResetLastError();
long data = DatabaseImport(handler,"trades","backup_trades.csv",DATABASE_IMPORT_HEADER|DATABASE_IMPORT_APPEND,";",0,NULL);
if(data > 0)
  {
   logs.Info("'trades' table imported successfully","TickORM");
  }
else
  {
   logs.Error("Failed to import 'trades' table",GetLastError(),"TickORM");
  }

下面是一些重要细节:

  • 标识位组合:示例用到了DATABASE_IMPORT_HEADER(首行会被当作字段表头)以及DATABASE_IMPORT_APPEND(导入的数据会追加到现有数据表,不会删除原有数据)。

    值 描述
    DATABASE_IMPORT_HEADER 第一行包含数据表字段名称
    DATABASE_IMPORT_CRLF 文件换行符为CRLF(默认换行符为LF)
    DATABASE_IMPORT_APPEND 将数据追加写入已有数据表
    DATABASE_IMPORT_QUOTED_STRINGS 字符串值由双引号包裹
    DATABASE_IMPORT_COMMON_FOLDER 文件存放于所有客户端终端共用的公共目录:\Terminal\Common\File。
  • 分隔符 :必须和导出时使用的分隔符保持一致。

  • skip_rows:用于跳过文件开头若干行(例如,用来忽略注释行或者旧数据)。

  • skip_comments:用于设定注释标记符,凡以该符号开头的行都会被跳过。这在处理带人工注释的CSV文件时非常实用。

返回值代表成功导入的记录数量。

借助DatabaseExport和DatabaseImport,数据库数据导出与导入的基础流程就完整了。至此,我们已经掌握建表、插入、查询、更新、删除,还能按需备份数据或恢复数据。

但需要注意的是,每一项操作都需要直接编写SQL语句,并单独调用原生函数。在小型示例中这种方式尚可,但随着系统规模扩大,复杂度持续上升,出错概率也随之增加。这就引出一个无法回避的问题:我们真的需要手写全部SQL语句吗?

ORM的设计理念便由此而来。下一节我们讲解什么是ORM、为什么能彻底改变MQL5中操作数据库的方式,以及它在我们这套技术方案里的定位。


什么是ORM?我们为什么需要它?

到目前为止,我们一直在MQL5代码中直接编写SQL,“实时地”操作数据表与记录。这种方式虽然可行,但项目规模变大后,代码里会散落大量SQL语句,随之带来维护困难、代码重复和容易出错等问题。这时就需要ORM(对象关系映射)。

ORM是一个中间层,用于打通面向对象编程与关系型数据库这两个世界。不再手动编写SQL,而是把业务实体定义成类,由ORM自动把类转换成对应的SQL语句。换句话说:不再写INSERT INTO trades (...) VALUES (...),您只需要创建一个Trade对象,调用类似repository.save(trade)的方法即可。

ORM的主要特性包括:

  • 自动映射:将类和类的成员属性映射为数据表和字段;
  • SQL抽象:直接操作对象与方法,不再手写SQL字符串;
  • 一致性:减少重复逻辑,保存、查找删除等通用操作集中管理;
  • 可移植性:部分ORM支持同一套代码适配多种数据库,无需修改源代码。

在MQL5中,ORM的价值尤为突出,如果将SQL硬编码在代码里,后期维护会非常麻烦。试想一个EA里面包含数十种业务实体(交易记录、订单、日志、绩效指标)。代码中到处散落INSERT、SELECT、UPDATE语句,埋下更多隐患,让业务逻辑难以迭代,还容易引发数据不一致。

ORM要解决的“痛点”:

  • 避免EA代码多处重复编写SQL;
  • 可简化维护,数据表结构变更只需要修改一处(实体类);
  • 代码可读性更强,以领域对象(如Trade、User、Order)的视角开发,而不是一直关注表和字段;
  • 支持更多高级功能:自动建表、数据表结构版本管理,还能更方便地对接日志与审计库。

简言之,没有ORM时,每一次数据操作都要在代码块内嵌入SQL;使用ORM之后,SQL变成底层隐藏细节,开发者可以专注核心交易逻辑。


MQL5中的ORM项目

在理解ORM的作用之后,我们来看它如何应用到MQL5生态。Java、C#、Python这类语言已经有成熟可用的ORM,但MQL5没有现成的完备框架。因此我们需要自研一套方案,适配这门语言本身的限制与特性。

本项目规划目标很明确:搭建一层封装层,让开发者直接操作对象,而非手写SQL;同时保证交易场景所需的简洁性与性能。

规划实现的功能包括:

  • 实体:数据库中的每张数据表都对应一个MQL5类;类中携带描述字段信息的元数据(类型、主键、是否自增、是否允许为空等)。因此,Trade类会直接映射trades数据表,而开发者使用时,可作为原生对象直接操作。
  • 仓储:不再手动编写INSERT、UPDATE或SELECT语句。每一个实体都配套一个仓储类,专门负责数据持久化与读取操作。例如,TradeRepository会统一封装Save(trade)、FindById(id)、Delete(trade)这类方法。这样一来,EA代码会更加整洁,不会到处散落SQL语句。
  • 查询构造器:针对复杂查询场景(筛选、排序、表连接),我们还会提供查询构造器。可以通过代码链式构造查询语句,安全地拼接条件,规避字符串拼接带来的错误,减少直接手写SQL。
  • 自动建表:ORM读取实体类的元数据,自动生成对应的数据表,建表前会先检查表是否存在。该功能省去项目初始化阶段手动编写SQL建表脚本的工作,保证数据表结构与实体类保持同步更新。
  • 日志与审计集成:ORM执行的每一次操作(新增、更新、删除)都可写入日志,并复用前面开发好的MQL5日志库。方便对数据库操作做审计追踪,辅助问题排查。
  • 可扩展性:架构为未来的扩展预留了空间,第一阶段先实现基础CRUD,后续还可以增加表结构迁移、实体间关联关系(一对多、多对多),甚至引入内存缓存来优化查询性能。

项目目标并不是在MQL5中复刻Hibernate框架,而是打造一套轻量化、高性能的抽象层,满足量化交易者对结构化数据持久化的实际需求。


总结与后续计划

本篇是系列文章的第一部分,至此,整个知识脉络已经清晰明朗:MQL5数据库不只是单纯存放信息的文件;只要合理使用,它就是一套强大的数据结构,可以有序、可靠地完成数据存储、查询与修改。我们学习了SQL的实际用法,涵盖建表、插入、读取记录,字段与数据类型操作,以及DatabaseOpen、DatabaseExecute、DatabasePrepare、DatabaseRead等MQL5原生数据库函数。

更重要的是,我们意识到每个项目都手写SQL,会产生大量重复代码、容易出错并且难以维护。ORM的核心理念就是提供一层抽象:把数据表转化为对象,将SQL查询封装成简单的方法,让开发者以自然的方式操作实体类,代码更整洁、逻辑集中管理。

本系列的后续章节,我们将开始开发这套抽象层。目标是实现一套轻量化高性能ORM,包含实体类、仓储类、查询构造器以及自动建表机制。届时,数据保存、查询、删除等操作不再依赖散落于代码各处的SQL,而是使用直观的方法,贴合交易业务领域模型。

掌握这套工具,不仅能提升数据操作的效率与安全性,还为复杂交易系统的迭代打下坚实基础,支撑更高级的数据分析、稳健回测以及订单和策略的完整历史记录。

本文到此结束。本系列的下一篇文章将进入实战开发:我们会定义ORM的基础类,实现对象持久化功能,把本文介绍的理论全部落实为可运行的MQL5代码。

本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/19285

附加的文件 |
Experts.zip (506.42 KB)
最近评论 | 前往讨论 (4)
hini
hini | 27 8月 2025 在 13:33
我很喜欢读你的技术文章!
joaopedrodev
joaopedrodev | 27 8月 2025 在 14:03

在这篇首篇文章中,我仅展示了冰山一角。 TickORM 的潜力远不止于此:其核心理念是彻底改变您在 MQL5中操作数据库 的方式。我们的目标是打造一种直观、简单且强大的解决方案,让打开数据库、创建存储库以及操作实体变得像操作数组一样自然。 到本系列文章结束时,实际使用将类似于下面的示例:无需编写任何手动查询,即可保存、搜索、更新和删除记录。这就是我正在通过 TickORM 铺就的道路。

int OnInit(void)
  {
   IDatabaseManager *database = new CDatabaseManager();
   if(database.OpenDatabase("trades.sqlite"))
     {
      //--- 实例存储库
      AccountRepository repo(database);
      
      //--- 插入
      Account acc;
      acc.number = 10;
      repo.save(acc);
      
      //--- 全选
      Account acc[];
      repo.findAll(acc);
      ArrayPrint(acc);
      
      //--- 按 ID 选择
      Account acc = repo.findById(1);
      Print(acc.id);
      Print(acc.number);
      
      //--- 更新
      Account acc;
      acc.id = 1;
      acc.number = 10;
      repo.update(acc);
      
      //--- 按 ID 删除
      repo.deleteById(1);
     }
   
   database.CloseDatabase();
   delete database;
   return(INIT_SUCCEEDED);
  }
Jedidiah
Jedidiah | 10 9月 2025 在 13:52
目前,MQL5 确实非常需要一个合适的 ORM。值得庆幸的是,有像你这样的人在开发如此出色且重要的库,让我们的工作变得轻松许多。老兄,我非常喜欢你迄今为止参与的所有项目 和文章。它们都解决了我在使用 MQL5 处理某些任务时遇到的棘手问题。
Stanislav Korotky
Stanislav Korotky | 10 9月 2025 在 19:11
joaopedrodev 在 MQL5中操作数据库 的方式。我们的目标是打造一种直观、简单且强大的解决方案,让打开数据库、创建存储库以及操作实体变得像操作数组一样自然。 到本系列文章结束时,实际使用将类似于下面的示例:无需编写任何手动查询,即可保存、搜索、更新和删除记录。这就是我正在通过 TickORM 铺就的道路。

附注:本书中已 介绍了一种针对 MQL5 的 ORM 实现方案。

从新手到专家:使用 Reporting EA 打造详尽的交易报告 从新手到专家:使用 Reporting EA 打造详尽的交易报告
本文深入探讨如何丰富交易报告的细节,并以 PDF 格式通过电子邮件发送最终报告。本文是前序文章内容的延伸,我们将继续探索如何利用 MQL5 与 Python,生成并定时输出格式专业、使用便捷的交易报告。跟随本文,你将了解如何在 MQL5 生态中优化交易报告的生成流程。
MQL5交易策略自动化(第二十八部分):构建带可视化反馈的价格行为蝙蝠谐波形态 MQL5交易策略自动化(第二十八部分):构建带可视化反馈的价格行为蝙蝠谐波形态
在本文中,我们将在MQL5中开发一套蝙蝠形态交易系统,该系统借助枢轴点与斐波那契比率识别看涨、看跌蝙蝠谐波形态,并在精准的入场、止损与止盈价位触发交易;并结合图表对象实现直观的可视化展示。
新手在交易中的10个基本错误 新手在交易中的10个基本错误
新手在交易中会犯的10个基本错误: 在市场刚开始时交易, 获利时不适当地仓促, 在损失的时候追加投资, 从最好的仓位开始平仓, 翻本心理, 最优越的仓位, 用永远买进的规则进行交易, 在第一天就平掉获利的仓位,当发出建一个相反的仓位警示时平仓, 犹豫。
MQL5自优化智能交易系统(第十三部分):基于矩阵分解浅谈控制理论 MQL5自优化智能交易系统(第十三部分):基于矩阵分解浅谈控制理论
金融市场不可预测,那些在历史回测中表现盈利的交易策略,投入实盘后往往会失效。究其原因:大多数策略部署后逻辑固定,无法自适应市场,也不能从失误中学习。我们借鉴控制理论思想,利用反馈控制器监测策略与市场的交互情况,并调整策略行为,使其趋向盈利。测试结果表明:在简单移动平均策略中加入反馈控制器后,利润提升、风险降低、交易效率提高,证明该方案在交易领域具备巨大潜力。