通过 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 q, const 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(info, columns);
if(cache) ArrayResize(data, columns);
for(int i = 0; i < columns; ++i)
{
DatabaseColumnName(query, i, info[i].name);
info[i].type = DatabaseColumnType(query, i);
info[i].size = DatabaseColumnSize(query, i);
if(cache) data[i] = this[i]; // overload operator[](int)
}
}
++cursor;
}
return success;
}
|
如果查询是第一次被访问,我们分配内存并读取列信息。如果请求了缓存,我们还会填充 data 数组。为此,会为每一列调用重载的运算符 '[]'。在其中,根据值的类型,我们调用适当的 DatabaseColumn 函数,并将结果值放入 MqlParam 结构体的一个或另一个字段中。
virtual MqlParam operator[](const int i = 0) const
{
MqlParam param = {};
if(i < 0 || i >= columns) return 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(query, i, param.integer_value);
break;
case DATABASE_FIELD_TYPE_FLOAT:
param.type = info[i].size == 4 ? TYPE_FLOAT : TYPE_DOUBLE;
DatabaseColumnDouble(query, i, param.double_value);
break;
case DATABASE_FIELD_TYPE_TEXT:
param.type = TYPE_STRING;
DatabaseColumnText(query, i, param.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(query, i, blob);
uchar key[], text[];
if(CryptEncode(CRYPT_BASE64, blob, key, text))
{
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 i, S &object[])
{
...
return DatabaseColumnBlob(query, i, object);
}
|
对于所描述的方法,执行查询并读取其结果的过程可以用以下伪代码表示(它将现有的 DBSQLite 和 DBQuery 类置于后台,但我们很快会将它们整合在一起):
int query = ...
DBRow *row = new DBRow(query);
while(row.next())
{
for(int i = 0; i < row.length(); ++i)
{
StructPrint(row[i]); // print the i-th column as an MqlParam structure
}
}
|
每次都显式地编写列循环并不优雅,因此该类提供了一种获取记录所有字段值的方法。
void readAll(MqlParam ¶ms[]) const
{
ArrayResize(params, columns);
for(int i = 0; i < columns; ++i)
{
params[i] = this[i];
}
}
|
此外,为了方便起见,该类还接收了运算符 '[]' 和 getBlob 方法的重载,用于通过字段名称而不是索引来读取字段。例如,
class DBRow
{
...
public:
int name2index(const string name) const
{
for(int i = 0; i < columns; ++i)
{
if(name == info[i].name) return i;
}
Print("Wrong column name: ", name);
SetUserError(3);
return -1;
}
MqlParam operator[](const string name) const
{
const int i = name2index(name);
if(i != -1) return this[i]; // operator()[int] overload
static MqlParam param = {};
return param;
}
...
};
|
这样你就可以访问选定的列。
int query = ...
DBRow *row = new DBRow(query);
for(int i = 1; row.next(); )
{
Print(i++, " ", row["trades"], " ", row["profit"], " ", row["drawdown"]);
}
|
但是,仍然像 MqlParam 数组那样单独获取记录的元素,不能称为真正的 OOP 方法。最好是将整个数据库表记录读入一个对象,一个应用程序结构体中。回想一下,MQL5 API 提供了一个合适的函数:DatabaseReadBind。这样,我们就获得一种优势,能够描述派生类 DBRow 并覆盖其虚方法 DBRead。
这个 DBRowStruct 类是一个模板,并期望参数 S 是允许在 DatabaseReadBind 中绑定的简单结构体之一。
template<typename S>
class DBRowStruct: public 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(query, object));
}
public:
DBRowStruct(const int q, const bool c = false): DBRow(q, c)
{
}
S get() const
{
return object;
}
};
|
有了派生类,我们几乎可以无缝地从基类获取对象。
int query = ...
DBRowStruct<MyStruct> *row = new DBRowStruct<MyStruct>(query);
MyStruct structs[];
while(row.next())
{
PUSH(structs, row.get());
}
|
现在就可以将 DBRow/DBRowStruct 与 DBQuery 关联起来,将伪代码转换为工作代码了。在 DBQuery 中,我们向 DBRow 对象添加一个自动指针,该对象将包含来自查询结果的当前记录的数据(如果已执行)。使用自动指针使调用代码无需担心释放 DBRow 对象:它们要么随 DBQuery 一起删除,要么在由于查询重新启动(如果需要)而重新创建时删除。DBRow 或 DBRowStruct 对象的初始化由模板方法 start 完成。
class DBQuery
{
protected:
...
AutoPtr<DBRow> row; // current entry
public:
DBQuery(const int owner, const string s): db(owner), sql(s),
handle(PRTF(DatabasePrepare(db, sql)))
{
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(columns, row.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 类中并在内部管理短暂存活的 DBQuery 和 DBRowStruct<S> 对象是有意义的。
class DBSQLite
{
...
public:
template<typename S>
bool read(const long rowid, S &s, const 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), column, rowid);
PRTF(sql);
DBQuery q(handle, sql);
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(rowid, s))
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 &object, const string table = NULL)
{
const static string query = "INSERT INTO '%s' VALUES(%s) RETURNING rowid;";
const int n = ArrayRange(DBEntity<S>::prototype, 0);
const string sql = StringFormat(query,
StringLen(table) ? table : typename(S), qlist(n));
PRTF(sql);
DBQuery q(handle, sql);
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 = 1; i < 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(Struct, long, id, DB_CONSTRAINT::PRIMARY_KEY);
DB_FIELD(Struct, string, name);
DB_FIELD(Struct, double, number);
DB_FIELD_C1(Struct, datetime, timestamp, DB_CONSTRAINT::CURRENT_TIMESTAMP);
DB_FIELD(Struct, blob, image);
|
为了填充一个小的测试结构体数组,该脚本具有输入变量:它们指定了三种货币,其报价将进入 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)的值最终是使用 DatabaseBind 或 DatabaseBindArray 函数按编号绑定的,方法中的 bindAll 结构体应建立编号与其字段之间的对应关系:假定按声明顺序进行简单编号。
struct Struct
{
...
bool bindAll(DBQuery &q) const
{
uint pixels[] = {};
uint w, h;
if(StringLen(image)) // load binary data
{
if(StringFind(image, "::") == 0) // this is a resource
{
ResourceReadImage(image, pixels, w, h);
// debug/test example (not BMP, no header)
FileSave(StringSubstr(image, 2) + ".raw", pixels);
}
else // it's a file
{
const string res = "::" + image;
ResourceCreate(res, image);
ResourceReadImage(res, pixels, w, h);
ResourceFree(res);
}
}
// when id = NULL, the base will assign a new rowid
return (id == 0 ? q.bindNull(0) : q.bind(0, id))
&& q.bind(1, name)
&& q.bind(2, number)
// && q.bind(3, timestamp) // this field will be autofilled CURRENT_TIMESTAMP
&& q.bindBlob(4, pixels);
}
...
};
|
rowid 方法非常简单。
struct Struct
{
...
long rowid(const long setter = 0)
{
if(setter) id = setter;
return id;
}
};
|
定义结构体后,我们描述一个包含 4 个元素的测试数组。其中只有 2 个附加了图像。所有对象都具有零标识符,因为它们尚未在数据库中。
Struct demo[] =
{
{0, "dollar", 1.0, 0, "::Images\\dollar.bmp"},
{0, "euro", SymbolInfoDouble(EURUSD, SYMBOL_ASK), 0, "::Images\\euro.bmp"},
{0, "yuan", 1.0 / SymbolInfoDouble(USDCNH, SYMBOL_BID), 0, NULL},
{0, "yen", 1.0 / SymbolInfoDouble(USDJPY, SYMBOL_BID), 0, NULL},
};
|
在主函数 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 = 0; i < 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 = 0; i < ArraySize(rows); ++i)
{
Print(i);
MqlParam fields[];
rows[i].readAll(fields);
ArrayPrint(fields);
}
...
|
两种选项都应给出相同的结果,尽管呈现方式不同(见下面的日志)。
接下来,我们的脚本暂停 1 秒,以便我们可以注意到我们将要更改的后续条目时间戳的变化。
Print("Pause...");
Sleep(1000);
...
|
对于 result[] 数组中的对象,我们分配位于脚本旁边文件夹中的 "yuan.bmp" 图像。然后,我们更新数据库中的对象。
for(int i = 0; i < 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(id1, s))
{
Print("Length of string with Blob: ", StringLen(s.image));
Print(s.image);
}
...
|
然后,我们用 getBlob 读取完整数据(总长度大于上面的行)。
DBRow *r;
if(db.read(id1, r, "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 对象的结果(这样做只是为了在日志中可视化字节序列);后者通过 DatabaseRead 和 DatabaseColumnBlob,这两种方式给出的结果不同:当然,第二种方法是正确的: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)