通过 ORM 对象在 SQLite 中执行 CRUD 操作的示例

我们已经学习了在数据库中实现信息完整生命周期所需的所有函数,即 CRUD (Create, Read, Update, Delete)。但在进行实践之前,我们需要完成 ORM 层。

从前面几节中,已经很清楚与数据库工作的单位是一条记录:它可以是数据库表中的一条记录,也可以是查询结果中的一个元素。为了在 ORM 层面读取单条记录,我们引入 DBRow 类。每条记录都由一个 SQL 查询生成,因此其句柄会传递给构造函数。

正如我们所知,一条记录可以由多个列组成,其数量和类型可让我们找到 DatabaseColumn函数。为了使用 DBRow 向 MQL 程序公开这些信息,我们预留了相关变量: columns 以及一个 DBRowColumn 结构体数组(后者包含三个字段,用于存储列的名称、类型和大小)。

此外,DBRow 对象在必要时可以在自身内部缓存从数据库获取的值。为此,使用了 MqlParam 类型的 <text style="font-style:italic;">data</text> 数组。由于我们预先不知道特定列中将是什么类型的值,因此使用 MqlParam 作为一种在其他编程环境中可用的通用 Variant 类型。

class DBRow
{
protected:
   const int query
   int columns;
   DBRowColumn info[];
   MqlParam data[];
   const bool cache;
   int cursor;
   ...
public:
   DBRow(const int qconst bool c = false):
      query(q), cache(c), columns(0), cursor(-1)
   {
   }
   
   int length() const
   {
      return columns;
   }
   ...
};

cursor 变量跟踪查询结果中的当前记录号。在请求完成之前,cursor 等于 -1。

虚方法 DBread 负责执行查询;它调用 DatabaseRead

protected:
   virtual bool DBread()
   {
      return PRTF(DatabaseRead(query));
   }

我们稍后会看到为什么需要一个虚方法。使用 DBread 的公共方法 next 提供了对结果记录的“滚动”浏览,看起来像这样。

public:
   virtual bool next()
   {
      ...
      const bool success = DBread();
      if(success)
      {
         if(cursor == -1)
         {
            columns = DatabaseColumnsCount(query);
            ArrayResize(infocolumns);
            if(cacheArrayResize(datacolumns);
            for(int i = 0i < columns; ++i)
            {
               DatabaseColumnName(queryiinfo[i].name);
               info[i].type = DatabaseColumnType(queryi);
               info[i].size = DatabaseColumnSize(queryi);
               if(cachedata[i] = this[i]; // overload operator[](int)
            }
         }
         ++cursor;
      }
      return success;
   }

如果查询是第一次被访问,我们分配内存并读取列信息。如果请求了缓存,我们还会填充 data 数组。为此,会为每一列调用重载的运算符 '[]'。在其中,根据值的类型,我们调用适当的 DatabaseColumn 函数,并将结果值放入 MqlParam 结构体的一个或另一个字段中。

   virtual MqlParam operator[](const int i = 0const
   {
      MqlParam param = {};
      if(i < 0 || i >= columnsreturn param;
      if(ArraySize(data) > 0 && cursor != -1// if there is a cache, return from it
      {
         return data[i];
      }
      switch(info[i].type)
      {
      case DATABASE_FIELD_TYPE_INTEGER:
         switch(info[i].size)
         {
         case 1:
            param.type = TYPE_CHAR;
            break;
         case 2:
            param.type = TYPE_SHORT;
            break;
         case 4:
            param.type = TYPE_INT;
            break;
         case 8:
         default:
            param.type = TYPE_LONG;
            break;
         }
         DatabaseColumnLong(queryiparam.integer_value);
         break;
      case DATABASE_FIELD_TYPE_FLOAT:
         param.type = info[i].size == 4 ? TYPE_FLOAT : TYPE_DOUBLE;
         DatabaseColumnDouble(queryiparam.double_value);
         break;
      case DATABASE_FIELD_TYPE_TEXT:
         param.type = TYPE_STRING;
         DatabaseColumnText(queryiparam.string_value);
         break;
      case DATABASE_FIELD_TYPE_BLOB// return base64 only for information we can't
         {                           // return binary data in MqlParam - exact 
            uchar blob[];            // representation of binary fields is given by getBlob 
            DatabaseColumnBlob(queryiblob);
            uchar key[], text[];
            if(CryptEncode(CRYPT_BASE64blobkeytext))
            {
               param.string_value = CharArrayToString(text);
            }
         }
         param.type = TYPE_BLOB;
         break;
      case DATABASE_FIELD_TYPE_NULL:
         param.type = TYPE_NULL;
         break;
      }
      return param;
   }

提供了 getBlob 方法以从 BLOB 字段中完整读取二进制数据(如果没有关于内容格式的更具体信息,请使用 uchar 类型作为 S 以获取字节数组)。

   template<typename S>
   int getBlob(const int iS &object[])
   {
      ...
      return DatabaseColumnBlob(queryiobject);
   }

对于所描述的方法,执行查询并读取其结果的过程可以用以下伪代码表示(它将现有的 DBSQLiteDBQuery 类置于后台,但我们很快会将它们整合在一起):

int query = ...
DBRow *row = new DBRow(query);
while(row.next())
{
   for(int i = 0i < row.length(); ++i)
   {
      StructPrint(row[i]); // print the i-th column as an MqlParam structure
   }
}

每次都显式地编写列循环并不优雅,因此该类提供了一种获取记录所有字段值的方法。

   void readAll(MqlParam &params[]) const
   {
      ArrayResize(paramscolumns);
      for(int i = 0i < columns; ++i)
      {
         params[i] = this[i];
      }
   }

此外,为了方便起见,该类还接收了运算符 '[]' 和 getBlob 方法的重载,用于通过字段名称而不是索引来读取字段。例如,

class DBRow
{
   ...
public:
   int name2index(const string nameconst
   {
      for(int i = 0i < columns; ++i)
      {
         if(name == info[i].namereturn i;
      }
      Print("Wrong column name: "name);
      SetUserError(3);
      return -1;
   }
   
   MqlParam operator[](const string nameconst
   {
      const int i = name2index(name);
      if(i != -1return this[i]; // operator()[int] overload
      static MqlParam param = {};
      return param;
   }
   ...
};

这样你就可以访问选定的列。

int query = ...
DBRow *row = new DBRow(query);
for(int i = 1row.next(); )
{
   Print(i++, " "row["trades"], " "row["profit"], " "row["drawdown"]);
}

但是,仍然像 MqlParam 数组那样单独获取记录的元素,不能称为真正的 OOP 方法。最好是将整个数据库表记录读入一个对象,一个应用程序结构体中。回想一下,MQL5 API 提供了一个合适的函数:DatabaseReadBind。这样,我们就获得一种优势,能够描述派生类 DBRow 并覆盖其虚方法 DBRead

这个 DBRowStruct 类是一个模板,并期望参数 S 是允许在 DatabaseReadBind 中绑定的简单结构体之一。

template<typename S>
class DBRowStructpublic DBRow
{
protected:
   S object;
   
   virtual bool DBread() override
   {
      // NB: inherited structures and nested structures are not allowed;
      // count of structure fields should not exceed count of columns in table/query
      return PRTF(DatabaseReadBind(queryobject));
   }
   
public:
   DBRowStruct(const int qconst bool c = false): DBRow(qc)
   {
   }
   
   S get() const
   {
      return object;
   }
};

有了派生类,我们几乎可以无缝地从基类获取对象。

int query = ...
DBRowStruct<MyStruct> *row = new DBRowStruct<MyStruct>(query);
MyStruct structs[];
while(row.next())
{
   PUSH(structsrow.get());
}

现在就可以将 DBRow/DBRowStructDBQuery 关联起来,将伪代码转换为工作代码了。在 DBQuery 中,我们向 DBRow 对象添加一个自动指针,该对象将包含来自查询结果的当前记录的数据(如果已执行)。使用自动指针使调用代码无需担心释放 DBRow 对象:它们要么随 DBQuery 一起删除,要么在由于查询重新启动(如果需要)而重新创建时删除。DBRowDBRowStruct 对象的初始化由模板方法 start 完成。

class DBQuery
{
protected:
   ...
   AutoPtr<DBRowrow;    // current entry
public:
   DBQuery(const int ownerconst string s): db(owner), sql(s),
      handle(PRTF(DatabasePrepare(dbsql)))
   {
      row = NULL;
   }
   
   template<typename S>
   DBRow *start()
   {
      DatabaseReset(handle);
      row = typename(S) == "DBValue" ? new DBRow(handle) : new DBRowStruct<S>(handle);
      return row[];
   }

DBValue 类型是一个虚拟结构体,仅用于指示程序创建底层的 DBRow 对象,而不违反带有 DatabaseReadBind 调用的代码行的可编译性。

通过 start 并对请求进行如下准备之后,此前展示的所有伪代码片段便可以正常运行了:

DBSQLite db("MQL5Book/DB/Example1");                            // open base
DBQuery *query = db.prepare("PRAGMA table_xinfo('Struct')");    // prepare the request
DBRowStruct<DBTableColumn> *row = query.start<DBTableColumn>(); // get object cursor 
DBTableColumn columns[];                                        // receiving array of objects
while(row.next())             // loop while there are records in the query result
{
   PUSH(columnsrow.get());  // getting an object from the current record
}
ArrayPrint(columns);

此示例从数据库中读取有关特定表配置的元信息(我们在 执行没有 MQL5 数据绑定的查询一节的 DBTableColumn 示例中创建了它):每一列都由一条包含多个字段的记录描述(SQLite 标准),这在结构体 DBTableColumn 中形式化了。

struct DBTableColumn
{
   int cid;              // identifier (serial number)
   string name;          // name
   string type;          // type
   bool not_null;        // attribute NOT NULL (yes/no)
   string default_value// default value
   bool primary_key;     // PRIMARY KEY sign (yes/no)
};

为了让用户不必每次都编写将结果记录转换为结构体对象的循环,DBQuery 类提供了一个模板方法 readAll,该方法用查询结果中的信息填充一个引用的结构体数组。类似的 readAll 方法会填充一个指向 DBRow 对象的指针数组(这更适合接收包含来自不同表的列的综合查询结果)。

在四重操作中,DBRowStruct::get CRUD 方法负责字母 R (Read)。为了使对象的读取功能更完整,我们将支持按标识符从数据库中进行对象的点恢复。

SQLite 数据库中的绝大多数表都有一个主键 rowid(除非开发者出于某种原因在描述中使用了 "WITHOUT ROWID" 选项),因此新的 read 方法将接受一个键值作为参数。默认情况下,表名假定等于接收结构体的类型,但可以通过 table 参数更改为替代类型。考虑到这样的请求是一次性的并且应该返回一条记录,将 read 方法直接放在 DBSQLite 类中并在内部管理短暂存活的 DBQueryDBRowStruct<S> 对象是有意义的。

class DBSQLite
{
   ...
public:
   template<typename S>
   bool read(const long rowidS &sconst string table = NULL,
      const string column = "rowid")
   {
      const static string query = "SELECT * FROM '%s' WHERE %s=%ld;";
      const string sql = StringFormat(query,
         StringLen(table) ? table : typename(S), columnrowid);
      PRTF(sql);
      DBQuery q(handlesql);
      if(!q.isValid()) return false;
      DBRowStruct<S> *r = q.start<S>();
      if(r.next())
      {
         s = r.get();
         return true;
      }
      return false;
   }
};

主要工作由 SQL 查询 "SELECT * FROM '%s' WHERE %s=%ld;" 完成,该查询通过匹配 rowid 键从指定表中返回包含所有字段的记录。

现在你可以像这样从数据库创建一个特定对象(假定我们感兴趣的标识符必须存储在某处)。

   DBSQLite db("MQL5Book/DB/Example1");
   long rowid = ... // ill in the identifier
   Struct s
   if(db.read(rowids))
      StructPrint(s);

最后,在一些需要最大查询灵活性的复杂情况下(例如,多个表的组合,通常是带有 JOIN 的 SELECT,或嵌套查询),我们仍然必须允许使用显式的 SQL 命令来获取选择结果,尽管这违反了 ORM 原则。这种可能性由 DBSQLite::prepare 方法开启,我们已经在 管理预置查询一节中介绍过。

我们已经考虑了所有主要的读取方式。

然而,我们还没有任何东西可以从数据库中读取,因为我们跳过了添加记录的步骤。

我们尝试实现对象的创建 (C)。回想一下,在我们的对象概念中,结构体类型半自动地定义数据库表(使用 DB_FIELD 宏)。例如,Struct 结构体允许在数据库中创建一个名为 "Struct" 的表,其列集与该结构体的字段相对应。我们通过 DBSQLite 类中的模板方法 createTable 提供了此功能。现在,同样需要编写一个模板方法 insert,它将向此表中添加一条记录。

一个结构体对象被传递给该方法,对于其类型,必须存在已填充的 DBEntity<S>::prototype <S> 数组(它由宏填充)。借助这个数组,我们可以形成一个参数列表(更准确地说,是它们的替代符 '?n'):这是由静态方法 qlist 完成的。然而,查询的准备工作仍然只完成了一半。在下面的代码中,我们将需要根据对象的特性绑定输入数据。

"INSERT" 命令中添加了 "RETURNING rowid" 语句,因此当查询成功时,我们期望得到一个包含单个值(新 rowid)的结果行。

class DBSQLite
{
   ...
public:
   template<typename S>
   long insert(S &objectconst string table = NULL)
   {
      const static string query = "INSERT INTO '%s' VALUES(%s) RETURNING rowid;";
      const int n = ArrayRange(DBEntity<S>::prototype0);
      const string sql = StringFormat(query,
         StringLen(table) ? table : typename(S), qlist(n));
      PRTF(sql);
      DBQuery q(handlesql);
      if(!q.isValid()) return 0;
      DBRow *r = q.start<DBValue>();
      if(object.bindAll(q))
      {
         if(r.next()) // the result should be one record with one new rowid value
         {
            return object.rowid(r[0].integer_value);
         }
      }
      return 0;
   }
   
   static string qlist(const int n)
   {
      string result = "?1";
      for(int i = 1i < n; ++i)
      {
         result += StringFormat(",?%d", (i + 1));
      }
      return result;
   }
};

insert 方法的源代码中有一点需要特别注意。为了将值绑定到查询参数,我们调用 object.bindAll(q) 方法。这意味着在你想要与数据库集成的应用程序结构体中,你需要实现这样一个方法,该方法为引擎提供所有成员变量。

此外,为了识别对象,假定存在一个带有主键的字段,并且只有该对象“知道”这个字段是什么。因此,该结构体具有 rowid 方法,它具有双重作用:首先,它将在数据库中分配的记录标识符传输给对象;其次,如果之前已经分配过,它允许从对象中找出这个标识符。

用于更改记录的 DBSQLite::update (U) 方法在许多方面与 insert 类似,因此建议你自行熟悉它。其基础是 SQL 查询 "UPDATE '%s' SET (%s)=(%s) WHERE rowid=%ld;",该查询应该传递结构体的所有字段(bindAll() 对象)和键(rowid() 对象)。

最后,我们提到通过对象进行的记录的点删除 (D) 是在 DBSQLite::remove 方法中实现的(单词 delete 是 MQL5 的一个运算符)。

我们在一个示例脚本 DBfillTableFromStructArray.mq5 中展示所有方法,其中定义了新 Struct 结构体。

我们将把几种常用类型的值作为该结构体的字段。

struct Struct
{
   long id;
   string name;
   double number;
   datetime timestamp;
   string image;
   ...
};

在字符串字段 image 中,调用代码将指定图形资源的名称或文件名,在绑定到数据库时,相应的二进制数据将作为 BLOB 复制。随后,当我们从数据库中将数据读入 Struct 对象时,二进制数据最终会进入 image 字符串,但当然会有失真(因为字符串会在第一个空字节处中断)。要从数据库中准确提取 BLOB,你需要调用 DBRow::getBlob 方法(基于 DatabaseColumnBlob)。

创建关于 Struct 结构体字段的元信息会提供以下宏。MQL 程序可以基于它们自动在数据库中为 Struct 对象创建一个表,并根据对象的特性初始化传递给查询的数据的绑定(这种绑定不应与用于获取查询结果的反向绑定,即 DatabaseReadBind,相混淆)。

DB_FIELD_C1(StructlongidDB_CONSTRAINT::PRIMARY_KEY);
DB_FIELD(Structstringname);
DB_FIELD(Structdoublenumber);
DB_FIELD_C1(StructdatetimetimestampDB_CONSTRAINT::CURRENT_TIMESTAMP);
DB_FIELD(Structblobimage);

为了填充一个小的测试结构体数组,该脚本具有输入变量:它们指定了三种货币,其报价将进入 number 字段。我们还在脚本中嵌入了两个标准图像,以测试 BLOB 的处理:它们将“进入”image 字段。timestamp 字段将由我们的 ORM 类自动填充为记录的当前插入或修改时间戳。id 字段中的主键将必须由 SQLite 本身填充。

#resource "\\Images\\euro.bmp"
#resource "\\Images\\dollar.bmp"
   
input string Database = "MQL5Book/DB/Example2";
input string EURUSD = "EURUSD";
input string USDCNH = "USDCNH";
input string USDJPY = "USDJPY";

由于输入查询变量(那些相同的 ?n)的值最终是使用 DatabaseBindDatabaseBindArray 函数按编号绑定的,方法中的 bindAll 结构体应建立编号与其字段之间的对应关系:假定按声明顺序进行简单编号。

struct Struct
{
   ...
   bool bindAll(DBQuery &qconst
   {
      uint pixels[] = {};
      uint wh;
      if(StringLen(image))                // load binary data
      {
         if(StringFind(image"::") == 0// this is a resource
         {
            ResourceReadImage(imagepixelswh);
            // debug/test example (not BMP, no header)
            FileSave(StringSubstr(image2) + ".raw"pixels);
         }
         else                             // it's a file
         {
            const string res = "::" + image;
            ResourceCreate(resimage);
            ResourceReadImage(respixelswh);
            ResourceFree(res);
         }
      }
      // when id = NULL, the base will assign a new rowid
      return (id == 0 ? q.bindNull(0) : q.bind(0id))
         && q.bind(1name)
         && q.bind(2number)
         // && q.bind(3, timestamp) // this field will be autofilled CURRENT_TIMESTAMP
         && q.bindBlob(4pixels);
   }
   ...
};

rowid 方法非常简单。

struct Struct
{
   ...
   long rowid(const long setter = 0)
   {
      if(setterid = setter;
      return id;
   }
};

定义结构体后,我们描述一个包含 4 个元素的测试数组。其中只有 2 个附加了图像。所有对象都具有零标识符,因为它们尚未在数据库中。

Struct demo[] =
{
   {0"dollar"1.00"::Images\\dollar.bmp"},
   {0"euro"SymbolInfoDouble(EURUSDSYMBOL_ASK), 0"::Images\\euro.bmp"},
   {0"yuan"1.0 / SymbolInfoDouble(USDCNHSYMBOL_BID), 0NULL},
   {0"yen"1.0 / SymbolInfoDouble(USDJPYSYMBOL_BID), 0NULL},
};

在主函数 OnStart 中,我们创建或打开一个数据库(默认为 MQL5Book/DB/Example2.sqlite)。为以防万一,我们尝试删除 "Struct" 表,以确保重复执行脚本时的结果可重现性和调试,然后我们将为 Struct 结构体创建一个表。

void OnStart()
{
   DBSQLite db(Database);
   if(!PRTF(db.isOpen())) return;
   PRTF(db.deleteTable(typename(Struct)));
   if(!PRTF(db.createTable<Struct>(true))) return;
   ...

我们使用循环,而不是逐个添加对象:

 // -> this option (set aside)
   for(int i = 0i < ArraySize(demo); ++i)
   {
      PRTF(db.insert(demo[i])); // get a new rowid on each call
   }

在这个循环中,我们将使用 insert 方法的另一种实现,它一次性接收一个对象数组作为输入并将其在单个请求中处理,这样效率更高(但该方法的总体思路是前面考虑的单个对象的 insert 方法)。

   db.insert(demo);  // new rowids are placed in objects
   ArrayPrint(demo);
   ...

现在,我们尝试根据某些条件从数据库中选择记录,例如,那些没有分配图像的记录。为此,让我们准备一个包装在 DBQuery 对象中的 SQL 查询,然后我们以两种方式获取其结果:通过绑定到 Struct 结构体或通过通用类 DBRow 的实例。

   DBQuery *query = db.prepare(StringFormat("SELECT * FROM %s WHERE image IS NULL",
      typename(Struct)));
   
   // approach 1: application type of the Struct structure
   Struct result[];
   PRTF(query.readAll(result));
   ArrayPrint(result);
   
   query.reset(); // reset the query to try again
   
   // approach 2: generic DBRow record container with MqlParam values
   DBRow *rows[];
   query.readAll(rows); // get DBRow objects with cached values
   for(int i = 0i < ArraySize(rows); ++i)
   {
      Print(i);
      MqlParam fields[];
      rows[i].readAll(fields);
      ArrayPrint(fields);
   }
   ...

两种选项都应给出相同的结果,尽管呈现方式不同(见下面的日志)。

接下来,我们的脚本暂停 1 秒,以便我们可以注意到我们将要更改的后续条目时间戳的变化。

   Print("Pause...");
   Sleep(1000);
   ...

对于 result[] 数组中的对象,我们分配位于脚本旁边文件夹中的 "yuan.bmp" 图像。然后,我们更新数据库中的对象。

   for(int i = 0i < ArraySize(result); ++i)
   {
      result[i].image = "yuan.bmp";
      db.update(result[i]);
   }
   ...

运行脚本后,你可以在 MetaEditor 内置的数据库导航器中确保所有四条记录在数据库中都有 BLOB,以及前两条记录和后两条记录的时间戳差异。

我们演示二进制数据的提取。我们将首先看到 BLOB 如何映射到 image 字符串字段(二进制数据不用于日志,我们这样做仅为演示目的)。

   const long id1 = 1;
   Struct s;
   if(db.read(id1s))
   {
      Print("Length of string with Blob: "StringLen(s.image));
      Print(s.image);
   }
   ...

然后,我们用 getBlob 读取完整数据(总长度大于上面的行)。

   DBRow *r;
   if(db.read(id1r"Struct"))
   {
      uchar bytes[];
      Print("Actual size of Blob: "r.getBlob("image"bytes));
      FileSave("temp.bmp.raw"bytes); // not BMP, no header
   }

我们需要得到 temp.bmp.raw 文件,它与 MQL5/Files/Images/dollar.bmp.raw 相同,后者是在 Struct::bindAll 方法中为调试目的创建的。因此,很容易验证写入和读取的二进制数据的精确对应关系。

请注意,由于我们将资源的二进制内容存储在数据库中,它不是 BMP 源文件:资源会进行 颜色归一化 并存储一个无头像素数组以及关于图像的元信息。

运行该脚本会生成详细的日志。特别是,数据库和表的创建由以下几行标记。

db.isOpen()=true / ok
db.deleteTable(typename(Struct))=true / ok
sql=CREATE TABLE IF NOT EXISTS Struct (id INTEGER PRIMARY KEY,
name TEXT ,
number REAL ,
timestamp INTEGER CURRENT_TIMESTAMP,
image BLOB ); / ok
db.createTable<Struct>(true)=true / ok

用于插入对象数组的 SQL 查询准备一次,然后使用预先绑定的不同数据多次执行(此处仅显示一次迭代)。DatabaseBind 函数调用的数量与查询中的 '?n' 变量匹配('?4' 被我们的类自动替换为 SQL STRFTIME('%s') 函数调用,以获取当前 UTC 时间戳)。

sql=INSERT INTO 'Struct' VALUES(?1,?2,?3,STRFTIME('%s'),?5) RETURNING rowid; / ok
DatabasePrepare(db,sql)=131073 / ok
DatabaseBindArray(handle,index,value)=true / ok
DatabaseBind(handle,index,value)=true / ok
DatabaseBind(handle,index,value)=true / ok
DatabaseBindArray(handle,index,value)=true / ok
DatabaseRead(query)=true / ok
...

接下来,一个已经分配了主键 rowid 的结构体数组输出到日志的第一列。

    [id]   [name] [number]         [timestamp]               [image]
[0]    1 "dollar"  1.00000 1970.01.01 00:00:00 "::Images\dollar.bmp"
[1]    2 "euro"    1.00402 1970.01.01 00:00:00 "::Images\euro.bmp"  
[2]    3 "yuan"    0.14635 1970.01.01 00:00:00 null                 
[3]    4 "yen"     0.00731 1970.01.01 00:00:00 null

选择没有图像的记录会得到以下结果(我们用不同方法执行此查询两次:第一次我们填充 Struct 结构体数组,第二次是 DBRow 数组,从中为每个字段获取 MqlParam 形式的“值”)。

DatabasePrepare(db,sql)=196609 / ok
DatabaseReadBind(query,object)=true / ok
DatabaseReadBind(query,object)=true / ok
DatabaseReadBind(query,object)=false / DATABASE_NO_MORE_DATA(5126)
query.readAll(result)=true / ok
    [id] [name] [number]         [timestamp] [image]
[0]    3 "yuan"  0.14635 2022.08.20 13:14:38 null   
[1]    4 "yen"   0.00731 2022.08.20 13:14:38 null   
DatabaseRead(query)=true / ok
DatabaseRead(query)=true / ok
DatabaseRead(query)=false / DATABASE_NO_MORE_DATA(5126)
0
    [type] [integer_value] [double_value] [string_value]
[0]      4               3        0.00000 null          
[1]     14               0        0.00000 "yuan"        
[2]     13               0        0.14635 null          
[3]     10      1661001278        0.00000 null          
[4]      0               0        0.00000 null          
1
    [type] [integer_value] [double_value] [string_value]
[0]      4               4        0.00000 null          
[1]     14               0        0.00000 "yen"         
[2]     13               0        0.00731 null          
[3]     10      1661001278        0.00000 null          
[4]      0               0        0.00000 null          
...

脚本的第二部分更新了几个找到的没有图像的记录,并向它们添加了 BLOB。

Pause...
sql=UPDATE 'Struct' SET (id,name,number,timestamp,image)=
   (?1,?2,?3,STRFTIME('%s'),?5) WHERE rowid=3; / ok
DatabasePrepare(db,sql)=262145 / ok
DatabaseBind(handle,index,value)=true / ok
DatabaseBind(handle,index,value)=true / ok
DatabaseBind(handle,index,value)=true / ok
DatabaseBindArray(handle,index,value)=true / ok
DatabaseRead(handle)=false / DATABASE_NO_MORE_DATA(5126)
sql=UPDATE 'Struct' SET (id,name,number,timestamp,image)=
   (?1,?2,?3,STRFTIME('%s'),?5) WHERE rowid=4; / ok
DatabasePrepare(db,sql)=327681 / ok
DatabaseBind(handle,index,value)=true / ok
DatabaseBind(handle,index,value)=true / ok
DatabaseBind(handle,index,value)=true / ok
DatabaseBindArray(handle,index,value)=true / ok
DatabaseRead(handle)=false / DATABASE_NO_MORE_DATA(5126)
...

最后,可通过以下两种方式获取二进制数据:不兼容方式和兼容方式,前者通过 image 字符串字段作为读取整个 DatabaseReadBind 对象的结果(这样做只是为了在日志中可视化字节序列);后者通过 DatabaseReadDatabaseColumnBlob,这两种方式给出的结果不同:当然,第二种方法是正确的:4096 字节的 BLOB 的长度和内容都得到了恢复。

sql=SELECT * FROM 'Struct' WHERE rowid=1; / ok
DatabasePrepare(db,sql)=393217 / ok
DatabaseReadBind(query,object)=true / ok
Length of string with Blob: 922

ʭ7?ʭ7?ʭ7?ʭ7?ʭ7?ʭ7?ɬ7?ȫ6?ũ6?Ĩ5???5?¦5?Ĩ5?ƪ6?ȫ6?Ȭ7?ɬ7?ɬ7?ʭ7?ʭ7?ʭ7?ʭ7?ʭ7?ʭ7?ʭ7?ʭ7?ʭ7?ʭ7?ʭ7??҉??֒??ٛ...
sql=SELECT * FROM 'Struct' WHERE rowid=1; / ok
DatabasePrepare(db,sql)=458753 / ok
DatabaseRead(query)=true / ok
Actual size of Blob: 4096

总结我们自己开发的 ORM 包装器的中间结果,我们呈现了其类的概括性方案。

ORM 类图 (MQL5<->SQL)

ORM 类图 (MQL5<->SQL)