![SQL и MQL5: Работаем с базой данных SQLite](https://c.mql5.com/i/articles/overlay.png)
SQL и MQL5: Работаем с базой данных SQLite
Small. Fast. Reliable.
Choose any of three.
Вступление
Многие разработчики задумывались над необходимостью использовать в своих проектах базы данных для хранения информации, но каждый раз останавливались, зная, какими дополнительными затратами времени сопровождается установка SQL-сервера. Если для программистов выполнить это не так сложно (или СУБД уже установлена для других целей), то для обычного пользователя данная операция вызовет много вопросов, либо вообще отобьет желание устанавливать софт.
В таком случае программист просто не связывается с СУБД, понимая, что кроме него этим решением воспользуются единицы. В конечном итоге в ход идет работа с файлами (часто не одним при разрозненности данных): использование CSV, реже XML или JSON, либо запись бинарных данных с жестким размером структуры и т.п.
Но, как оказывается, есть отличная альтернатива SQL-серверу! При этом не нужно устанавливать дополнительный софт, вся работа идет локально в вашем проекте, но при этом вы используете всю мощь языка SQL. Имя этой альтернативы - SQLite.
Цель данной статьи - обеспечить быстрый старт в использовании SQLite. Поэтому я не буду углубляться в тонкости и всевозможные наборы параметров или флагов функций, а создам легкую обвертку-коннектор, которая выполняет SQL команды, и на примерах покажу ее применение.
Необходимые условия для работы со статьей:
- Хорошее настроение ;)
- Распакуйте архив из приложения к статье в папку рабочего терминала MetaTrader 5
- Установите любую удобную для вас утилиту просмотра SQLite баз (например, SQLiteStudio)
- Добавьте в закладки официальную документацию по SQLite http://www.sqlite.org
Содержание
1. Принципы SQLite
2. Работа с SQLite API
2.1. Открытие и закрытие базы данных
2.2. Выполнение SQL запросов
2.3. Получение данных из таблиц
2.4. Запись параметрических данных со связыванием
2.5. Транзакции / Multirow insert (пример создания таблицы сделок торгового счета)
3. Сборка 64-битной DLL версии SQLite3
1. Принципы SQLite
SQLite – это СУБД, главной особенностью которой является отсутствие "слушающего" SQL-сервера. Сервером уже является ваше приложение. Работа с базой данных в SQLite – это фактически работа с файлом (на диске или в памяти). Все данные вы можете архивировать или же переносить на другой компьютер без какой либо потребности специально их устанавливать.
Использование SQLite дает разработчику и пользователю несколько неоспоримых преимуществ:
- не устанавливать дополнительный софт;
- данные хранятся в локальном файле, что создает прозрачность их контроля, т.е. их можно просматривать и редактировать независимо от вашего приложения;
- возможность импорта или экспорта таблиц в другие СУБД;
- в коде используются привычные SQL запросы, что позволяет в любой момент перевести приложение на работу с другими СУБД.
Работать с SQLite возможно тремя способами:
- воспользоваться DLL файлом с полным набором API функций;
- использовать shell команды к EXE файлу;
- скомпилировать ваш проект, включив в него исходные коды 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 функциях. Исходный код импорта функций API находится в файле SQLite3Import.mqh
Типы данных SQLite
В третьей версии SQLite предусматривается пять типов данных
Тип |
Описание |
---|---|
NULL | Отсутствие данных. |
INTEGER | Целое размером 1, 2, 3, 4, 6 или 8 байт в зависимости от величины хранимого значения. |
REAL | Вещественное размером 8 байт. |
TEXT | Строка с конечным символом \0 в кодировке UTF-8 или UTF-16. |
BLOB | Произвольные бинарные данные |
Для указания типов полей при создании таблицы в SQL запросе можно воспользоваться также и другими именами типов, например, 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] - путь к файлу, либо имя файла, если файл открывается в текущем месторасположении. ppDb [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 запросов
Любой SQL запрос в SQLite3 проходит минимум три этапа:
- sqlite3_prepare - верификация и получение списка инструкций (statement);
- sqlite3_step - выполнение этих инструкций;
- sqlite3_finalize - завершение и очистка памяти.
Эта схема, в основном, подходит для создания или удаления таблиц, либо для записи небинарных данных, т.е. для случаев, когда запрос SQL не подразумевает возврата каких-либо данных, кроме как статуса успешности его выполнения.
Если же запрос подразумевает получение данных или запись бинарных данных, то на втором этапе нам надо будет воспользоваться функциями sqlite3_column_хх или sqlite3_bind_хх, соответственно. Эти функции подробно рассмотрим в следующем разделе.
Напишем метод CSQLite3Base::Query для выполнения простого SQL запроса:
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ 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_prepare, sqlite3_step и sqlite3_finalize идут друг за другом.
Рассмотрим применение CSQLite3Base::Query при работе с таблицами в SQLite:
// Создание таблицы (CREATE TABLE) sql3.Query("CREATE TABLE IF NOT EXISTS `TestQuery` (`ticket` INTEGER, `open_price` DOUBLE, `comment` TEXT)");
После выполнения данной команды в базе данных появится таблица:
// Переименование таблицы (ALTER TABLE RENAME) sql3.Query("ALTER TABLE `TestQuery` RENAME TO `Trades`"); // Добавление столбца (ALTER TABLE ADD COLUMN) sql3.Query("ALTER TABLE `Trades` ADD COLUMN `profit`");
После выполнения этих команд имеем таблицу с новым названием и добавленным полем:
// Добавление ряда (INSERT INTO) sql3.Query("INSERT INTO `Trades` VALUES(3, 1.212, 'info', 1)"); // Обновление ряда (UPDATE) sql3.Query("UPDATE `Trades` SET `open_price`=5.555, `comment`='New price' WHERE(`ticket`=3)")
В результате добавления строки и ее изменения в таблице появится такая запись:
И, наконец, следующие последовательно выполненные команды очистят базу данных.
// Удаление всех рядов из таблицы (DELETE FROM) sql3.Query("DELETE FROM `Trades`") // Удаление таблицы (DROP TABLE) sql3.Query("DROP TABLE IF EXISTS `Trades`"); // Сжатие базы данных (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_хх. Схематически это можно изобразить так:
- sqlite3_prepare
- sqlite3_column_count - узнаем число столбцов полученной таблицы
- Пока результат шага sqlite3_step == SQLITE_ROW
- sqlite3_column_хх - читаем ячейки этой строки
- sqlite3_finalize
Так как мы подходим к обширному разделу по чтению и записи данных, настал удобный момент описать три класса-контейнера, с помощью которых будет происходить весь обмен. Требуемую модель данных нам диктует сам способ хранения информации в БД:
База данных
|
Таблица – это массив рядов.
|
Ряд – это массив ячеек.
|
Ячейка – это байтовый буфер произвольной длины.
//+------------------------------------------------------------------+ //| 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
};
Обратите внимание, что к пяти базовым типам SQLite3 добавлен тип CT_UNDEF для идентификации начального состояния ячейки. Целый тип INTEGER разделен на два CT_INT и CT_INT64 в соответствии с аналогично разделенными функциями sqlite3_bind_intXX, sqlite3_column_intXX.
Получение данных
Для получения данных из ячейки создадим метод, обобщающий функции вида 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); }
Функция получилась довольно объемная, но, по сути, она лишь читает данные из текущей инструкции и сохраняет их в ячейку.
Также перегрузим функцию CSQLite3Base::Query, добавив первым параметром контейнер-таблицу CSQLite3Table для принимаемых данных.
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ 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 }
Все нужные функции для получения данных есть. Переходим к их примерам:
// Чтение данных (SELECT) CSQLite3Table tbl; sql3.Query(tbl, "SELECT * FROM `Trades`")
Выведем результат данного запроса на печать в терминал командой Print(TablePrint(tbl)). В журнале увидим следующее (порядок снизу вверх):
// Пример расчета стат. информации из таблиц (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`")
// Получение имен всех таблиц в базе sql3.Query(tbl, "SELECT `name` FROM `sqlite_master` WHERE `type`='table' ORDER BY `name`;");
Результат данного запроса выведем аналогично командой Print(TablePrint(tbl)). Увидим имеющуюся таблицу:
Как видно из примеров, результаты выполнения запросов помещаются в переменную tbl, из которой, затем несложно получить и обработать их по вашему усмотрению.
2.4. Запись параметрических данных со связыванием
Еще одна тема, которую стоит обязательно рассмотреть новичку, это запись информации в базу данных, которая имеет "неудобный" формат. Конечно же, речь идет про бинарные данные, которые "в лоб" не передать в обычной текстовой инструкции INSERT или UPDATE, т.к. при первом же нуле строка посчитается оконченной. Или, например, бывают случаи, когда в самой строке присутствуют спецсимволы одинарной кавычки '.
В некоторых ситуациях позднее связывание удобно, когда имеем широкую таблицу, и записывать все поля в одну строку трудно и ненадежно, так как есть вероятность что-то проглядеть или потерять. Для данной операции связывания необходимы функции семейства sqlite3_bind_хх.
Чтоб применить связывание, необходимо вместо передаваемых данных поставить шаблон. Я рассмотрю один из вариантов - это знак "?". То есть UPDATE-запрос будет выглядеть так:
Затем, нужно выполнить подряд две функции: sqlite3_bind_double и sqlite3_bind_text, чтоб поместить данные в open_price и comment. В общем, схему работы с bind-функциями можно представить в виде:
- sqlite3_prepare
- По порядку вызываем sqlite3_bind_хх и записываем требуемые данные в инструкцию
- sqlite3_step
- 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. Транзакции / Multirow inserts
Перед рассмотрением этой темы вам понадобится знание еще одной функции SQLite API. В прошлом разделе я рассмотрел трехэтапную обработку запросов prepare+step+finalize. Альтернативным и в некоторых случаях простым, а в некоторых - даже необходимым решением является функция sqlite3_exec:
int sqlite3_exec(sqlite3_p64 ppDb, const char &sql[], PTR64 callback, PTR64 pvoid, PTRPTR64 errmsg); ppDb [in] - хендл базы данных sql [in] - SQL запрос Три остальных параметра пока что не рассматриваем применительно к MQL5. Возвращает SQLITE_OK при успешном выполнении или код ошибки.
Ее суть – выполнить запрос за один вызов без создания трехэтапных конструкций.
Добавим ее вызов в коннектор:
//+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ 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");
Транзакции
Теперь представим ситуацию, когда необходимо добавить несколько тысяч строк в таблицу. Если занести все это в цикл:
for (int i=0; i<N; i++) sql3.Query("INSERT INTO `Table` VALUES(1, 2, 'text')");
то получим о о ч ч е н ь м е е е д л е н н о е выполнение. На вставку тысячи строк потратится более 10(!) секунд, ибо так в SQLite делать не рекомендуется. Выходом из данной ситуации является использование транзакций: все 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()); }
Выполнив данный скрипт на своем счете, моментально записал сделки своего счета в таблицу.
В журнале терминала вывелась статистическая информация
Поэкспериментируйте с данным скриптом: закомментируйте строки с BEGIN, ROLLBACK и COMMIT, и если у вас на счете более сотни сделок, то вы сразу же ощутите разницу. Кстати, по некоторым тестам, транзакции SQLite работают быстрее, чем в MySQL или PostgreSQL.
3. Сборка 64-битной версии sqlite3_64.dll
- Скачиваем исходный код SQLite (amalgamation) и вытягиваем из него файл sqlite3.c.
- Скачиваем DLL sqlite-dll-win32 и распаковываем из нее файл sqlite3.dll.
- В папке, где распаковали dll файл, выполняем в консоли команду LIB.EXE /DEF:sqlite3.def. Убедитесь что пути к файлу lib.exe прописаны в системной переменной PATH, либо найдите его у вас в Visual Studio.
- Создаем проект DLL, выбрав конфигурацию Release для 64 битных платформ.
- Добавляем в проект скачанный файл sqlite3.c и полученный файл sqlite3.def. Если на некоторые функции из def файла компилятор будет "ругаться", в этом случае просто закомментируйте их.
- В настройках проекта установим следующие параметры:
C/C++ --> General --> Debug Information Format = Program Database (/Zi)
C/C++ --> Precompiled Headers --> Create/Use Precompiled Header = Not Using Precompiled Headers (/Yu) - Компилируем и получаем 64-битную dll.
Заключение
Надеюсь, что статья станет вашей настольной инструкцией в деле освоения SQLite. Вполне возможно, что вы будете использовать ее в своих будущих проектах. Этот краткий обзор дал вам некоторое представление о функциональности SQLite, как об идеальном и надежном решении для приложений.
В статье мы рассмотрели все примеры, с которыми вы столкнетесь при обработке торговых данных. В качестве домашнего задания - создайте простой сборщик тиков, с записью тиков в таблицу для каждого символа. Исходный код библиотеки классов и тестовые скрипты вы найдете в приложении к статье.
Удачи и профитов!
![Структура данных в MetaTrader 4 build 600 и выше](https://c.mql5.com/2/13/1143_38.png)
![Оффлайновые графики и новый MQL4](https://c.mql5.com/2/13/1137_68.png)
![Рецепты MQL5 - Разработка мультивалютного индикатора для анализа расхождения цен](https://c.mql5.com/2/0/avatar__11.png)
![Создание цифровых фильтров, не запаздывающих по времени](https://c.mql5.com/2/0/gmomentum-test-logo-200x200-7398.png)
![MQL5 - Язык торговых стратегий для клиентского терминала MetaTrader 5](https://c.mql5.com/i/registerlandings/logo-2.png)
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Это легко проверить, на самом деле. Попробуй для начала в "Мои документы" запихнуть БД. Если получится, значит дело не в правах. А вообще, смотря как ты это всё делал. Я лично недели 2 продолбался, пока создал себе всё что нужно для работы с SQLite3. Пришлось переводить документацию, т.к. на русском языке нет адекватной информации по этому поводу. Как и в этой статье много мусора и левака. Выше я привл один из моментов. Потом я забил и сам изучил вопрос.
У меня есть для этого такой wrapper:
А вот и функция stringToUtf8:
Она у меня в отдельной библиотеке.
Да, у меня было 2 ошибки:
1 - кодировка (спасибо огромное за функцию, я уже не знал как перевести нормально в UTF -8)
2 - права доступа.
Да, у меня было 2 ошибки:
1 - кодировка (спасибо огромное за функцию, я уже не знал как перевести нормально в UTF -8)
2 - права доступа.
Да, пожалуйста. Сам вспоминаю, сколько продолбался с этим. С такими горе-статьями.. Вариант один есть. Берёшь читаешь оф. документацию, либо спрашиваешь.. Спрашивать было не у кого. Пришлось изучать самостоятельно. Тут суть в том, что если писать на С++ спросить есть у кого. А когда через прокладку писать и передавать в мкл, тут уже мало кто может помочь. Влом плюсовцам в этом вникать))
Это легко проверить, на самом деле. Попробуй для начала в "Мои документы" запихнуть БД. Если получится, значит дело не в правах. А вообще, смотря как ты это всё делал. Я лично недели 2 продолбался, пока создал себе всё что нужно для работы с SQLite3. Пришлось переводить документацию, т.к. на русском языке нет адекватной информации по этому поводу. Как и в этой статье много мусора и левака. Выше я привл один из моментов. Потом я забил и сам изучил вопрос.
У меня есть для этого такой wrapper:
А вот и функция stringToUtf8:
Она у меня в отдельной библиотеке.
а я использую для преобразования к utf
Возможно кому-то будет полезно.
В МТ4 код работает почти без изменений
У меня долго не создавался сам файл БД. Т.е. результаты есть, а файла нет.
Решение: прописать полный путь к файлу, например так:
Вопрос
Видимо это особенности кодировки.
Если записывать русский текст в БД, то в SQLite studio он выглядит криво (ромбики с вопросом).
А если вбить русский текст вручную, то потом он криво выглядит в МТ4.
Вопрос: как сделать отображение текста в правильной кодировке, чтоб был виден русский текст?