事务

SQLite 支持transactions,即逻辑上相关的一组操作,这些操作要么全部执行,要么完全不执行,这确保了数据库中数据的一致性。

transaction的概念在数据库上下文中具有新的含义,不同于我们过去用来描述 交易事务的含义。交易事务是指对交易账户实体(包括订单、成交和仓位)的单独操作。

事务提供了数据库更改的 4 个主要特性:

  • 原子性(不可分割)– 事务成功完成后,其中包含的所有更改都将进入数据库;如果发生错误,则任何更改都不会进入数据库。
  • 一致性 – 数据库当前的正确状态只能更改为另一种正确状态(根据应用程序逻辑,排除中间状态)。
  • 隔离性 – 当前连接的事务中的更改在该事务结束之前对同一数据库的其他连接不可见,反之亦然,在当前连接存在未完成事务时,其他连接的更改在当前连接中也不可见。
  • 持久性 – 成功事务的更改保证存储在数据库中。

这些特性的术语(原子性、一致性、隔离性和持久性)构成了数据库理论中众所周知的缩写 ACID。

即使程序的正常运行由于系统故障而中断,数据库也将保持其工作状态。

最常用来说明事务的例子是银行系统,其中资金从一个客户的账户转移到另一个客户的账户。这应该影响两条客户余额记录:一条记录中的余额按转账金额减少,另一条则增加。如果这些更改中只有一个生效,将会打乱银行账户的平衡:根据哪个操作失败,转账金额可能会消失,或者相反,无中生有。

可以举一个更接近交易实践的例子,但基于“相反”的原则。事实是 MetaTrader 5 中记录订单、成交和仓位的系统并非事务性的。

特别是,正如我们从 创建 EA 交易一章中了解到的,一个已触发的订单(市价或挂单),在活动订单列表中消失后,可能不会立即显示在仓位列表中。因此,为了分析实际结果,有必要在 MQL 程序中实现对交易环境更新(实际化)的等待。如果记账系统基于事务,那么订单的执行、历史记录中交易的注册以及仓位的出现都将被包含在一个事务中并相互协调。终端开发者选择了不同的方法:尽可能快速和异步地返回交易环境的任何修改,其完整性必须由 MQL 程序监控。

任何更改数据库的 SQL 命令(实际上,除了 SELECT 之外的所有命令)如果事先没有显式地进行处理,都将自动被包装在一个事务中。

MQL5 API 提供了 3 个用于管理事务的函数:DatabaseTransactionBeginDatabaseTransactionCommitDatabaseTransactionRollback。所有函数成功时返回 true,如果出错则返回 false

bool DatabaseTransactionBegin(int database)

DatabaseTransactionBegin 函数在具有从 DatabaseOpen获取的指定描述符的数据库中开始执行事务。

对数据库进行的所有后续更改都累积在内部事务缓存中,并且在调用 DatabaseTransactionCommit 函数之前不会进入数据库。

MQL5 中的事务不能嵌套:如果事务已经启动,则重新调用 DatabaseTransactionBegin 将返回错误标志并在日志中输出消息。

database error, cannot start a transaction within a transaction
DatabaseTransactionBegin(db)=false / DATABASE_ERROR(5601)

相应地,你不能多次尝试并完成事务。

bool DatabaseTransactionCommit(int database)

DatabaseTransactionCommit 函数结束先前在具有指定句柄的数据库中启动的事务,并应用所有累积的更改(保存它们)。如果 MQL 程序启动一个事务但在关闭数据库之前未应用它,则所有更改都将丢失。

如果需要,程序可以撤销事务,从而撤销自事务开始以来的所有更改。

bool DatabaseTransactionRollback(int database)

DatabaseTransactionRollback 函数对包含 database 句柄的数据库先前启动的事务中所含的所有操作执行“回滚”。

我们来完成 DBSQLite 类中用于处理事务的方法,同时考虑到它们嵌套的限制,我们将在 transaction 变量中计算这个限制。如果为 0,则 begin 方法通过调用 DatabaseTransactionBegin 启动一个事务。所有后续启动事务的尝试只会增加计数器。在 commit 方法中,我们递减计数器,当计数器达到 0 时,我们调用 DatabaseTransactionCommit

class DBSQLite
{
protected:
   int transaction;
   ...
public:
   bool begin()
   {
      if(transaction > 0)   // already in transaction
      {
         transaction++;     // keep track of the nesting level
         return true
      }
      return (bool)(transaction = PRTF(DatabaseTransactionBegin(handle)));
   }
   
   bool commit()
   {
      if(transaction > 0)
      {
         if(--transaction == 0// outermost transaction
            return PRTF(DatabaseTransactionCommit(handle));
      }
      return false;
   }
   bool rollback()
   {
      if(transaction > 0)
      {
         if(--transaction == 0)
            return PRTF(DatabaseTransactionRollback(handle));
      }
      return false;
   }
};

此外,我们创建 DBTransaction 类,它将允许在块(例如,函数)内部描述对象,确保在程序退出块时自动启动事务并随后应用(或取消)它。

class DBTransaction
{
   DBSQLite *db;
   const bool autocommit;
public:
   DBTransaction(DBSQLite &ownerconst bool c = false): db(&owner), autocommit(c)
   {
      if(CheckPointer(db) != POINTER_INVALID)
      {
         db.begin();
      }
   }
   
   ~DBTransaction()
   {
      if(CheckPointer(db) != POINTER_INVALID)
      {
         autocommit ? db.commit() : db.rollback();
      }
   }
   
   bool commit()
   {
      if(CheckPointer(db) != POINTER_INVALID)
      {
         const bool done = db.commit();
         db = NULL;
         return done;
      }
      return false;
   }
};

使用此类对象的策略无需处理块(函数)各种退出选项。

void DataFunction(DBSQLite &db)
{
   DBTransaction tr(db);
   DBQuery *query = db.prepare("UPDATE..."); // batch changes
   ... // base modification
   if(... /* error1 */) return;             // automatic rollback
   ... // base modification
   if(... /* error2 */) return;             // automatic rollback
   tr.commit();
}

要使对象在任何阶段自动应用更改,请在其构造函数的第二个参数中传递 true

void DataFunction(DBSQLite &db)
{
   DBTransaction tr(dbtrue);
   DBQuery *query = db.prepare("UPDATE...");  // batch changes
   ... // base modification
   if(... /* condition1 */) return;           // automatic commit
   ... // base modification
   if(... /* condition2 */) return;           // automatic commit
   ...
}                                             // automatic commit

你可以在循环内部描述 DBTransaction 对象,然后在每次迭代时,将启动并关闭一个单独的事务。

事务的演示将在 使用 SQLite 搜索交易策略的示例一节中提供。