Ví dụ về các thao tác CRUD trong SQLite thông qua các đối tượng ORM
Chúng ta đã nghiên cứu tất cả các hàm cần thiết để thực hiện toàn bộ vòng đời của thông tin trong cơ sở dữ liệu, tức là CRUD (Tạo, Đọc, Cập nhật, Xóa). Nhưng trước khi tiến hành thực hành, chúng ta cần hoàn thiện lớp ORM.
Từ vài phần trước, đã rõ ràng rằng đơn vị làm việc với cơ sở dữ liệu là một bản ghi: nó có thể là một bản ghi trong bảng cơ sở dữ liệu hoặc một phần tử trong kết quả của một truy vấn. Để đọc một bản ghi duy nhất ở cấp độ ORM, hãy giới thiệu lớp DBRow
. Mỗi bản ghi được tạo ra bởi một truy vấn SQL, vì vậy xử lý của nó được truyền vào hàm tạo.
Như chúng ta biết, một bản ghi có thể bao gồm nhiều cột, số lượng và kiểu của chúng cho phép chúng ta tìm hiểu thông qua các hàm DatabaseColumn
DatabaseColumn. Để hiển thị thông tin này cho chương trình MQL sử dụng DBRow
, chúng ta đã dành sẵn các biến liên quan: columns
và một mảng cấu trúc DBRowColumn
(cái sau chứa ba trường để lưu trữ tên, kiểu và kích thước của cột).
Ngoài ra, các đối tượng DBRow
có thể, nếu cần, lưu trữ trong chính nó các giá trị lấy từ cơ sở dữ liệu. Để làm điều này, mảng data
kiểu MqlParam
MqlParam được sử dụng. Vì chúng ta không biết trước kiểu giá trị nào sẽ có trong một cột cụ thể, chúng ta sử dụng MqlParam
như một loại phổ quát Variant
có sẵn trong các môi trường lập trình khác.
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;
}
...
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Biến cursor
theo dõi số bản ghi hiện tại từ kết quả truy vấn. Cho đến khi yêu cầu hoàn tất, cursor
bằng -1.
Phương thức ảo DBread
chịu trách nhiệm thực thi truy vấn; nó gọi DatabaseRead
.
protected:
virtual bool DBread()
{
return PRTF(DatabaseRead(query));
}
2
3
4
5
Chúng ta sẽ thấy sau tại sao chúng ta cần một phương thức ảo. Phương thức công khai next
, sử dụng DBread
, cung cấp khả năng "cuộn" qua các bản ghi kết quả và trông như sau.
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;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Nếu truy vấn được truy cập lần đầu tiên, chúng ta cấp phát bộ nhớ và đọc thông tin cột. Nếu yêu cầu lưu trữ đệm, chúng ta bổ sung thêm mảng data
. Để làm điều này, toán tử '[]' được nạp chồng được gọi cho mỗi cột. Trong đó, tùy thuộc vào kiểu giá trị, chúng ta gọi hàm DatabaseColumn
phù hợp và đặt giá trị thu được vào một hoặc một trường khác của cấu trúc 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;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
Phương thức getBlob
được cung cấp để đọc hoàn toàn dữ liệu nhị phân từ các trường BLOB (sử dụng kiểu uchar
làm S để lấy mảng byte nếu không có thông tin cụ thể hơn về định dạng nội dung).
template<typename S>
int getBlob(const int i, S &object[])
{
...
return DatabaseColumnBlob(query, i, object);
}
2
3
4
5
6
Đối với các phương thức đã mô tả, quá trình thực thi truy vấn và đọc kết quả của nó có thể được biểu diễn bằng mã giả sau (nó bỏ qua các lớp hiện có DBSQLite
và DBQuery
, nhưng chúng ta sẽ sớm kết hợp tất cả lại):
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
}
}
2
3
4
5
6
7
8
9
Việc viết vòng lặp qua các cột một cách rõ ràng mỗi lần không thanh lịch, vì vậy lớp cung cấp một phương thức để lấy giá trị của tất cả các trường của bản ghi.
void readAll(MqlParam ¶ms[]) const
{
ArrayResize(params, columns);
for(int i = 0; i < columns; ++i)
{
params[i] = this[i];
}
}
2
3
4
5
6
7
8
Ngoài ra, lớp này nhận được sự tiện lợi từ việc nạp chồng toán tử '[]' và phương thức getBlob
để đọc các trường theo tên của chúng thay vì chỉ số. Ví dụ:
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;
}
...
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Bằng cách này, bạn có thể truy cập các cột đã chọn.
int query = ...
DBRow *row = new DBRow(query);
for(int i = 1; row.next(); )
{
Print(i++, " ", row["trades"], " ", row["profit"], " ", row["drawdown"]);
}
2
3
4
5
6
Nhưng việc lấy từng phần tử của bản ghi riêng lẻ, dưới dạng mảng MqlParam
, không thể gọi là cách tiếp cận OOP thực sự. Sẽ tốt hơn nếu đọc toàn bộ bản ghi bảng cơ sở dữ liệu vào một đối tượng, một cấu trúc ứng dụng. Hãy nhớ rằng API MQL5 cung cấp một hàm phù hợp: DatabaseReadBind
. Đây là nơi chúng ta nhận được lợi thế từ khả năng mô tả một lớp dẫn xuất DBRow
và ghi đè phương thức ảo DBRead
của nó.
Lớp DBRowStruct
này là một mẫu và mong đợi tham số S là một trong những cấu trúc đơn giản được phép liên kết trong 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;
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Với lớp dẫn xuất, chúng ta có thể lấy các đối tượng từ cơ sở dữ liệu gần như liền mạch.
int query = ...
DBRowStruct<MyStruct> *row = new DBRowStruct<MyStruct>(query);
MyStruct structs[];
while(row.next())
{
PUSH(structs, row.get());
}
2
3
4
5
6
7
Bây giờ là lúc để biến mã giả thành mã hoạt động bằng cách liên kết DBRow/DBRowStruct
với DBQuery
. Trong DBQuery
, chúng ta thêm một con trỏ tự động đến đối tượng DBRow
, đối tượng này sẽ chứa dữ liệu về bản ghi hiện tại từ kết quả truy vấn (nếu nó được thực thi). Sử dụng con trỏ tự động giải phóng mã gọi khỏi việc lo lắng về việc giải phóng các đối tượng DBRow
: chúng được xóa cùng với DBQuery
hoặc khi được tạo lại do khởi động lại truy vấn (nếu cần). Việc khởi tạo đối tượng DBRow
hoặc DBRowStruct
được hoàn thành bởi một phương thức mẫu 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[];
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Kiểu DBValue
là một cấu trúc giả chỉ cần thiết để hướng dẫn chương trình tạo đối tượng DBRow
cơ bản, mà không vi phạm khả năng biên dịch của dòng có lời gọi DatabaseReadBind
.
Với phương thức start
, tất cả các đoạn mã giả trên trở thành hoạt động nhờ việc chuẩn bị yêu cầu sau:
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);
2
3
4
5
6
7
8
9
Ví dụ này đọc siêu thông tin về cấu hình của một bảng cụ thể từ cơ sở dữ liệu (chúng ta đã tạo nó trong ví dụ DBcreateTableFromStruct.mq5
trong phần Thực thi truy vấn mà không liên kết dữ liệu MQL5): mỗi cột được mô tả bởi một bản ghi riêng biệt với vài trường (chuẩn SQLite), được chính thức hóa trong cấu trúc 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)
};
2
3
4
5
6
7
8
9
Để giúp người dùng không phải viết vòng lặp mỗi lần với việc chuyển đổi các bản ghi kết quả thành các đối tượng cấu trúc, lớp DBQuery
cung cấp một phương thức mẫu readAll
điền vào một mảng tham chiếu của các cấu trúc với thông tin từ kết quả truy vấn. Một phương thức readAll
tương tự điền vào một mảng các con trỏ đến các đối tượng DBRow
(điều này phù hợp hơn để nhận kết quả của các truy vấn tổng hợp với các cột từ các bảng khác nhau).
Trong bộ tứ thao tác CRUD, phương thức DBRowStruct::get
chịu trách nhiệm cho chữ R (Đọc). Để làm cho việc đọc một đối tượng trở nên đầy đủ chức năng hơn, chúng ta sẽ hỗ trợ khôi phục điểm của một đối tượng từ cơ sở dữ liệu bằng định danh của nó.
Phần lớn các bảng trong cơ sở dữ liệu SQLite có khóa chính rowid
(trừ khi nhà phát triển vì lý do nào đó sử dụng tùy chọn "WITHOUT ROWID" trong mô tả), vì vậy phương thức read
mới sẽ nhận giá trị khóa làm tham số. Theo mặc định, tên bảng được giả định bằng với kiểu của cấu trúc nhận nhưng có thể được thay đổi thành một bảng thay thế thông qua tham số table
. Xét rằng yêu cầu như vậy là một yêu cầu một lần và nên trả về một bản ghi, việc đặt phương thức read
trực tiếp vào lớp DBSQLite
và quản lý các đối tượng ngắn hạn DBQuery
và DBRowStruct<S>
bên trong là hợp lý.
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;
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Công việc chính được thực hiện bởi truy vấn SQL SELECT * FROM '%s' WHERE %s=%ld;
, truy vấn này trả về một bản ghi với tất cả các trường từ bảng được chỉ định bằng cách khớp với khóa rowid
.
Bây giờ bạn có thể tạo một đối tượng cụ thể từ cơ sở dữ liệu như sau (giả định rằng định danh mà chúng ta quan tâm phải được lưu trữ ở đâu đó).
DBSQLite db("MQL5Book/DB/Example1");
long rowid = ... // điền định danh
Struct s;
if(db.read(rowid, s))
StructPrint(s);
2
3
4
5
Cuối cùng, trong một số trường hợp phức tạp khi cần sự linh hoạt tối đa trong việc truy vấn (ví dụ, kết hợp nhiều bảng, thường là một SELECT
với JOIN
, hoặc các truy vấn lồng nhau), chúng ta vẫn phải cho phép một lệnh SQL rõ ràng để lấy một tập hợp dữ liệu, mặc dù điều này vi phạm nguyên tắc ORM. Khả năng này được mở ra bởi phương thức DBSQLite::prepare
, mà chúng ta đã trình bày trong bối cảnh quản lý các truy vấn đã chuẩn bị.
Chúng ta đã xem xét tất cả các cách đọc chính.
Tuy nhiên, chúng ta chưa có gì để đọc từ cơ sở dữ liệu, vì chúng ta đã bỏ qua bước thêm bản ghi.
Hãy thử thực hiện việc tạo đối tượng (C). Nhớ rằng trong khái niệm đối tượng của chúng ta, các kiểu cấu trúc tự động xác định các bảng cơ sở dữ liệu một cách bán tự động (sử dụng macro DB_FIELD
). Ví dụ, cấu trúc Struct
cho phép tạo bảng Struct
trong cơ sở dữ liệu với một tập hợp các cột tương ứng với các trường của cấu trúc. Chúng ta đã cung cấp điều này với phương thức mẫu createTable
trong lớp DBSQLite
. Bây giờ, tương tự, bạn cần viết một phương thức mẫu insert
, phương thức này sẽ thêm một bản ghi vào bảng đó.
Một đối tượng của cấu trúc được truyền vào phương thức, với kiểu mà mảng DBEntity<S>::prototype
đã được điền (nó được điền bằng macro). Nhờ mảng này, chúng ta có thể tạo danh sách các tham số (chính xác hơn là các ký tự thay thế '?n'): điều này được thực hiện bởi phương thức tĩnh qlist
. Tuy nhiên, việc chuẩn bị truy vấn chỉ là một nửa công việc. Trong mã bên dưới, chúng ta sẽ cần liên kết dữ liệu đầu vào dựa trên các thuộc tính của đối tượng.
Một câu lệnh RETURNING rowid
đã được thêm vào lệnh INSERT
, vì vậy khi truy vấn thành công, chúng ta mong đợi một hàng kết quả duy nhất với một giá trị: rowid
mới.
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()) // kết quả nên là một bản ghi với một giá trị rowid mới
{
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;
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
Mã nguồn của phương thức insert
có một điểm cần chú ý đặc biệt. Để liên kết các giá trị với các tham số truy vấn, chúng ta gọi phương thức object.bindAll(q)
. Điều này có nghĩa là trong cấu trúc ứng dụng mà bạn muốn tích hợp với cơ sở dữ liệu, bạn cần triển khai một phương thức như vậy để cung cấp tất cả các biến thành viên cho engine.
Ngoài ra, để định danh các đối tượng,假 định rằng có một trường với khóa chính, và chỉ đối tượng "biết" trường này là gì. Vì vậy, cấu trúc có phương thức rowid
, phục vụ hai tác dụng: đầu tiên, nó chuyển định danh bản ghi được gán trong cơ sở dữ liệu sang đối tượng, và thứ hai, nó cho phép tìm ra định danh này từ đối tượng, nếu nó đã được gán trước đó.
Phương thức DBSQLite::update
(U) để thay đổi một bản ghi có nhiều điểm tương đồng với insert
, và do đó bạn được đề nghị tự làm quen với nó. Cơ sở của nó là truy vấn SQL UPDATE '%s' SET (%s)=(%s) WHERE rowid=%ld;
, được cho là sẽ truyền tất cả các trường của cấu trúc (đối tượng bindAll()
) và khóa (đối tượng rowid()
).
Cuối cùng, chúng ta đề cập rằng việc xóa điểm (D) của một bản ghi theo đối tượng được triển khai trong phương thức DBSQLite::remove
(từ delete
là một toán tử MQL5).
Hãy thể hiện tất cả các phương thức trong một ví dụ kịch bản DBfillTableFromStructArray.mq5
, nơi cấu trúc mới Struct
được định nghĩa.
Chúng ta sẽ tạo một số giá trị của các kiểu thường dùng làm trường của cấu trúc.
struct Struct
{
long id;
string name;
double number;
datetime timestamp;
string image;
...
};
2
3
4
5
6
7
8
9
Trong trường chuỗi image
, mã gọi sẽ chỉ định tên của tài nguyên đồ họa hoặc tên tệp, và tại thời điểm liên kết với cơ sở dữ liệu, dữ liệu nhị phân tương ứng sẽ được sao chép dưới dạng BLOB. Sau đó, khi chúng ta đọc dữ liệu từ cơ sở dữ liệu vào các đối tượng Struct
, dữ liệu nhị phân sẽ nằm trong chuỗi image
nhưng tất nhiên sẽ bị méo mó (vì chuỗi sẽ bị ngắt ở byte null đầu tiên). Để trích xuất chính xác các BLOB từ cơ sở dữ liệu, bạn sẽ cần gọi phương thức DBRow::getBlob
(dựa trên DatabaseColumnBlob
).
Việc tạo siêu thông tin về các trường của cấu trúc Struct
được cung cấp bởi các macro sau. Dựa trên chúng, một chương trình MQL có thể tự động tạo bảng trong cơ sở dữ liệu cho các đối tượng Struct
, cũng như bắt đầu liên kết dữ liệu được truyền vào các truy vấn dựa trên các thuộc tính của các đối tượng (liên kết này không nên nhầm lẫn với liên kết ngược để lấy kết quả truy vấn, tức là 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);
2
3
4
5
Để điền vào một mảng thử nghiệm nhỏ các cấu trúc, kịch bản có các biến đầu vào: chúng chỉ định một bộ ba tiền tệ mà giá của chúng sẽ rơi vào trường number
. Chúng ta cũng đã nhúng hai hình ảnh tiêu chuẩn vào kịch bản để kiểm tra việc làm việc với BLOB: chúng sẽ "đi" vào trường image
. Trường timestamp
sẽ được các lớp ORM của chúng ta tự động điền với dấu thời gian chèn hoặc sửa đổi hiện tại của bản ghi. Khóa chính trong trường id
sẽ phải được SQLite tự điền.
#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";
2
3
4
5
6
7
Vì các giá trị cho các biến truy vấn đầu vào (những '?n' đó) được liên kết, cuối cùng, sử dụng các hàm DatabaseBind
hoặc DatabaseBindArray
dưới các số, cấu trúc bindAll
của chúng ta trong phương thức nên thiết lập sự tương ứng giữa các số và các trường của chúng: một cách đánh số đơn giản được giả định theo thứ tự khai báo.
struct Struct
{
...
bool bindAll(DBQuery &q) const
{
uint pixels[] = {};
uint w, h;
if(StringLen(image)) // tải dữ liệu nhị phân
{
if(StringFind(image, "::") == 0) // đây là một tài nguyên
{
ResourceReadImage(image, pixels, w, h);
// ví dụ gỡ lỗi/thử nghiệm (không phải BMP, không có tiêu đề)
FileSave(StringSubstr(image, 2) + ".raw", pixels);
}
else // đây là một tệp
{
const string res = "::" + image;
ResourceCreate(res, image);
ResourceReadImage(res, pixels, w, h);
ResourceFree(res);
}
}
// khi id = NULL, cơ sở dữ liệu sẽ gán một rowid mới
return (id == 0 ? q.bindNull(0) : q.bind(0, id))
&& q.bind(1, name)
&& q.bind(2, number)
// && q.bind(3, timestamp) // trường này sẽ được tự động điền CURRENT_TIMESTAMP
&& q.bindBlob(4, pixels);
}
...
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Phương thức rowid
rất đơn giản.
struct Struct
{
...
long rowid(const long setter = 0)
{
if(setter) id = setter;
return id;
}
};
2
3
4
5
6
7
8
9
Sau khi định nghĩa cấu trúc, chúng ta mô tả một mảng thử nghiệm gồm 4 phần tử. Chỉ 2 trong số đó có hình ảnh đính kèm. Tất cả các đối tượng có định danh bằng 0 vì chúng chưa có trong cơ sở dữ liệu.
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},
};
2
3
4
5
6
7
Trong hàm chính OnStart
, chúng ta tạo hoặc mở một cơ sở dữ liệu (mặc định là MQL5Book/DB/Example2.sqlite
). Để đề phòng, chúng ta cố gắng xóa bảng Struct
để đảm bảo khả năng tái tạo kết quả và gỡ lỗi khi kịch bản được lặp lại, sau đó chúng ta sẽ tạo một bảng cho cấu trúc Struct
.
void OnStart()
{
DBSQLite db(Database);
if(!PRTF(db.isOpen())) return;
PRTF(db.deleteTable(typename(Struct)));
if(!PRTF(db.createTable<Struct>(true))) return;
...
}
2
3
4
5
6
7
8
Thay vì thêm các đối tượng từng cái một, chúng ta sử dụng một vòng lặp:
// -> tùy chọn này (đặt sang một bên)
for(int i = 0; i < ArraySize(demo); ++i)
{
PRTF(db.insert(demo[i])); // nhận một rowid mới cho mỗi lần gọi
}
2
3
4
5
Trong vòng lặp này, chúng ta sẽ sử dụng một triển khai thay thế của phương thức insert
, phương thức này nhận một mảng các đối tượng làm đầu vào cùng một lúc và xử lý chúng trong một yêu cầu duy nhất, điều này hiệu quả hơn (nhưng cách tiếp cận chung của phương thức là phương thức insert
đã xem xét trước đó cho một đối tượng).
db.insert(demo); // các rowid mới được đặt trong các đối tượng
ArrayPrint(demo);
...
2
3
Bây giờ hãy thử chọn các bản ghi từ cơ sở dữ liệu theo một số điều kiện, ví dụ, những bản ghi không có hình ảnh được gán. Để làm điều này, hãy chuẩn bị một truy vấn SQL được bao bọc trong đối tượng DBQuery
, và sau đó chúng ta nhận kết quả của nó theo hai cách: thông qua liên kết với các cấu trúc Struct
hoặc qua các thể hiện của lớp chung DBRow
.
DBQuery *query = db.prepare(StringFormat("SELECT * FROM %s WHERE image IS NULL",
typename(Struct)));
// cách tiếp cận 1: kiểu ứng dụng của cấu trúc Struct
Struct result[];
PRTF(query.readAll(result));
ArrayPrint(result);
query.reset(); // đặt lại truy vấn để thử lại
// cách tiếp cận 2: bộ chứa bản ghi chung DBRow với các giá trị MqlParam
DBRow *rows[];
query.readAll(rows); // nhận các đối tượng DBRow với các giá trị được lưu trữ
for(int i = 0; i < ArraySize(rows); ++i)
{
Print(i);
MqlParam fields[];
rows[i].readAll(fields);
ArrayPrint(fields);
}
...
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Cả hai tùy chọn nên cho kết quả giống nhau, mặc dù được trình bày khác nhau (xem nhật ký bên dưới).
Tiếp theo, kịch bản của chúng ta tạm dừng trong 1 giây để chúng ta có thể nhận thấy sự thay đổi trong dấu thời gian của các mục tiếp theo mà chúng ta sẽ thay đổi.
Print("Pause...");
Sleep(1000);
...
2
3
Đối với các đối tượng trong mảng result[]
, chúng ta gán hình ảnh yuan.bmp
nằm trong thư mục bên cạnh kịch bản. Sau đó, chúng ta cập nhật các đối tượng trong cơ sở dữ liệu.
for(int i = 0; i < ArraySize(result); ++i)
{
result[i].image = "yuan.bmp";
db.update(result[i]);
}
...
2
3
4
5
6
Sau khi chạy kịch bản, bạn có thể đảm bảo rằng tất cả bốn bản ghi có BLOB trong trình điều hướng cơ sở dữ liệu tích hợp trong MetaEditor, cũng như sự khác biệt trong dấu thời gian cho hai bản ghi đầu tiên và hai bản ghi cuối cùng.
Hãy thể hiện việc trích xuất dữ liệu nhị phân. Chúng ta sẽ thấy đầu tiên cách một BLOB được ánh xạ vào trường chuỗi image
(dữ liệu nhị phân không dành cho nhật ký, chúng ta chỉ làm điều này cho mục đích trình diễn).
const long id1 = 1;
Struct s;
if(db.read(id1, s))
{
Print("Length of string with Blob: ", StringLen(s.image));
Print(s.image);
}
...
2
3
4
5
6
7
8
Sau đó, chúng ta đọc toàn bộ dữ liệu với getBlob
(tổng độ dài lớn hơn chuỗi ở trên).
DBRow *r;
if(db.read(id1, r, "Struct"))
{
uchar bytes[];
Print("Actual size of Blob: ", r.getBlob("image", bytes));
FileSave("temp.bmp.raw", bytes); // không phải BMP, không có tiêu đề
}
2
3
4
5
6
7
Chúng ta cần lấy tệp temp.bmp.raw
, giống hệt với MQL5/Files/Images/dollar.bmp.raw
, được tạo trong phương thức Struct::bindAll
cho mục đích gỡ lỗi. Do đó, dễ dàng xác minh sự tương ứng chính xác của dữ liệu nhị phân được ghi và đọc.
Lưu ý rằng vì chúng ta đang lưu trữ nội dung nhị phân của tài nguyên trong cơ sở dữ liệu, nó không phải là tệp nguồn BMP: các tài nguyên tạo ra chuẩn hóa màu và lưu trữ một mảng pixel không có tiêu đề với siêu thông tin về hình ảnh.
Trong khi chạy, kịch bản tạo ra một nhật ký chi tiết. Đặc biệt, việc tạo cơ sở dữ liệu và bảng được đánh dấu bằng các dòng sau.
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
2
3
4
5
6
7
8
Truy vấn SQL để chèn một mảng các đối tượng được chuẩn bị một lần và sau đó thực thi nhiều lần với việc liên kết trước dữ liệu khác nhau (chỉ hiển thị một lần lặp lại ở đây). Số lượng lời gọi hàm DatabaseBind
khớp với các biến '?n' trong truy vấn ('?4' được các lớp của chúng ta tự động thay thế bằng lời gọi hàm SQL STRFTIME('%s')
để lấy dấu thời gian UTC hiện tại).
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
...
2
3
4
5
6
7
8
Tiếp theo, một mảng các cấu trúc với các khóa chính rowid
đã được gán được xuất ra nhật ký trong cột đầu tiên.
[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
2
3
4
5
Việc chọn các bản ghi không có hình ảnh cho kết quả sau (chúng ta thực thi truy vấn này hai lần với các phương thức khác nhau: lần đầu tiên chúng ta điền mảng các cấu trúc Struct
, và lần thứ hai là mảng DBRow
, từ đó cho mỗi trường chúng ta nhận được "giá trị" dưới dạng 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
...
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Phần thứ hai của kịch bản cập nhật một vài bản ghi được tìm thấy không có hình ảnh và thêm BLOB vào chúng.
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)
...
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Cuối cùng, khi lấy dữ liệu nhị phân theo hai cách — không tương thích, qua trường chuỗi image
như kết quả của việc đọc toàn bộ đối tượng DatabaseReadBind
(điều này chỉ được thực hiện để hiển thị chuỗi byte trong nhật ký) và tương thích, qua DatabaseRead
và DatabaseColumnBlob
— chúng ta nhận được kết quả khác nhau: tất nhiên, phương pháp thứ hai là đúng: độ dài và nội dung của BLOB trong 4096 byte được khôi phục.
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
2
3
4
5
6
7
8
9
Tóm tắt kết quả trung gian của việc phát triển trình bao bọc ORM của riêng chúng ta, chúng ta trình bày một sơ đồ tổng quát của các lớp của nó.
ORM Class Diagram (MQL5<->SQL)