c++ 11 是如何簡化你的資料庫訪問介面的
之前寫過一篇文章專門分析了 c++ 模板編譯過程中報的一個錯誤:《fatal error C1045: 編譯器限制 : 連結規範巢狀太深 》,其中涉及到了 qtl —— 一個使用 c++ 11 構建的資料庫訪問庫,當時限於篇幅,沒有深入研究它是如何藉助 c++ 11 來簡化資料庫訪問介面的,本文現在就來探討一下這方面的內容。
沒有 c++ 11 之前,苦逼的程式設計師對於 sql 操作的輸入輸出,只好一行行敲程式碼,例如在呼叫資料庫介面前設定繫結引數;在呼叫成功後,迴圈遍歷查詢的記錄。很多時候資料庫表對應在程式中就是一個結構體,程式設計師需要花費大量的精力將資料庫表字段對應到結構體成員上、或反之,完全沒有體現出來程式設計師應有的價值。而 qtl 這種 c++ 11 庫的出現,可以極大的簡化上面的程式編寫,下面還是用之前文章中提到的例子作為演示,讓大家感受一下:
插入單條資料
1 uint64_t test_insert_single(qtl::sqlite::database &db) 2 { 3 time_t now = time(0); 4 int tmp = rand() % 1000; 5 uint64_t id = db.insert_direct("insert into popbox_msg(msgid, msgtype, appname, uid, status, count, msgbody, stamp) values(?, ?, ?, ?, ?, ?, ?, ?)", 6 std::to_string(tmp), 108, "GDraw", "1923374929399", 1, 0, "this is msgbody", now); 7 8 printf("insert record with msgid %d return %d\n", tmp, (int)id); 9 return id; 10 }
插入操作需要輸入資料,將資料編入 sql 是一種思路,但更好的方法是使用佔位符 (?) 和資料繫結 (binding) 來防止 sql 注入問題,而這會給介面帶來不定數量的輸入引數,幸好 c++ 11 的可變模板引數特性允許使用者提供不限數量與型別的輸入資料,是不是很方便?下面是 qtl 提供的插入單條資料介面:
1 uint64_t qtl::base_database<T, Command>::insert<Params>(const std::string & query_text, const Params & params); 2 uint64_t qtl::base_database<T, Command>::insert<Params>(const char * query_text, const Params & params); 3 uint64_t qtl::base_database<T, Command>::insert<Params>(const char * query_text, size_t text_length, const Params & params); 4 5 uint64_t qtl::base_database<T, Command>::insert_direct<...Params>(const std::string & query_text, const Params & ...params); 6 uint64_t qtl::base_database<T, Command>::insert_direct<...Params>(const char * query_text, const Params & ...params); 7 uint64_t qtl::base_database<T, Command>::insert_direct<...Params>(const char * query_text, size_t text_length, const Params & ...params);
其中主要分兩組:insert 與 insert_direct,前者只提供一個輸入繫結引數,後者可以提供多個。而且這些介面會很貼心的將新插入記錄的 rowid 返回,方便後續操作這條記錄。
更新單條資料
1 void test_update_single(qtl::sqlite::database &db, uint64_t rowid) 2 { 3 time_t now = time(0); 4 uint64_t affected = 0; 5 db.execute_direct("update popbox_msg set status=?, count=?, stamp=? where rowid=?", &affected, 0, 3, now, (int)rowid); 6 printf("update record with rowid %d affected %d records\n", (int)rowid, (int)affected); 7 }
更新操作和插入操作類似,輸入資料是必不可少的,但它有時也需要更新符合條件的記錄,而這會帶來另一坨不定數量的輸入引數,不過好在二者都是輸入引數,可以合二為一使用一個維度的可變模板引數,依次將更新引數與條件引數羅列在 qtl 介面提供的引數列表中即可:
1 void qtl::base_database<T, Command>::execute<Params>(const std::string & query_text, const Params & params, uint64_t * affected = NULL); 2 void qtl::base_database<T, Command>::execute<Params>(const char * query_text, const Params & params, uint64_t * affected = NULL); 3 void qtl::base_database<T, Command>::execute<Params>(const char * query_text, size_t text_length, const Params & params, uint64_t * affected = NULL); 4 5 void qtl::base_database<T, Command>::execute_direct<...Params>(const std::string & query_text, uint64_t * affected, const Params & ...params); 6 void qtl::base_database<T, Command>::execute_direct<...Params>(const char * query_text, uint64_t * affected, const Params & ...params); 7 void qtl::base_database<T, Command>::execute_direct<...Params>(const char * query_text, size_t text_length, uint64_t * affected, const Params & ...params);
主要也是兩組介面:execute 與 execute_direct,前者只提供一個輸入繫結引數,後者可以提供多個。由於是插入多條資料,這裡沒有辦法返回某一條記錄的 rowid,代之以的是更新的行數 affected,如果這個引數為空,則不返回。
插入多條資料
void test_insert_multi(qtl::sqlite::database &db) { uint64_t affected = 0; int tmp[3] = { 0 }; for (int i=0; i<3; ++i) tmp[i] = rand() % 1000; auto stmt = db.open_command("insert into popbox_msg(msgid, msgtype, appname, uid, status, count, msgbody, stamp) " "values(?, 108, 'GDraw', '1923374929399', 1, 0, 'this is msgbody', strftime('%s','now'))"); qtl::execute(stmt, &affected, std::to_string(tmp[0]), std::to_string(tmp[1]), std::to_string(tmp[2])); printf("insert %d record\n", (int)affected); }
插入多條資料時,可變模板引數列表的每一個引數表示一個輸入繫結引數、針對一條新記錄,這樣一來就不太夠用了。例如上面這個例子中,相當於插入了三條不同的 popbox_msg 記錄,每個輸入引數對應記錄的 msgid 欄位,如果一條記錄有多個欄位需要輸入就不適用了,那種場景下就需要寫個迴圈多次呼叫插入單條資料的操作了(其實插入多條的介面底層也是遞迴為插入單條來執行的,所以這樣做效能沒有太大損失)。
更新多條資料
1 void test_update_multi(qtl::sqlite::database &db) 2 { 3 uint64_t affected = 0; 4 int id[3] = { 19, 20, 21 }; 5 6 auto stmt = db.open_command("update popbox_msg set status=0, count=2, stamp=strftime('%s','now') where rowid=? "); 7 qtl::execute(stmt, &affected, id[0], id[1], id[2]); 8 printf("update %d record\n", (int)affected); 9 }
其實和插入多條資料非常相似,每條記錄只能允許一個輸入繫結引數。
刪除資料
1 void test_delete(qtl::sqlite::database &db) 2 { 3 uint64_t affected = 0; 4 db.execute_direct("delete from popbox_msg where msgtype=? and appname=? and uid=?", &affected, 108, "GDraw", "1923374929399"); 5 printf("delete record affected %d rows\n", (int)affected); 6 }
刪除資料時由於只需要提供刪除條件的輸入繫結引數,而實際結果可能刪除一條、也可能刪除多條,所以不在數量上做區分。這裡使用的是和更新資料一樣的介面:execute 和 execute_direct,同樣的,前者只能允許一個輸入繫結引數,適合較簡單的 sql 語句;後者可以允許多個輸入繫結引數,適合較複雜的 sql。最後,刪除的行數由 affected 引數返回給呼叫者。
查詢單條資料
1 void test_query_single(qtl::sqlite::database &db, uint64_t rowid) 2 { 3 std::string msg; 4 db.query_first("select msgbody from popbox_msg where rowid=?", (int)rowid, msg); 5 printf("row %d: %s\n", (int)rowid, msg.c_str()); 6 }
查詢單條資料時可以直接將查詢到的資料以輸出引數方式回傳,而查詢條件往往又需要輸入繫結引數,那 qtl 是如何區分可變模板引數列表中哪些是入參、哪些是出參呢?答案是區分不了。因此在介面設計上,qtl 的查詢單條資料介面最多允許一個入參:
1 void qtl::base_database<T, Command>::query_first<Values>(const std::string & query_text, Values && values); 2 void qtl::base_database<T, Command>::query_first<Values>(const char * query_text, Values && values); 3 void qtl::base_database<T, Command>::query_first<Values>(const char * query_text, size_t text_length, Values && values); 4 5 void qtl::base_database<T, Command>::query_first<Params, Values>(const std::string & query_text, const Params & params, Values && values); 6 void qtl::base_database<T, Command>::query_first<Params, Values>(const char * query_text, const Params & params, Values && values); 7 void qtl::base_database<T, Command>::query_first<Params, Values>(const char * query_text, size_t text_length, const Params & params, Values && values);
主要分為兩組:只帶一個出參的 query_first;帶一個出參和一個入參的 query_first。這個介面只針對特別簡單的 sql 語句,如果想要返回一條記錄的多個欄位時,就必需使用另一組介面:query_first_direct
1 void qtl::base_database<T, Command>::query_first_direct<...Values>(const std::string & query_text, Values & ...values); 2 void qtl::base_database<T, Command>::query_first_direct<...Values>(const char * query_text, Values & ...values); 3 void qtl::base_database<T, Command>::query_first_direct<...Values>(const char * query_text, size_t text_length, Values & ...values);
遺憾的是這個介面雖然能提供多個出參,卻無法提供任何入參,所有入參必需事先構建在 sql 語句中,這十分不優雅,但沒有辦法。下面是使用的例子:
1 void test_query_single_ex(qtl::sqlite::database &db, uint64_t rowid) 2 { 3 time_t stamp = 0; 4 int status = 0, count = 0; 5 6 std::ostringstream oss; 7 oss << "select status, count, stamp from popbox_msg where rowid=" << rowid; 8 db.query_first_direct(oss.str (), status, count, stamp); 9 printf("row %d: status %d, count %d, stamp %d\n", (int)rowid, status, count, (int)stamp); 10 }
從這個實際例子看,以後 c++ 可變模板引數列表可能需要支援兩個引數列,一列是輸入引數,一列是輸出引數了。但是轉念一想,這樣好像也不對,因為出參與入參在呼叫點並無任何區別,編譯器如何知道哪個是出參哪個是入參呢?所以這個問題可能還真是無解了。
查詢多條資料
1 void test_query_multi(qtl::sqlite::database &db) 2 { 3 int cnt = 0; 4 db.query("select status, count, stamp from popbox_msg where appname=?", "GDraw", 5 [&cnt](int status, int count, time_t stamp){ 6 printf("%d, %d, %d\n", status, count, (int)stamp); 7 cnt++; 8 }); 9 10 printf("query %d records\n", cnt); 11 }
因為可能返回多條資料,這裡使用回撥函式(一般為 lambda 表示式)來接收讀取的記錄。回撥函式引數列表必需與 select 選擇的資料庫表列相匹配。
1 void qtl::base_database<T, Command>::query<ValueProc>(const std::string & query_text, ValueProc && proc); 2 void qtl::base_database<T, Command>::query<ValueProc>(const char * query_text, ValueProc && proc); 3 void qtl::base_database<T, Command>::query<ValueProc>(const char * query_text, size_t text_length, ValueProc && proc); 4 5 void qtl::base_database<T, Command>::query<Params, ValueProc>(const std::string & query_text, const Params & params, ValueProc && proc); 6 void qtl::base_database<T, Command>::query<Params, ValueProc>(const char * query_text, const Params & params, ValueProc && proc); 7 void qtl::base_database<T, Command>::query<Params, ValueProc>(const char * query_text, size_t text_length, const Params & params, ValueProc && proc);
query 介面分為兩組:一組只提供一個回撥函式用於接收資料;另一組還提供一個額外的輸入繫結引數。對於複雜的 sql 查詢,這個還是不太夠用,我不清楚為什麼不能在 ValueProc proc 引數後面加一個可變模板引數列表,這樣就不可以接收多個輸入繫結引數了麼?此處存疑。不過這個好歹比 query_first 要麼只返回一個欄位、要麼返回多個欄位但不接收輸入引數要強一點。除了優點,這個介面也有一個不惹人注意的 bug,請看下面這段程式碼:
1 void test_query_multi(qtl::sqlite::database &db) 2 { 3 int cnt = 0; 4 db.query("select msgid, msgtype, appname, uid, status, count, msgbody, stamp from popbox_msg where appname=?", "GDraw", 5 [&cnt](std::string const& msgid, int msgtype, std::string const& appname, std::string const& uid, int status, int count, std::string const& msgbody, time_t stamp){ 6 printf("%s, %d, %s, %s, %d, %d, %s, %d\n", msgid.c_str(), msgtype, appname.c_str(), uid.c_str(), status, count, msgbody.c_str(), (int)stamp); 7 cnt++; 8 }); 9 10 printf("query %d records\n", cnt); 11 }
我增加了從資料庫表中選取的欄位,並相應的增加了 lambda 表示式的引數列表,當數量達到一個閾值時(親測為8),VS2013 編譯器將報錯退出:
e:\code\qtl\include\qtl\apply_tuple.h(17): fatal error C1045: 編譯器限制 : 連結規範巢狀太深
具體分析請參考我的另一篇文章:《fatal error C1045: 編譯器限制 : 連結規範巢狀太深》。這裡我著重想說明的是,使用這種方式傳遞的欄位在某些編譯器上是有上限的,所以可移植性不太好。相信聰明的你已經猜到了,由於 query_first_direct 使用了和 query 相同的底層機制,query_first_direct 在 VS2013 上也存在相同的問題。幸好 qtl 還有另外一種方法,可以解決上面的問題,這就是結構體成員繫結:
1 class popbox_msg_t 2 { 3 public: 4 void dump(char const* prompt) const; 5 6 int msgtype = 0; // 108 or 402 7 int status = 0; // send to server result, (1:ok; 0:fail) 8 int count = 0; // retry times, if exceed POPBOX_MSG_RETRY_MAX, stop retry 9 time_t stamp = 0; // receive time 10 std::string msgid; 11 std::string msgbody; 12 std::string appname; 13 std::string uid; 14 }; 15 16 17 void popbox_msg_t::dump(char const* prompt) const 18 { 19 tm* t = localtime(&stamp); 20 printf("%s : %s,%s,%s,%d,%d,%d, %04d-%02d-%02d %02d:%02d:%02d, %s\n", 21 prompt, 22 appname.c_str(), 23 uid.c_str(), 24 msgid.c_str(), 25 msgtype, 26 status, 27 count, 28 t->tm_year + 1900, 29 t->tm_mon + 1, 30 t->tm_mday + 1, 31 t->tm_hour, 32 t->tm_min, 33 t->tm_sec, 34 msgbody.c_str()); 35 } 36 37 namespace qtl 38 { 39 template<> 40 inline void bind_record<qtl::sqlite::statement, popbox_msg_t>(qtl::sqlite::statement& command, popbox_msg_t&& v) 41 { 42 int n = 0; 43 qtl::bind_field(command, n++, v.msgid); 44 qtl::bind_field(command, n++, v.msgtype); 45 qtl::bind_field(command, n++, v.appname); 46 qtl::bind_field(command, n++, v.uid); 47 qtl::bind_field(command, n++, v.status); 48 qtl::bind_field(command, n++, v.count); 49 qtl::bind_field(command, n++, v.msgbody); 50 qtl::bind_field(command, n++, v.stamp); 51 } 52 } 53 54 void test_query_multi_ex(qtl::sqlite::database &db) 55 { 56 int cnt = 0; 57 db.query("select msgid, msgtype, appname, uid, status, count, msgbody, stamp from popbox_msg where appname=?", "GDraw", 58 [&cnt](popbox_msg_t const& pm){ 59 pm.dump("msg"); 60 cnt++; 61 }); 62 63 printf("query %d records\n", cnt); 64 }
同樣是 query 介面,同樣是 lambda 表示式作為回撥函式,不同的是,我提前聲明瞭一個結構體 popbox_msg_t,並提供了 qtl::bind_record 模板函式的一個特化、來將資料庫表的列與結構體成員二者關聯起來,這樣我的 lambda 表示式只要接收結構體就夠了,qtl 在底層會自動根據 bind_record 將讀取的資料初始化到結構體中供我們使用。因為這種方式避免了羅列各個輸出引數,所以可以很好的避免上述問題。 另外關於 bind_record 補充一點,最新版本的 qtl 可以在 bind_record 模板特化中使用一個 bind_fields 來指定所有成員的對應關係了(我使用的舊版沒有這個介面),類似於這樣:
qtl::bind_fields(command, v.msgid, v.msgtype, v.appname, v.uid, v.status, v.count, v.msgbody, v.stamp);
是不是更簡單了呢?有了結構體繫結,還可以玩出許多花樣,例如直接用結構體的成員函式來代替 lambda 表示式:
1 class popbox_msg_t 2 { 3 public: 4 void dump(char const* prompt) const; 5 void print(); 6 7 int msgtype = 0; // 108 or 402 8 int status = 0; // send to server result, (1:ok; 0:fail) 9 int count = 0; // retry times, if exceed POPBOX_MSG_RETRY_MAX, stop retry 10 time_t stamp = 0; // receive time 11 std::string msgid; 12 std::string msgbody; 13 std::string appname; 14 std::string uid; 15 }; 16 17 18 void popbox_msg_t::dump(char const* prompt) const 19 { 20 tm* t = localtime(&stamp); 21 printf("%s : %s,%s,%s,%d,%d,%d, %04d-%02d-%02d %02d:%02d:%02d, %s\n", 22 prompt, 23 appname.c_str(), 24 uid.c_str(), 25 msgid.c_str(), 26 msgtype, 27 status, 28 count, 29 t->tm_year + 1900, 30 t->tm_mon + 1, 31 t->tm_mday + 1, 32 t->tm_hour, 33 t->tm_min, 34 t->tm_sec, 35 msgbody.c_str()); 36 } 37 38 void popbox_msg_t::print () 39 { 40 dump("msg"); 41 } 42 43 namespace qtl 44 { 45 template<> 46 inline void bind_record<qtl::sqlite::statement, popbox_msg_t>(qtl::sqlite::statement& command, popbox_msg_t&& v) 47 { 48 int n = 0; 49 qtl::bind_field(command, n++, v.msgid); 50 qtl::bind_field(command, n++, v.msgtype); 51 qtl::bind_field(command, n++, v.appname); 52 qtl::bind_field(command, n++, v.uid); 53 qtl::bind_field(command, n++, v.status); 54 qtl::bind_field(command, n++, v.count); 55 qtl::bind_field(command, n++, v.msgbody); 56 qtl::bind_field(command, n++, v.stamp); 57 } 58 } 59 60 void test_query_multi_ex(qtl::sqlite::database &db) 61 { 62 int cnt = 0; 63 db.query("select msgid, msgtype, appname, uid, status, count, msgbody, stamp from popbox_msg where appname=?", "GDraw", 64 &popbox_msg_t::print); 65 66 printf("query %d records\n", cnt); 67 }
程式碼高亮的部分就是兩個版本的差異,這裡使用了 popbox_msg_t 的一個成員函式 print 來充當 lambda 表示式的作用,這樣做可以將程式碼集中到結構體中進行維護。不過缺點也是明顯的,就是不能自由的選取外部輸入引數了,例如對遍歷記錄數的統計,在新版本中就沒辦法做到了。除了上面的方式,還有一種新花樣:
1 void test_query_multi_ul(qtl::sqlite::database &db) 2 { 3 int cnt = 0; 4 for(auto& pm : db.result<popbox_msg_t>("select msgid, msgtype, appname, uid, status, count, msgbody, stamp from popbox_msg where appname=?", "GDraw")) 5 { 6 pm.dump("msg"); 7 cnt++; 8 } 9 10 printf("query %d records\n", cnt); 11 }
這種方式已經脫離了 query 介面,使用的是 result 介面,雖然殼變了,但是底層機制和 query 是一致的,都是通過 bind_record 將查詢到的資料填充到結構體中,下面是 result 的介面定義:
1 query_result<Command,Record> qtl::base_database<T, Command>::result<Record>(const std::string & query_text); 2 query_result<Command,Record> qtl::base_database<T, Command>::result<Record>(const char * query_text); 3 query_result<Command,Record> qtl::base_database<T, Command>::result<Record>(const char * query_text, size_t text_length); 4 5 query_result<Command,Record> qtl::base_database<T, Command>::result<Record, Params>(const std::string & query_text, const Params & params); 6 query_result<Command,Record> qtl::base_database<T, Command>::result<Record, Params>(const char * query_text, const Params & params); 7 query_result<Command,Record> qtl::base_database<T, Command>::result<Record, Params>(const char * query_text, size_t text_length, const Params & params);
主要分為兩組:一組只接收 sql 輸入;另一組還可以接收一個額外的輸入繫結引數。除了返回型別,與 query 介面幾乎一模一樣,可以理解成是將 query 的回撥函式轉化成了 result 返回的 query_result 集合。像上面例子那樣寫程式碼,幾乎找到了之前 c 語言操作資料庫的感覺,特別是不用把需要的外部變數在 lambda 表示式裡一一捕獲了,在迴圈裡就可以直接用它們,就是一個字:爽!
如果有多個操作都從一個表中查詢,可能只是選取的欄位不同,那麼這種情況下一個結構體就不夠了,必需為每個查詢定義一個獨一無二的結構體並提供相應的 bind_record 函式(即使這些結構體擁有近似的成員)。這樣簡直是重複造輪子,難道不能定義一個包含所有欄位的“超集”結構體,讓它來包打所有這個表的查詢嗎?有的人可能會想,你把 sql 語句改造一下,每次選取所有欄位、多餘的不要用就好了呀!但是這樣肯定不是一個優雅的解決方案,qtl 最新版本中包含了關於這方面的解決方案,那就是自定義繫結,請看下面這個例子:
1 void my_bind(popbox_msg_t&& v, qtl::sqlite::statement& command) 2 { 3 int n = 0; 4 qtl::bind_field(command, n++, v.status); 5 qtl::bind_field(command, n++, v.count); 6 qtl::bind_field(command, n++, v.stamp); 7 } 8 9 void test_query_multi_custom(qtl::sqlite::database &db) 10 { 11 int cnt = 0; 12 db.query_explicit("select status, count, stamp from popbox_msg where appname=?", "GDraw", 13 qtl::custom_bind(popbox_msg_t(), my_bind), 14 [&cnt](popbox_msg_t const& pm){ 15 printf("msg: %d, %d, %d\n", pm.status, pm.count, pm.stamp); 16 cnt++; 17 }); 18 19 printf("query %d records\n", cnt); 20 }
這個例子可以和前面的 popbox_msg_t 定義及其預設 bind_record 函式放在一起,由於這裡我們使用 query_explicit 介面明確指定了使用的繫結函式是 my_bind,之前定義的預設繫結函式就不再起作用啦。這個查詢只要表中的三個欄位,因此在查詢結束後也只有三個欄位可用。我在下載了最新版本的 qtl 並嘗試編譯這程式碼時,編譯器報錯說沒有找到 custom_bind 的定義,我全文搜尋了一下也確實沒有,但是這個例子可是我照著官網寫的啊,難不成作者後來修改了程式碼忘記同步文件了嗎?不得而知。
最後,對於資料庫應用來說,檢視 (view) 和過程 (procedure) 也是資料庫經常接觸到的概念,有的資料庫過程會呼叫多個 select 語句查詢結果,此時我們的介面又該怎麼接收這些資料呢?答案就是 query_multi 和 query_multi_with_params,它們允許使用者提供多個回撥函式,一般就是寫多個 lambda 表示式啦,這樣就可以按過程中呼叫 select 語句的順序來接收對應的查詢結果了:
1 void qtl::base_database<T, Command>::query_multi<...ValueProc>(const std::string & query_text, ValueProc && ...proc); 2 void qtl::base_database<T, Command>::query_multi<...ValueProc>(const char * query_text, ValueProc && ...proc); 3 void qtl::base_database<T, Command>::query_multi<...ValueProc>(const char * query_text, size_t text_length, ValueProc && ...proc); 4 5 void qtl::base_database<T, Command>::query_multi_with_params<Params, ...ValueProc>(const std::string & query_text, const Params & params, ValueProc && ...proc); 6 void qtl::base_database<T, Command>::query_multi_with_params<Params, ...ValueProc>(const char * query_text, const Params & params, ValueProc && ...proc); 7 void qtl::base_database<T, Command>::query_multi_with_params<Params, ...ValueProc>(const char * query_text, size_t text_length, const Params & params, ValueProc && ...proc);
query_multi_with_params 顧名思義,就是在 query_multi 的基礎上,允許一個額外的輸入繫結引數。當然這個功能比較偏門,我沒有專門寫 demo 去驗證。
下載
本文所有測試用例都是基於獲取並開啟 qtl::sqlite::database 物件的基礎,那麼這個物件又是如何開啟的呢,請看下面框架:
1 int main(int argc, char* argv[]) 2 { 3 int ret = 0; 4 srand(time(0)); 5 uint64_t rowid = 0; 6 qtl::sqlite::database db(SQLITE_TIMEOUT); 7 8 try 9 { 10 // copy of gcm.db, and create following table: 11 // create table popmsg (msgid text not null, msgtype integer not null, cid text not null, uid text not null, 12 // status integer not null, count integer not null, msgbody text not null, stamp timestamp not null, 13 // primary key (msgid, msgtype, cid, uid)); 14 db.open("../data/gcm.db", NULL); 15 printf("open db OK\n"); 16 17 #if 0 18 rowid = test_insert_single(db); 19 #endif 20 21 #if 0 22 test_update_single(db, rowid); 23 #endif 24 25 #if 0 26 test_insert_multi(db); 27 #endif 28 29 #if 0 30 test_update_multi(db); 31 #endif 32 33 #if 0 34 test_delete(db); 35 #endif 36 37 #if 0 38 test_query_single(db, rowid); 39 #endif 40 41 #if 0 42 test_query_single_ex(db, rowid); 43 #endif 44 45 #if 0 46 test_query_multi(db); 47 #endif 48 49 #if 0 50 test_query_multi_ex(db); 51 #endif 52 53 #if 0 54 test_query_multi_ul(db); 55 #endif 56 57 //test_query_multi_custom(db); 58 59 db.close(); 60 } 61 catch (qtl::sqlite::error &e) 62 { 63 printf("manipute db error %d: %s\n", e.code(), e.what()); 64 db.close(); 65 return -1; 66 } 67 68 return 0; 69 }
可以看到資料庫的開啟、關閉過程。因為 qtl 檢測到底層資料庫錯誤時,是通過丟擲異常的方式來向上層報告的,所以所有用例都包含在 try_catch 結構中。可以通過編譯開關來開啟各個用例,多個用例之間可以組合起來使用,例如同時開啟 test_insert_single 和 test_query_single 兩個用例。所有相關的內容,包括 qtl、sqlite 標頭檔案;sqlite lib 與 dll 和 so;sqlite 樣例資料 db 檔案;甚至編譯好的可執行檔案(Win10 x64 與 Linux x64),我都打包上傳到部落格園了,可以點選 這裡下載。
qtl 庫最新版本不包含在裡面 ,可以從這裡獲取:https://github.com/goodpaperman/qtl
結語
本文並不是 qtl 的使用指南,qtl 的許多內容(事務、語句物件、blob 型別、非同步IO、indicator)都沒有介紹。這裡只是使用 qtl 這個典型的 c++11 庫、以及資料庫的“增刪改查”四大操作、來說明新技術是如何"顛覆"使用者呼叫介面的,以及在一些特定場景下(例如 query_first 既要不定輸入引數,也要不定輸出引數), c++ 新特性是否有可能去滿足這種需求。從這裡也能看出,c++ 的新需求新特性並不是憑空衍生的,而是從類似 qtl 這種模板庫的實際需要產生的(如何寫出使用者呼叫更方便的介面),如果我們離開這些場景去學 c++ 新特性,會感到知識點紛繁複雜,而例子又全然不貼切,完全感覺不到新特性解決的痛點。當然 qtl 也不是盡善盡美,例如在使用回撥函式處理輸出資料的情況下,能不能給輸入資料來個“不限量”引數列表?qtl 沒有這樣做是 c++ 不支援,還是 qtl 懶沒有做到這一步,這就暫時不得而知了。
&n