Thực thi truy vấn không liên kết dữ liệu MQL5
Một số truy vấn SQL là các lệnh mà bạn chỉ cần gửi nguyên trạng đến động cơ. Chúng không yêu cầu đầu vào biến hoặc kết quả. Ví dụ, nếu chương trình MQL của chúng ta cần tạo một bảng, chỉ mục hoặc chế độ xem với cấu trúc và tên nhất định trong cơ sở dữ liệu, chúng ta có thể viết nó dưới dạng chuỗi hằng với câu lệnh "CREATE ...". Ngoài ra, việc sử dụng các truy vấn như vậy rất tiện lợi để xử lý hàng loạt các bản ghi hoặc kết hợp chúng (gộp, tính toán các chỉ số tổng hợp và sửa đổi cùng loại). Nghĩa là, với một truy vấn, bạn có thể chuyển đổi toàn bộ dữ liệu bảng hoặc điền vào các bảng khác dựa trên nó. Những kết quả này có thể được phân tích trong các truy vấn tiếp theo.
Trong tất cả các trường hợp này, điều quan trọng duy nhất là nhận được xác nhận về sự thành công của hành động. Các yêu cầu loại này được thực hiện bằng hàm DatabaseExecute
.
bool DatabaseExecute(int database, const string sql)
Hàm này thực thi một truy vấn trong cơ sở dữ liệu được chỉ định bởi mô tả database
. Yêu cầu chính nó được gửi dưới dạng chuỗi đã sẵn sàng sql
.
Hàm trả về một chỉ báo thành công (true
) hoặc lỗi (false
).
Ví dụ, chúng ta có thể bổ sung lớp DBSQLite
của mình với phương thức này (mô tả đã nằm trong đối tượng).
class DBSQLite
{
...
bool execute(const string sql)
{
return DatabaseExecute(handle, sql);
}
};
2
3
4
5
6
7
8
Sau đó, script tạo một bảng mới (và nếu cần, trước đó là chính cơ sở dữ liệu) có thể trông như thế này (DBcreateTable.mq5
).
input string Database = "MQL5Book/DB/Example1";
input string Table = "table1";
void OnStart()
{
DBSQLite db(Database);
if(db.isOpen())
{
PRTF(db.execute(StringFormat("CREATE TABLE %s (msg text)", Table))); // true
}
}
2
3
4
5
6
7
8
9
10
11
Sau khi thực thi script, hãy thử mở cơ sở dữ liệu được chỉ định trong MetaEditor và đảm bảo rằng nó chứa một bảng trống với một trường văn bản "msg" duy nhất. Nhưng điều này cũng có thể được thực hiện bằng lập trình (xem phần tiếp theo).
Nếu chúng ta chạy script lần thứ hai với cùng tham số, chúng ta sẽ nhận được một lỗi (dù không nghiêm trọng, không buộc chương trình đóng).
database error, table table1 already exists
db.execute(StringFormat(CREATE TABLE %s (msg text),Table))=false / DATABASE_ERROR(5601)
2
Điều này là do bạn không thể tạo lại một bảng đã tồn tại. Nhưng SQL cho phép bỏ qua lỗi này và chỉ tạo bảng nếu nó chưa tồn tại, nếu không thì gần như không làm gì và trả về chỉ báo thành công. Để làm điều này, chỉ cần thêm "IF NOT EXISTS" trước tên trong truy vấn.
db.execute(StringFormat("CREATE TABLE IF NOT EXISTS %s (msg text)", Table));
Trong thực tế, các bảng được yêu cầu để lưu trữ thông tin về các đối tượng trong lĩnh vực ứng dụng, chẳng hạn như báo giá, giao dịch và tín hiệu giao dịch. Do đó, mong muốn tự động hóa việc tạo bảng dựa trên mô tả của các đối tượng trong MQL5. Như chúng ta sẽ thấy dưới đây, các hàm SQLite cung cấp khả năng liên kết kết quả truy vấn với các cấu trúc MQL5 (nhưng không phải với các lớp). Liên quan đến điều này, trong khuôn khổ của lớp bao bọc ORM, chúng ta sẽ phát triển một cơ chế tạo truy vấn SQL "CREATE TABLE" theo mô tả struct
của loại cụ thể trong MQL5.
Điều này đòi hỏi phải đăng ký tên và kiểu của các trường cấu trúc theo một cách nào đó trong danh sách chung tại thời điểm biên dịch, và sau đó, tại giai đoạn thực thi chương trình, các truy vấn SQL có thể được tạo từ danh sách này.
Một số danh mục thực thể MQL5 được phân tích cú pháp tại giai đoạn biên dịch, có thể được sử dụng để xác định kiểu và tên:
Trước hết, cần nhớ rằng các mô tả trường thu thập được liên quan đến bối cảnh của một cấu trúc cụ thể và không nên bị trộn lẫn, vì chương trình có thể chứa nhiều cấu trúc khác nhau với tên và kiểu có thể trùng khớp. Nói cách khác, mong muốn tích lũy thông tin trong các danh sách riêng biệt cho mỗi loại cấu trúc. Một loại mẫu là lý tưởng cho việc này, tham số mẫu của nó (S) sẽ là cấu trúc ứng dụng. Hãy gọi mẫu này là DBEntity
.
template<typename S>
struct DBEntity
{
static string prototype[][3]; // 0 - type, 1 - name, 2 - constraints
...
};
template<typename T>
static string DBEntity::prototype[][3];
2
3
4
5
6
7
8
9
Bên trong mẫu, có một mảng đa chiều prototype
, trong đó chúng ta sẽ ghi mô tả của các trường. Để chặn kiểu và tên của trường ứng dụng, bạn sẽ cần khai báo một cấu trúc mẫu khác, DBField
, bên trong DBEntity
: lần này tham số T của nó là kiểu của chính trường. Trong hàm tạo, chúng ta có thông tin về kiểu này (typename(T)
), và chúng ta cũng nhận được tên của trường (và tùy chọn, ràng buộc) làm tham số.
template<typename S>
struct DBEntity
{
...
template<typename T>
struct DBField
{
T f;
DBField(const string name, const string constraints = "")
{
const int n = EXPAND(prototype);
prototype[n][0] = typename(T);
prototype[n][1] = name;
prototype[n][2] = constraints;
}
};
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Trường f
không được sử dụng nhưng cần thiết vì các cấu trúc không thể trống.
Giả sử chúng ta có một cấu trúc ứng dụng Data
(DBmetaProgramming.mq5
).
struct Data
{
long id;
string name;
datetime timestamp;
double income;
};
2
3
4
5
6
7
Chúng ta có thể tạo một bản tương tự kế thừa từ DBEntity<DataDB>
, nhưng với các trường được thay thế dựa trên DBField
, giống hệt với tập hợp ban đầu.
struct DataDB: public DBEntity<DataDB>
{
DB_FIELD(long, id);
DB_FIELD(string, name);
DB_FIELD(datetime, timestamp);
DB_FIELD(double, income);
} proto;
2
3
4
5
6
7
Bằng cách thay thế tên của cấu trúc vào tham số mẫu cha, cấu trúc cung cấp cho chương trình thông tin về các thuộc tính của chính nó.
Hãy chú ý đến việc định nghĩa một lần biến proto
cùng với khai báo cấu trúc. Điều này là cần thiết vì, trong các mẫu, mỗi loại tham số cụ thể chỉ được biên dịch nếu ít nhất một đối tượng của loại này được tạo trong mã nguồn. Điều quan trọng đối với chúng ta là việc tạo đối tượng proto này diễn ra ngay từ đầu khi chương trình khởi chạy, tại thời điểm khởi tạo các biến toàn cục.
Một macro được ẩn dưới định danh DB_FIELD:
#define DB_FIELD(T,N) struct T##_##N: DBField<T> { T##_##N() : DBField<T>(#N) { } } \
_##T##_##N;
2
Dưới đây là cách nó mở rộng cho một trường duy nhất:
struct Type_Name: DBField<Type>
{
Type_Name() : DBField<Type>(Name) { }
} _Type_Name;
2
3
4
Ở đây, cấu trúc không chỉ được định nghĩa mà còn được tạo ngay lập tức: trên thực tế, nó thay thế trường ban đầu.
Vì cấu trúc DBField
chứa một biến duy nhất f
của kiểu mong muốn, kích thước và biểu diễn nhị phân bên trong của Data
và DataDB
là giống nhau. Điều này có thể dễ dàng được xác minh bằng cách chạy script DBmetaProgramming.mq5
.
void OnStart()
{
PRTF(sizeof(Data));
PRTF(sizeof(DataDB));
ArrayPrint(DataDB::prototype);
}
2
3
4
5
6
Nó xuất ra log:
DBEntity<Data>::DBField<long>::DBField<long>(const string,const string)
long id
DBEntity<Data>::DBField<string>::DBField<string>(const string,const string)
string name
DBEntity<Data>::DBField<datetime>::DBField<datetime>(const string,const string)
datetime timestamp
DBEntity<Data>::DBField<double>::DBField<double>(const string,const string)
double income
sizeof(Data)=36 / ok
sizeof(DataDB)=36 / ok
[,0] [,1] [,2]
[0,] "long" "id" ""
[1,] "string" "name" ""
[2,] "datetime" "timestamp" ""
[3,] "double" "income" ""
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Tuy nhiên, để truy cập các trường, bạn sẽ cần viết một cái gì đó không tiện lợi: data._long_id.f
, data._string_name.f
, data._datetime_timestamp.f
, data._double_income.f
.
Chúng ta sẽ không làm điều này, không chỉ và không hẳn vì sự bất tiện, mà vì cách xây dựng các siêu cấu trúc này không tương thích với các nguyên tắc liên kết dữ liệu với các truy vấn SQL. Trong các phần tiếp theo, chúng ta sẽ khám phá các hàm database
cho phép lấy bản ghi của bảng và kết quả của các truy vấn SQL trong các cấu trúc MQL5. Tuy nhiên, chỉ được phép sử dụng các cấu trúc đơn giản không có kế thừa và các thành viên tĩnh của loại đối tượng. Do đó, cần thay đổi một chút nguyên tắc tiết lộ siêu thông tin.
Chúng ta sẽ phải giữ nguyên các kiểu ban đầu của cấu trúc và thực sự lặp lại mô tả cho cơ sở dữ liệu, đảm bảo không có sự khác biệt (lỗi đánh máy). Điều này không quá tiện lợi, nhưng hiện tại không có cách nào khác.
Chúng ta sẽ chuyển khai báo của các thực thể DBEntity
và DBField
ra ngoài các cấu trúc ứng dụng. Trong trường hợp này, macro DB_FIELD sẽ nhận thêm một tham số (S), trong đó cần truyền kiểu của cấu trúc ứng dụng (trước đây nó được lấy ngầm bằng cách khai báo bên trong chính cấu trúc).
#define DB_FIELD(S,T,N) \
struct S##_##T##_##N: DBEntity<S>::DBField<T> \
{ \
S##_##T##_##N() : DBEntity<S>::DBField<T>(#N) {} \
}; \
const S##_##T##_##N _##S##_##T##_##N;
2
3
4
5
6
Vì các cột bảng có thể có ràng buộc, chúng cũng sẽ cần được truyền vào hàm tạo DBField
nếu cần thiết. Để làm điều này, hãy thêm một vài macro với các tham số phù hợp (về lý thuyết, một cột có thể có nhiều ràng buộc, nhưng thường không quá hai).
#define DB_FIELD_C1(S,T,N,C1) \
struct S##_##T##_##N: DBEntity<S>::DBField<T> \
{ \
S##_##T##_##N() : DBEntity<S>::DBField<T>(#N, C1) {} \
}; \
const S##_##T##_##N _##S##_##T##_##N;
#define DB_FIELD_C2(S,T,N,C1,C2) \
struct S##_##T##_##N: DBEntity<S>::DBField<T> \
{ \
S##_##T##_##N() : DBEntity<S>::DBField<T>(#N, C1 + " " + C2) {} \
}; \
const S##_##T##_##N _##S##_##T##_##N;
2
3
4
5
6
7
8
9
10
11
12
13
Tất cả ba macro, cũng như các phát triển tiếp theo, được thêm vào tệp tiêu đề DBSQLite.mqh
.
Cần lưu ý rằng việc liên kết "tự chế" này của các đối tượng với một bảng chỉ cần thiết khi nhập dữ liệu vào cơ sở dữ liệu, bởi vì việc đọc dữ liệu từ bảng vào một đối tượng đã được triển khai trong MQL5 bằng hàm
DatabaseReadBind
DatabaseReadBind.
Hãy cải thiện việc triển khai của DBField
. Các kiểu MQL5 không hoàn toàn tương ứng với các lớp lưu trữ SQL, do đó cần thực hiện chuyển đổi khi điền vào phần tử prototype[n][0]
. Điều này được thực hiện bởi phương thức tĩnh affinity
.
template<typename T>
struct DBField
{
T f;
DBField(const string name, const string constraints = "")
{
const int n = EXPAND(prototype);
prototype[n][0] = affinity(typename(T));
...
}
static string affinity(const string type)
{
const static string ints[] =
{
"bool", "char", "short", "int", "long",
"uchar", "ushort", "uint", "ulong", "datetime",
"color", "enum"
};
for(int i = 0; i < ArraySize(ints); ++i)
{
if(type == ints[i]) return DB_TYPE::INTEGER;
}
if(type == "float" || type == "double") return DB_TYPE::REAL;
if(type == "string") return DB_TYPE::TEXT;
return DB_TYPE::BLOB;
}
};
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
Các hằng số văn bản của các kiểu chung SQL được sử dụng ở đây được đặt trong một không gian tên riêng: chúng có thể cần thiết ở nhiều nơi khác nhau trong các chương trình MQL vào một thời điểm nào đó, và cần đảm bảo không có xung đột tên.
namespace DB_TYPE
{
const string INTEGER = "INTEGER";
const string REAL = "REAL";
const string TEXT = "TEXT";
const string BLOB = "BLOB";
const string NONE = "NONE";
const string _NULL = "NULL";
}
2
3
4
5
6
7
8
9
Các thiết lập sẵn của các ràng buộc có thể cũng được mô tả trong nhóm của chúng để tiện lợi (như một gợi ý).
namespace DB_CONSTRAINT
{
const string PRIMARY_KEY = "PRIMARY KEY";
const string UNIQUE = "UNIQUE";
const string NOT_NULL = "NOT NULL";
const string CHECK = "CHECK (%s)"; // yêu cầu một biểu thức
const string CURRENT_TIME = "CURRENT_TIME";
const string CURRENT_DATE = "CURRENT_DATE";
const string CURRENT_TIMESTAMP = "CURRENT_TIMESTAMP";
const string AUTOINCREMENT = "AUTOINCREMENT";
const string DEFAULT = "DEFAULT (%s)"; // yêu cầu một biểu thức (hằng số, hàm)
}
2
3
4
5
6
7
8
9
10
11
12
Vì một số ràng buộc yêu cầu tham số (nơi dành sẵn cho chúng được đánh dấu bằng bộ định dạng chuẩn %s
), hãy thêm một kiểm tra sự hiện diện của chúng. Dưới đây là dạng cuối cùng của hàm tạo DBField
.
template<typename T>
struct DBField
{
T f;
DBField(const string name, const string constraints = "")
{
const int n = EXPAND(prototype);
prototype[n][0] = affinity(typename(T));
prototype[n][1] = name;
if(StringLen(constraints) > 0 // tránh lỗi STRING_SMALL_LEN(5035)
&& StringFind(constraints, "%") >= 0)
{
Print("Constraint requires an expression (skipped): ", constraints);
}
else
{
prototype[n][2] = constraints;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Do sự kết hợp của các macro và các đối tượng phụ trợ DBEntity<S>
và DBField<T>
điền vào một mảng các nguyên mẫu, bên trong lớp DBSQlite
, có thể triển khai việc tạo tự động một truy vấn SQL để tạo bảng cấu trúc.
Phương thức createTable
được mẫu hóa với kiểu cấu trúc ứng dụng và chứa một bản phác thảo truy vấn ("CREATE TABLE %s %s (%s);"). Đối số đầu tiên cho nó là chỉ dẫn tùy chọn "IF NOT EXISTS". Tham số thứ hai là tên của bảng, mặc định được lấy là kiểu của tham số mẫu typename(S)
, nhưng có thể được thay thế bằng một thứ khác nếu cần bằng tham số đầu vào tên (nếu nó không phải là NULL). Cuối cùng, đối số thứ ba trong dấu ngoặc là danh sách các cột bảng: nó được hình thành bởi phương thức trợ giúp columns
dựa trên mảng DBEntity <S>::prototype
.
class DBSQLite
{
...
template<typename S>
bool createTable(const string name = NULL,
const bool not_exist = false, const string table_constraints = "") const
{
const static string query = "CREATE TABLE %s %s (%s);";
const string fields = columns<S(table_constraints);
if(fields == NULL)
{
Print("Structure '", typename(S), "' with table fields is not initialized");
SetUserError(4);
return false;
}
// việc cố gắng tạo một bảng đã tồn tại sẽ gây ra lỗi,
// nếu không sử dụng IF NOT EXISTS
const string sql = StringFormat(query,
(not_exist ? "IF NOT EXISTS" : ""),
StringLen(name) ? name : typename(S), fields);
PRTF(sql);
return DatabaseExecute(handle, sql);
}
template<typename S>
string columns(const string table_constraints = "") const
{
static const string continuation = ",\n";
string result = "";
const int n = ArrayRange(DBEntity<S>::prototype, 0);
if(!n) return NULL;
for(int i = 0; i < n; ++i)
{
result += StringFormat("%s%s %s %s",
i > 0 ? continuation : "",
DBEntity<S>::prototype[i][1], DBEntity<S>::prototype[i][0],
DBEntity<S>::prototype[i][2]);
}
if(StringLen(table_constraints))
{
result += continuation + table_constraints;
}
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
36
37
38
39
40
41
42
43
44
45
Đối với mỗi cột, mô tả bao gồm tên, kiểu và một ràng buộc tùy chọn. Ngoài ra, có thể truyền một ràng buộc chung trên bảng (table_constraints
).
Trước khi gửi truy vấn SQL đã tạo tới hàm DatabaseExecute
, phương thức createTable
tạo ra một đầu ra gỡ lỗi của văn bản truy vấn vào nhật ký (tất cả các đầu ra như vậy trong các lớp ORM có thể được tắt tập trung bằng cách thay thế macro PRTF).
Bây giờ mọi thứ đã sẵn sàng để viết một script kiểm tra DBcreateTableFromStruct.mq5
, script này, dựa trên khai báo cấu trúc, sẽ tạo bảng tương ứng trong SQLite. Trong tham số đầu vào, chúng ta chỉ đặt tên của cơ sở dữ liệu, và chương trình sẽ tự chọn tên bảng theo kiểu cấu trúc.
#include <MQL5Book/DBSQLite.mqh>
input string Database = "MQL5Book/DB/Example1";
struct Struct
{
long id;
string name;
double income;
datetime time;
};
DB_FIELD_C1(Struct, long, id, DB_CONSTRAINT::PRIMARY_KEY);
DB_FIELD(Struct, string, name);
DB_FIELD(Struct, double, income);
DB_FIELD(Struct, string, time);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Trong hàm chính OnStart
, chúng ta tạo một bảng bằng cách gọi createTable
với các thiết lập mặc định. Nếu chúng ta không muốn nhận dấu hiệu lỗi khi cố gắng tạo lại nó lần sau, chúng ta cần truyền true
làm tham số đầu tiên (db.createTable<Struct> (true)
).
void OnStart()
{
DBSQLite db(Database);
if(db.isOpen())
{
PRTF(db.createTable<Struct>());
PRTF(db.hasTable(typename(Struct)));
}
}
2
3
4
5
6
7
8
9
Phương thức hasTable
kiểm tra sự hiện diện của một bảng trong cơ sở dữ liệu theo tên bảng. Chúng ta sẽ xem xét việc triển khai phương thức này trong phần tiếp theo. Bây giờ, hãy chạy script. Sau lần chạy đầu tiên, bảng được tạo thành công và bạn có thể thấy truy vấn SQL trong nhật ký (nó được hiển thị với các dấu xuống dòng, như cách chúng ta đã định dạng trong mã).
sql=CREATE TABLE Struct (id INTEGER PRIMARY KEY,
name TEXT ,
income REAL ,
time TEXT ); / ok
db.createTable<Struct>()=true / ok
db.hasTable(typename(Struct))=true / ok
2
3
4
5
6
Lần chạy thứ hai sẽ trả về lỗi từ lời gọi DatabaseExecute
, vì bảng này đã tồn tại, điều này được chỉ ra thêm bởi kết quả của hasTable
.
sql=CREATE TABLE Struct (id INTEGER PRIMARY KEY,
name TEXT ,
income REAL ,
time TEXT ); / ok
database error, table Struct already exists
db.createTable<Struct>()=false / DATABASE_ERROR(5601)
db.hasTable(typename(Struct))=true / ok
2
3
4
5
6
7