Транзакции

SQLite поддерживает механизм транзакций — логически связанных наборов действий, которые могут быть выполнены либо целиком, либо не выполнены вовсе, что обеспечивает непротиворечивость данных в базе.

Понятие транзакция имеет в контексте баз данных новый смысл, отличающийся от того, что мы использовали при описании торговых транзакций: там под транзакцией подразумевалась отдельная операция над сущностями торгового счета — ордерами, сделками, позициями.

Транзакции обеспечивают 4 основных характеристики изменений базы:

  • Атомарность (неделимость) — при успешном завершении транзакции в базу попадут все входящие в неё изменения, а в случае ошибки — не попадет ничего;
  • Согласованность — текущее правильное состояние базы может измениться только на другое правильное (промежуточные, по прикладной логике, состояния исключены);
  • Изолированность — изменения в транзакции текущего подключения не видны до конца этой транзакции в других подключениях к той же базе и наоборот — изменения из других подключений не видны в текущем подключении, пока здесь есть незавершенная транзакция;
  • Надежность — изменения из успешной транзакции гарантированно сохраняются в базе.

Англоязычные термины этих характеристик — Atomic, Consistent, Isolated, Durable — формируют известный в теории баз данных акроним ACID.

Даже если нормальный ход программы будет прерван из-за системного сбоя, база данных сохранит свое рабочее состояние.

Наиболее часто использование транзакций иллюстрируют примером банковской системы, в базе которой выполняется перевод средств со счета одного клиента на счет другого. Он должен затронуть две записи с балансами клиентов: в одной остаток уменьшается на размер перевода, а в другой — увеличивается. Ситуация, когда применилось бы только одно из этих изменений, нарушило бы баланс банковских счетов: в зависимости от того, какая операция отказала, перечисленная сумма могла бы пропасть или, наоборот, взяться ниоткуда.

Можно привести более приближенный к трейдерской практике пример, но по принципу "от противного". Дело в том, что система учета ордеров, сделок и позиций в MetaTrader 5 не является транзакционной.

В частности, как мы знаем из главы про Создание экспертов, сработавший ордер (рыночный или отложенный), пропав из списка активных, может не сразу отобразиться в списке позиций. Поэтому для анализа фактического результата приходится в MQL-программе реализовывать ожидание обновления (актуализации) торгового окружения. Если бы система учета строилась на транзакциях, то исполнение ордера, регистрация сделки в истории и появление позиции были бы заключены в транзакцию и согласованы друг с другом. Разработчики терминала выбрали другой подход: максимально быстро и асинхронно возвращать любые модификации торгового окружения, а за их целостностью должна следить MQL-программа.

Любая SQL-команда, которая изменяет базу (то есть фактически все, кроме SELECT), автоматически будет обернута в транзакцию, если это не было сделано предварительно явным образом.

MQL5 API предоставляет 3 функции для управления транзакциями: DatabaseTransactionBegin, DatabaseTransactionCommit, DatabaseTransactionRollback. Все функции возвращают 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)   // уже в транзакции
      {
         transaction++;     // отслеживаем уровень вложенности
         return true
      }
      return (bool)(transaction = PRTF(DatabaseTransactionBegin(handle)));
   }
   
   bool commit()
   {
      if(transaction > 0)
      {
         if(--transaction == 0// самая внешняя транзакция
            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..."); // пакетные изменения
   ... // модификация базы
   if(... /* ошибка1 */) return;             // автоматический rollback
   ... // модификация базы
   if(... /* ошибка2 */) return;             // автоматический rollback
   tr.commit();
}

Чтобы объект автоматически применял изменения на любой стадии, следует передать true во втором параметре его конструктора.

void DataFunction(DBSQLite &db)
{
   DBTransaction tr(dbtrue);
   DBQuery *query = db.prepare("UPDATE..."); // пакетные изменения
   ... // модификация базы
   if(... /* условие1 */) return;            // автоматический commit
   ... // модификация базы
   if(... /* условие2 */) return;            // автоматический commit
   ...
}                                            // автоматический commit

Вы можете описать объект DBTransaction внутри цикла, и тогда на каждой итерации будет начинаться и закрываться отдельная транзакция.

Демонстрация транзакций будет приведена в разделе Пример поиска торговой стратегии средствами SQLite.