前序:
這里要注明,我是一個跨平臺專注者,并不喜歡只用 windows 平臺。我以前的工作就是為 unix 平臺寫代碼。下面我所寫的東西,雖然沒有驗證,但是我已盡量不使用任何 windows 的東西,只使用標準 C 或標準C++。但是,我沒有嘗試過在別的系統、別的編譯器下編譯,因此下面的敘述如果不正確,則留待以后修改。
下面我的代碼仍然用 VC 編寫,因為我覺得VC是一個很不錯的IDE,可以加快代碼編寫速度(例如配合 Vassist )。下面我所說的編譯環境,是VC2003。如果讀者覺得自己習慣于 unix 下用 vi 編寫代碼速度較快,可以不用管我的說明,只需要符合自己習慣即可,因為我用的是標準 C 或 C++ 。不會給任何人帶來不便。
一、版本
從 www.sqlite.org 網站可下載到最新的 sqlite 代碼和編譯版本。我寫此文章時,最新代碼是 3.3.17 版本。
很久沒有去下載 sqlite 新代碼,因此也不知道 sqlite 變化這么大。以前很多文件,現在全部合并成一個 sqlite3.c 文件。如果單獨用此文件,是挺好的,省去拷貝一堆文件還擔心有沒有遺漏。但是也帶來一個問題:此文件太大,快接近7萬行代碼,VC開它整個機器都慢下來了。如果不需要改它代碼,也就不需要打開 sqlite3.c 文件,機器不會慢。但是,下面我要寫通過修改 sqlite 代碼完成加密功能,那時候就比較痛苦了。如果個人水平較高,建議用些簡單的編輯器來編輯,例如UltraEdit 或 Notepad 。速度會快很多。
二、基本編譯
這個不想多說了,在 VC 里新建 dos 控制臺空白工程,把 sqlite3.c 和 sqlite3.h 添加到工程,再新建一個 main.cpp文件。在里面寫:
1
2
3
4
5
6
7
8
|
extern "C" { #include "./sqlite3.h" }; int main( int , char ** ) { return 0; } |
為什么要 extern “C” ?如果問這個問題,我不想說太多,這是C++的基礎。要在 C++ 里使用一段 C 的代碼,必須要用 extern “C” 括起來。C++跟 C雖然語法上有重疊,但是它們是兩個不同的東西,內存里的布局是完全不同的,在C++編譯器里不用extern “C”括起C代碼,會導致編譯器不知道該如何為 C 代碼描述內存布局。
可能在 sqlite3.c 里人家已經把整段代碼都 extern “C” 括起來了,但是你遇到一個 .c 文件就自覺的再括一次,也沒什么不好。
基本工程就這樣建立起來了。編譯,可以通過。但是有一堆的 warning。可以不管它。
三、SQLITE操作入門
sqlite提供的是一些C函數接口,你可以用這些函數操作數據庫。通過使用這些接口,傳遞一些標準 sql 語句(以 char * 類型)給 sqlite 函數,sqlite 就會為你操作數據庫。
sqlite 跟MS的access一樣是文件型數據庫,就是說,一個數據庫就是一個文件,此數據庫里可以建立很多的表,可以建立索引、觸發器等等,但是,它實際上得到的就是一個文件。備份這個文件就備份了整個數據庫。
sqlite 不需要任何數據庫引擎,這意味著如果你需要 sqlite 來保存一些用戶數據,甚至都不需要安裝數據庫(如果你做個小軟件還要求人家必須裝了sqlserver 才能運行,那也太黑心了)。
下面開始介紹數據庫基本操作。
1 基本流程(1)關鍵數據結構
sqlite 里最常用到的是 sqlite3 * 類型。從數據庫打開開始,sqlite就要為這個類型準備好內存,直到數據庫關閉,整個過程都需要用到這個類型。當數據庫打開時開始,這個類型的變量就代表了你要操作的數據庫。下面再詳細介紹。
(2)打開數據庫
int sqlite3_open( 文件名, sqlite3 ** );
用這個函數開始數據庫操作。
需要傳入兩個參數,一是數據庫文件名,比如:c://DongChunGuang_Database.db。
文件名不需要一定存在,如果此文件不存在,sqlite 會自動建立它。如果它存在,就嘗試把它當數據庫文件來打開。
sqlite3 ** 參數即前面提到的關鍵數據結構。這個結構底層細節如何,你不要關它。
函數返回值表示操作是否正確,如果是 SQLITE_OK 則表示操作正常。相關的返回值sqlite定義了一些宏。具體這些宏的含義可以參考 sqlite3.h 文件。里面有詳細定義(順便說一下,sqlite3 的代碼注釋率自稱是非常高的,實際上也的確很高。只要你會看英文,sqlite 可以讓你學到不少東西)。
下面介紹關閉數據庫后,再給一段參考代碼。
(3)關閉數據庫
int sqlite3_close(sqlite3 *);
前面如果用 sqlite3_open 開啟了一個數據庫,結尾時不要忘了用這個函數關閉數據庫。
下面給段簡單的代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
extern "C" { #include "./sqlite3.h" }; int main( int , char ** ) { sqlite3 * db = NULL; //聲明sqlite關鍵結構指針 int result; //打開數據庫 //需要傳入 db 這個指針的指針,因為 sqlite3_open 函數要為這個指針分配內存,還要讓db指針指向這個內存區 result = sqlite3_open( “c: //Dcg_database.db”, &db ); if ( result != SQLITE_OK ) { //數據庫打開失敗 return -1; } //數據庫操作代碼 //… //數據庫打開成功 //關閉數據庫 sqlite3_close( db ); return 0; } |
這就是一次數據庫操作過程。
2 SQL語句操作
本節介紹如何用sqlite 執行標準 sql 語法。
(1)執行sql語句
int sqlite3_exec(sqlite3*, const char *sql, sqlite3_callback, void *, char **errmsg );
這就是執行一條 sql 語句的函數。
第1個參數不再說了,是前面open函數得到的指針。說了是關鍵數據結構。
第2個參數const char *sql 是一條 sql 語句,以/0結尾。
第3個參數sqlite3_callback 是回調,當這條語句執行之后,sqlite3會去調用你提供的這個函數。(什么是回調函數,自己找別的資料學習)
第4個參數void * 是你所提供的指針,你可以傳遞任何一個指針參數到這里,這個參數最終會傳到回調函數里面,如果不需要傳遞指針給回調函數,可以填NULL。等下我們再看回調函數的寫法,以及這個參數的使用。
第5個參數char ** errmsg 是錯誤信息。注意是指針的指針。sqlite3里面有很多固定的錯誤信息。執行 sqlite3_exec 之后,執行失敗時可以查閱這個指針(直接 printf(“%s/n”,errmsg))得到一串字符串信息,這串信息告訴你錯在什么地方。sqlite3_exec函數通過修改你傳入的指針的指針,把你提供的指針指向錯誤提示信息,這樣sqlite3_exec函數外面就可以通過這個 char*得到具體錯誤提示。
說明:通常,sqlite3_callback 和它后面的 void * 這兩個位置都可以填 NULL。填NULL表示你不需要回調。比如你做insert 操作,做 delete 操作,就沒有必要使用回調。而當你做 select 時,就要使用回調,因為 sqlite3 把數據查出來,得通過回調告訴你查出了什么數據。
(2)exec 的回調
typedef int (*sqlite3_callback)(void*,int,char**, char**);
你的回調函數必須定義成上面這個函數的類型。下面給個簡單的例子:
1
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
56
57
58
59
|
//sqlite3的回調函數 // sqlite 每查到一條記錄,就調用一次這個回調 int LoadMyInfo( void * para, int n_column, char ** column_value, char ** column_name ) { //para是你在 sqlite3_exec 里傳入的 void * 參數 //通過para參數,你可以傳入一些特殊的指針(比如類指針、結構指針),然后在這里面強制轉換成對應的類型(這里面是void*類型,必須強制轉換成你的類型才可用)。然后操作這些數據 //n_column是這一條記錄有多少個字段 (即這條記錄有多少列) // char ** column_value 是個關鍵值,查出來的數據都保存在這里,它實際上是個1維數組(不要以為是2維數組),每一個元素都是一個 char * 值,是一個字段內容(用字符串來表示,以/0結尾) //char ** column_name 跟 column_value是對應的,表示這個字段的字段名稱 //這里,我不使用 para 參數。忽略它的存在. int i; printf( “記錄包含 %d 個字段/n”, n_column ); for( i = 0 ; i < n_column; i ++ ) { printf( “字段名:%s ß> 字段值:%s/n”, column_name[i], column_value[i] ); } printf( “------------------/n“ ); return 0; } int main( int , char ** ) { sqlite3 * db; int result; char * errmsg = NULL; result = sqlite3_open( “c://Dcg_database.db”, &db ); if( result != SQLITE_OK ) { //數據庫打開失敗 return -1; } //數據庫操作代碼 //創建一個測試表,表名叫 MyTable_1,有2個字段: ID 和 name。其中ID是一個自動增加的類型,以后insert時可以不去指定這個字段,它會自己從0開始增加 result = sqlite3_exec( db, “create table MyTable_1( ID integer primary key autoincrement, name nvarchar(32) )”, NULL, NULL, errmsg ); if(result != SQLITE_OK ) { printf( “創建表失敗,錯誤碼:%d,錯誤原因:%s/n”, result, errmsg ); } //插入一些記錄 result = sqlite3_exec( db, “insert into MyTable_1( name ) values ( ‘走路' )”, 0, 0, errmsg ); if(result != SQLITE_OK ) { printf( “插入記錄失敗,錯誤碼:%d,錯誤原因:%s/n”, result, errmsg ); } result = sqlite3_exec( db, “insert into MyTable_1( name ) values ( ‘騎單車' )”, 0, 0, errmsg ); if(result != SQLITE_OK ) { printf( “插入記錄失敗,錯誤碼:%d,錯誤原因:%s/n”, result, errmsg ); } result = sqlite3_exec( db, “insert into MyTable_1( name ) values ( ‘坐汽車' )”, 0, 0, errmsg ); if(result != SQLITE_OK ) { printf( “插入記錄失敗,錯誤碼:%d,錯誤原因:%s/n”, result, errmsg ); } //開始查詢數據庫 result = sqlite3_exec( db, “select * from MyTable_1”, LoadMyInfo, NULL, errmsg ); //關閉數據庫 sqlite3_close( db ); return 0; } |
通過上面的例子,應該可以知道如何打開一個數據庫,如何做數據庫基本操作。
有這些知識,基本上可以應付很多數據庫操作了。
(3)不使用回調查詢數據庫
上面介紹的 sqlite3_exec 是使用回調來執行 select 操作。還有一個方法可以直接查詢而不需要回調。但是,我個人感覺還是回調好,因為代碼可以更加整齊,只不過用回調很麻煩,你得聲明一個函數,如果這個函數是類成員函數,你還不得不把它聲明成 static 的(要問為什么?這又是C++基礎了。C++成員函數實際上隱藏了一個參數:this,C++調用類的成員函數的時候,隱含把類指針當成函數的第一個參數傳遞進去。結果,這造成跟前面說的 sqlite 回調函數的參數不相符。只有當把成員函數聲明成 static 時,它才沒有多余的隱含的this參數)。
雖然回調顯得代碼整齊,但有時候你還是想要非回調的 select 查詢。這可以通過 sqlite3_get_table 函數做到。
int sqlite3_get_table(sqlite3*, const char *sql, char ***resultp, int *nrow, int *ncolumn, char **errmsg );
第1個參數不再多說,看前面的例子。
第2個參數是 sql 語句,跟 sqlite3_exec 里的 sql 是一樣的。是一個很普通的以/0結尾的char *字符串。
第3個參數是查詢結果,它依然一維數組(不要以為是二維數組,更不要以為是三維數組)。它內存布局是:第一行是字段名稱,后面是緊接著是每個字段的值。下面用例子來說事。
第4個參數是查詢出多少條記錄(即查出多少行)。
第5個參數是多少個字段(多少列)。
第6個參數是錯誤信息,跟前面一樣,這里不多說了。
下面給個簡單例子:
1
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
|
int main( int , char ** ) { sqlite3 * db; int result; char * errmsg = NULL; char **dbResult; //是 char ** 類型,兩個*號 int nRow, nColumn; int i , j; int index; result = sqlite3_open( “c://Dcg_database.db”, &db ); if( result != SQLITE_OK ) { //數據庫打開失敗 return -1; } //數據庫操作代碼 //假設前面已經創建了 MyTable_1 表 //開始查詢,傳入的 dbResult 已經是 char **,這里又加了一個 & 取地址符,傳遞進去的就成了 char *** result = sqlite3_get_table( db, “select * from MyTable_1”, &dbResult, &nRow, &nColumn, &errmsg ); if( SQLITE_OK == result ) { //查詢成功 index = nColumn; //前面說過 dbResult 前面第一行數據是字段名稱,從 nColumn 索引開始才是真正的數據 printf( “查到%d條記錄/n”, nRow ); for( i = 0; i < nRow ; i++ ) { printf( “第 %d 條記錄/n”, i+1 ); for( j = 0 ; j < nColumn; j++ ) { printf( “字段名:%s ß> 字段值:%s/n”, dbResult[j], dbResult [index] ); ++index; // dbResult 的字段值是連續的,從第0索引到第 nColumn - 1索引都是字段名稱,從第 nColumn 索引開始,后面都是字段值,它把一個二維的表(傳統的行列表示法)用一個扁平的形式來表示 } printf( “-------/n” ); } } //到這里,不論數據庫查詢是否成功,都釋放 char** 查詢結果,使用 sqlite 提供的功能來釋放 sqlite3_free_table( dbResult ); //關閉數據庫 sqlite3_close( db ); return 0; } |
到這個例子為止,sqlite3 的常用用法都介紹完了。
用以上的方法,再配上 sql 語句,完全可以應付絕大多數數據庫需求。
但有一種情況,用上面方法是無法實現的:需要insert、select 二進制。當需要處理二進制數據時,上面的方法就沒辦法做到。下面這一節說明如何插入二進制數據
3 操作二進制
sqlite 操作二進制數據需要用一個輔助的數據類型:sqlite3_stmt * 。
這個數據類型記錄了一個“sql語句”。為什么我把 “sql語句” 用雙引號引起來?因為你可以把 sqlite3_stmt * 所表示的內容看成是 sql語句,但是實際上它不是我們所熟知的sql語句。它是一個已經把sql語句解析了的、用sqlite自己標記記錄的內部數據結構。
正因為這個結構已經被解析了,所以你可以往這個語句里插入二進制數據。當然,把二進制數據插到 sqlite3_stmt 結構里可不能直接 memcpy ,也不能像 std::string 那樣用 + 號。必須用 sqlite 提供的函數來插入。
(1)寫入二進制
下面說寫二進制的步驟。
要插入二進制,前提是這個表的字段的類型是 blob 類型。我假設有這么一張表:
create table Tbl_2( ID integer, file_content blob )
首先聲明
sqlite3_stmt * stat;
然后,把一個 sql 語句解析到 stat 結構里去:
sqlite3_prepare( db, “insert into Tbl_2( ID, file_content) values( 10, ? )”, -1, &stat, 0 );
上面的函數完成 sql 語句的解析。第一個參數跟前面一樣,是個 sqlite3 * 類型變量,第二個參數是一個 sql 語句。
這個 sql 語句特別之處在于 values 里面有個 ? 號。在sqlite3_prepare函數里,?號表示一個未定的值,它的值等下才插入。
第三個參數我寫的是-1,這個參數含義是前面 sql 語句的長度。如果小于0,sqlite會自動計算它的長度(把sql語句當成以/0結尾的字符串)。
第四個參數是 sqlite3_stmt 的指針的指針。解析以后的sql語句就放在這個結構里。
第五個參數我也不知道是干什么的。為0就可以了。
如果這個函數執行成功(返回值是 SQLITE_OK 且 stat 不為NULL ),那么下面就可以開始插入二進制數據。
sqlite3_bind_blob( stat, 1, pdata, (int)(length_of_data_in_bytes), NULL ); // pdata為數據緩沖區,length_of_data_in_bytes為數據大小,以字節為單位
這個函數一共有5個參數。
第1個參數:是前面prepare得到的 sqlite3_stmt * 類型變量。
第2個參數:?號的索引。前面prepare的sql語句里有一個?號,假如有多個?號怎么插入?方法就是改變 bind_blob 函數第2個參數。這個參數我寫1,表示這里插入的值要替換 stat 的第一個?號(這里的索引從1開始計數,而非從0開始)。如果你有多個?號,就寫多個 bind_blob 語句,并改變它們的第2個參數就替換到不同的?號。如果有?號沒有替換,sqlite為它取值null。
第3個參數:二進制數據起始指針。
第4個參數:二進制數據的長度,以字節為單位。
第5個參數:是個析夠回調函數,告訴sqlite當把數據處理完后調用此函數來析夠你的數據。這個參數我還沒有使用過,因此理解也不深刻。但是一般都填NULL,需要釋放的內存自己用代碼來釋放。
bind完了之后,二進制數據就進入了你的“sql語句”里了。你現在可以把它保存到數據庫里:
1
|
int result = sqlite3_step( stat ); |
通過這個語句,stat 表示的sql語句就被寫到了數據庫里。
最后,要把 sqlite3_stmt 結構給釋放:
sqlite3_finalize( stat ); //把剛才分配的內容析構掉
(2)讀出二進制
下面說讀二進制的步驟。
跟前面一樣,先聲明 sqlite3_stmt * 類型變量:
1
|
sqlite3_stmt * stat; |
然后,把一個 sql 語句解析到 stat 結構里去:
1
|
sqlite3_prepare( db, “select * from Tbl_2”, -1, &stat, 0 ); |
當 prepare 成功之后(返回值是 SQLITE_OK ),開始查詢數據。
1
|
int result = sqlite3_step( stat ); |
這一句的返回值是SQLITE_ROW 時表示成功(不是 SQLITE_OK )。
你可以循環執行sqlite3_step 函數,一次step查詢出一條記錄。直到返回值不為 SQLITE_ROW 時表示查詢結束。
然后開始獲取第一個字段:ID 的值。ID是個整數,用下面這個語句獲取它的值:
int id = sqlite3_column_int( stat, 0 ); //第2個參數表示獲取第幾個字段內容,從0開始計算,因為我的表的ID字段是第一個字段,因此這里我填0
下面開始獲取 file_content 的值,因為 file_content 是二進制,因此我需要得到它的指針,還有它的長度:
1
2
|
const void * pFileContent = sqlite3_column_blob( stat, 1 ); int len = sqlite3_column_bytes( stat, 1 ); |
這樣就得到了二進制的值。
把 pFileContent 的內容保存出來之后,不要忘了釋放 sqlite3_stmt 結構:
sqlite3_finalize( stat ); //把剛才分配的內容析構掉
(3)重復使用 sqlite3_stmt 結構
如果你需要重復使用 sqlite3_prepare 解析好的 sqlite3_stmt 結構,需要用函數: sqlite3_reset。
1
|
result = sqlite3_reset(stat); |
這樣, stat 結構又成為 sqlite3_prepare 完成時的狀態,你可以重新為它 bind 內容。
4 事務處理
sqlite 是支持事務處理的。如果你知道你要同步刪除很多數據,不仿把它們做成一個統一的事務。
通常一次 sqlite3_exec 就是一次事務,如果你要刪除1萬條數據,sqlite就做了1萬次:開始新事務->刪除一條數據->提交事務->開始新事務->… 的過程。這個操作是很慢的。因為時間都花在了開始事務、提交事務上。
你可以把這些同類操作做成一個事務,這樣如果操作錯誤,還能夠回滾事務。
事務的操作沒有特別的接口函數,它就是一個普通的 sql 語句而已:
分別如下:
1
2
3
4
|
int result; result = sqlite3_exec( db, "begin transaction", 0, 0, &zErrorMsg ); //開始一個事務 result = sqlite3_exec( db, "commit transaction", 0, 0, &zErrorMsg ); //提交事務 result = sqlite3_exec( db, "rollback transaction", 0, 0, &zErrorMsg ); //回滾事務 |
四、C/C++開發接口簡介1 總覽
SQLite3是SQLite一個全新的版本,它雖然是在SQLite 2.8.13的代碼基礎之上開發的,但是使用了和之前的版本不兼容的數據庫格式和API. SQLite3是為了滿足以下的需求而開發的:
支持UTF-16編碼.
用戶自定義的文本排序方法.
可以對BLOBs字段建立索引.
因此為了支持這些特性我改變了數據庫的格式,建立了一個與之前版本不兼容的3.0版. 至于其他的兼容性的改變,例如全新的API等等,都將在理論介紹之后向你說明,這樣可以使你最快的一次性擺脫兼容性問題.
3.0版的和2.X版的API非常相似,但是有一些重要的改變需要注意. 所有API接口函數和數據結構的前綴都由"sqlite_"改為了"sqlite3_". 這是為了避免同時使用SQLite 2.X和SQLite 3.0這兩個版本的時候發生鏈接沖突.
由于對于C語言應該用什么數據類型來存放UTF-16編碼的字符串并沒有一致的規范. 因此SQLite使用了普通的void* 類型來指向UTF-16編碼的字符串. 客戶端使用過程中可以把void*映射成適合他們的系統的任何數據類型.
2 C/C++接口
SQLite 3.0一共有83個API函數,此外還有一些數據結構和預定義(#defines). (完整的API介紹請參看另一份文檔.) 不過你們可以放心,這些接口使用起來不會像它的數量所暗示的那么復雜. 最簡單的程序仍然使用三個函數就可以完成: sqlite3_open(), sqlite3_exec(), 和 sqlite3_close(). 要是想更好的控制數據庫引擎的執行,可以使用提供的sqlite3_prepare()函數把SQL語句編譯成字節碼,然后在使用sqlite3_step()函數來執行編譯后的字節碼. 以sqlite3_column_開頭的一組API函數用來獲取查詢結果集中的信息. 許多接口函數都是成對出現的,同時有UTF-8和UTF-16兩個版本. 并且提供了一組函數用來執行用戶自定義的SQL函數和文本排序函數.
(1)如何打開關閉數據庫
1
2
3
4
5
6
7
|
typedef struct sqlite3 sqlite3; int sqlite3_open(const char*, sqlite3**); int sqlite3_open16(const void*, sqlite3**); int sqlite3_close(sqlite3*); const char *sqlite3_errmsg(sqlite3*); const void *sqlite3_errmsg16(sqlite3*); int sqlite3_errcode(sqlite3*); |
sqlite3_open() 函數返回一個整數錯誤代碼,而不是像第二版中一樣返回一個指向sqlite3結構體的指針. sqlite3_open() 和sqlite3_open16() 的不同之處在于sqlite3_open16() 使用UTF-16編碼(使用本地主機字節順序)傳遞數據庫文件名. 如果要創建新數據庫, sqlite3_open16() 將內部文本轉換為UTF-16編碼, 反之sqlite3_open() 將文本轉換為UTF-8編碼.
打開或者創建數據庫的命令會被緩存,直到這個數據庫真正被調用的時候才會被執行. 而且允許使用PRAGMA聲明來設置如本地文本編碼或默認內存頁面大小等選項和參數.
sqlite3_errcode() 通常用來獲取最近調用的API接口返回的錯誤代碼. sqlite3_errmsg() 則用來得到這些錯誤代碼所對應的文字說明. 這些錯誤信息將以 UTF-8 的編碼返回,并且在下一次調用任何SQLite API函數的時候被清除. sqlite3_errmsg16() 和sqlite3_errmsg() 大體上相同,除了返回的錯誤信息將以 UTF-16 本機字節順序編碼.
SQLite3的錯誤代碼相比SQLite2沒有任何的改變,它們分別是:
1
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
|
#define SQLITE_OK 0 /* Successful result */ #define SQLITE_ERROR 1 /* SQL error or missing database */ #define SQLITE_INTERNAL 2 /* An internal logic error in SQLite */ #define SQLITE_PERM 3 /* Access permission denied */ #define SQLITE_ABORT 4 /* Callback routine requested an abort */ #define SQLITE_BUSY 5 /* The database file is locked */ #define SQLITE_LOCKED 6 /* A table in the database is locked */ #define SQLITE_NOMEM 7 /* A malloc() failed */ #define SQLITE_READONLY 8 /* Attempt to write a readonly database */ #define SQLITE_INTERRUPT 9 /* Operation terminated by sqlite_interrupt() */ #define SQLITE_IOERR 10 /* Some kind of disk I/O error occurred */ #define SQLITE_CORRUPT 11 /* The database disk image is malformed */ #define SQLITE_NOTFOUND 12 /* (Internal Only) Table or record not found */ #define SQLITE_FULL 13 /* Insertion failed because database is full */ #define SQLITE_CANTOPEN 14 /* Unable to open the database file */ #define SQLITE_PROTOCOL 15 /* Database lock protocol error */ #define SQLITE_EMPTY 16 /* (Internal Only) Database table is empty */ #define SQLITE_SCHEMA 17 /* The database schema changed */ #define SQLITE_TOOBIG 18 /* Too much data for one row of a table */ #define SQLITE_CONSTRAINT 19 /* Abort due to contraint violation */ #define SQLITE_MISMATCH 20 /* Data type mismatch */ #define SQLITE_MISUSE 21 /* Library used incorrectly */ #define SQLITE_NOLFS 22 /* Uses OS features not supported on host */ #define SQLITE_AUTH 23 /* Authorization denied */ #define SQLITE_ROW 100 /* sqlite_step() has another row ready */ #define SQLITE_DONE 101 /* sqlite_step() has finished executing */ |
(2)執行 SQL 語句
typedef int (*sqlite_callback)(void*,int,char**, char**);
int sqlite3_exec(sqlite3*, const char *sql, sqlite_callback, void*, char**);
sqlite3_exec 函數依然像它在SQLite2中一樣承擔著很多的工作. 該函數的第二個參數中可以編譯和執行零個或多個SQL語句. 查詢的結果返回給回調函數. 更多地信息可以查看API 參考.
在SQLite3里,sqlite3_exec一般是被準備SQL語句接口封裝起來使用的.
1
2
3
4
5
|
typedef struct sqlite3_stmt sqlite3_stmt; int sqlite3_prepare(sqlite3*, const char*, int, sqlite3_stmt**, const char**); int sqlite3_prepare16(sqlite3*, const void*, int, sqlite3_stmt**, const void**); int sqlite3_finalize(sqlite3_stmt*); int sqlite3_reset(sqlite3_stmt*); |
sqlite3_prepare 接口把一條SQL語句編譯成字節碼留給后面的執行函數. 使用該接口訪問數據庫是當前比較好的的一種方法.
sqlite3_prepare() 處理的SQL語句應該是UTF-8編碼的. 而sqlite3_prepare16() 則要求是UTF-16編碼的. 輸入的參數中只有第一個SQL語句會被編譯. 第四個參數則用來指向輸入參數中下一個需要編譯的SQL語句存放的SQLite statement對象的指針,任何時候如果調用 sqlite3_finalize() 將銷毀一個準備好的SQL聲明. 在數據庫關閉之前,所有準備好的聲明都必須被釋放銷毀. sqlite3_reset() 函數用來重置一個SQL聲明的狀態,使得它可以被再次執行.
SQL聲明可以包含一些型如"?" 或 "?nnn" 或 ":aaa"的標記, 其中"nnn" 是一個整數,"aaa" 是一個字符串. 這些標記代表一些不確定的字符值(或者說是通配符),可以在后面用sqlite3_bind 接口來填充這些值. 每一個通配符都被分配了一個編號(由它在SQL聲明中的位置決定,從1開始),此外也可以用 "nnn" 來表示 "?nnn" 這種情況. 允許相同的通配符在同一個SQL聲明中出現多次, 在這種情況下所有相同的通配符都會被替換成相同的值. 沒有被綁定的通配符將自動取NULL值.
1
2
3
4
5
6
7
8
|
int sqlite3_bind_blob(sqlite3_stmt*, int, const void*, int n, void(*)(void*)); int sqlite3_bind_double(sqlite3_stmt*, int, double); int sqlite3_bind_int(sqlite3_stmt*, int, int); int sqlite3_bind_int64(sqlite3_stmt*, int, long long int); int sqlite3_bind_null(sqlite3_stmt*, int); int sqlite3_bind_text(sqlite3_stmt*, int, const char*, int n, void(*)(void*)); int sqlite3_bind_text16(sqlite3_stmt*, int, const void*, int n, void(*)(void*)); int sqlite3_bind_value(sqlite3_stmt*, int, const sqlite3_value*); |
以上是 sqlite3_bind 所包含的全部接口,它們是用來給SQL聲明中的通配符賦值的. 沒有綁定的通配符則被認為是空值.綁定上的值不會被sqlite3_reset()函數重置. 但是在調用了sqlite3_reset()之后所有的通配符都可以被重新賦值.
在SQL聲明準備好之后(其中綁定的步驟是可選的), 需要調用以下的方法來執行:
int sqlite3_step(sqlite3_stmt*);
如果SQL返回了一個單行結果集,sqlite3_step() 函數將返回 SQLITE_ROW , 如果SQL語句執行成功或者正常將返回SQLITE_DONE , 否則將返回錯誤代碼. 如果不能打開數據庫文件則會返回 SQLITE_BUSY . 如果函數的返回值是SQLITE_ROW, 那么下邊的這些方法可以用來獲得記錄集行中的數據:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
const void *sqlite3_column_blob(sqlite3_stmt*, int iCol); int sqlite3_column_bytes(sqlite3_stmt*, int iCol); int sqlite3_column_bytes16(sqlite3_stmt*, int iCol); int sqlite3_column_count(sqlite3_stmt*); const char *sqlite3_column_decltype(sqlite3_stmt *, int iCol); const void *sqlite3_column_decltype16(sqlite3_stmt *, int iCol); double sqlite3_column_double(sqlite3_stmt*, int iCol); int sqlite3_column_int(sqlite3_stmt*, int iCol); long long int sqlite3_column_int64(sqlite3_stmt*, int iCol); const char *sqlite3_column_name(sqlite3_stmt*, int iCol); const void *sqlite3_column_name16(sqlite3_stmt*, int iCol); const unsigned char *sqlite3_column_text(sqlite3_stmt*, int iCol); const void *sqlite3_column_text16(sqlite3_stmt*, int iCol); int sqlite3_column_type(sqlite3_stmt*, int iCol); |
sqlite3_column_count()函數返回結果集中包含的列數. sqlite3_column_count() 可以在執行了 sqlite3_prepare()之后的任何時刻調用. sqlite3_data_count()除了必需要在sqlite3_step()之后調用之外,其他跟sqlite3_column_count() 大同小異. 如果調用sqlite3_step() 返回值是 SQLITE_DONE 或者一個錯誤代碼, 則此時調用sqlite3_data_count() 將返回 0 ,然而sqlite3_column_count() 仍然會返回結果集中包含的列數.
返回的記錄集通過使用其它的幾個 sqlite3_column_***() 函數來提取, 所有的這些函數都把列的編號作為第二個參數. 列編號從左到右以零起始. 請注意它和之前那些從1起始的參數的不同.
sqlite3_column_type()函數返回第N列的值的數據類型. 具體的返回值如下:
1
2
3
4
5
|
#define SQLITE_INTEGER 1 #define SQLITE_FLOAT 2 #define SQLITE_TEXT 3 #define SQLITE_BLOB 4 #define SQLITE_NULL 5 |
sqlite3_column_decltype() 則用來返回該列在 CREATE TABLE 語句中聲明的類型. 它可以用在當返回類型是空字符串的時候. sqlite3_column_name() 返回第N列的字段名. sqlite3_column_bytes() 用來返回 UTF-8 編碼的BLOBs列的字節數或者TEXT字符串的字節數. sqlite3_column_bytes16() 對于BLOBs列返回同樣的結果,但是對于TEXT字符串則按 UTF-16 的編碼來計算字節數. sqlite3_column_blob() 返回 BLOB 數據. sqlite3_column_text() 返回 UTF-8 編碼的 TEXT 數據. sqlite3_column_text16() 返回 UTF-16 編碼的 TEXT 數據. sqlite3_column_int() 以本地主機的整數格式返回一個整數值. sqlite3_column_int64() 返回一個64位的整數. 最后, sqlite3_column_double() 返回浮點數.
不一定非要按照sqlite3_column_type()接口返回的數據類型來獲取數據. 數據類型不同時軟件將自動轉換.
(3)用戶自定義函數
可以使用以下的方法來創建用戶自定義的SQL函數:
1
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
|
typedef struct sqlite3_value sqlite3_value; int sqlite3_create_function( sqlite3 *, const char *zFunctionName, int nArg, int eTextRep, void*, void (*xFunc)(sqlite3_context*,int,sqlite3_value**), void (*xStep)(sqlite3_context*,int,sqlite3_value**), void (*xFinal)(sqlite3_context*) ); int sqlite3_create_function16( sqlite3*, const void *zFunctionName, int nArg, int eTextRep, void*, void (*xFunc)(sqlite3_context*,int,sqlite3_value**), void (*xStep)(sqlite3_context*,int,sqlite3_value**), void (*xFinal)(sqlite3_context*) ); #define SQLITE_UTF8 1 #define SQLITE_UTF16 2 #define SQLITE_UTF16BE 3 #define SQLITE_UTF16LE 4 #define SQLITE_ANY 5 |
nArg 參數用來表明自定義函數的參數個數. 如果參數值為0,則表示接受任意個數的參數. 用 eTextRep 參數來表明傳入參數的編碼形式. 參數值可以是上面的五種預定義值. SQLite3 允許同一個自定義函數有多種不同的編碼參數的版本. 數據庫引擎會自動選擇轉換參數編碼個數最少的版本使用.
普通的函數只需要設置 xFunc 參數,而把 xStep 和 xFinal 設為NULL. 聚合函數則需要設置 xStep 和 xFinal 參數,然后把 xFunc 設為NULL. 該方法和使用sqlite3_create_aggregate() API一樣.
sqlite3_create_function16()和sqlite_create_function()的不同就在于自定義的函數名一個要求是 UTF-16 編碼,而另一個則要求是 UTF-8.
請注意自定函數的參數目前使用了sqlite3_value結構體指針替代了SQLite version 2.X中的字符串指針. 下面的函數用來從sqlite3_value結構體中提取數據:
1
2
3
4
5
6
7
8
9
|
const void *sqlite3_value_blob(sqlite3_value*); int sqlite3_value_bytes(sqlite3_value*); int sqlite3_value_bytes16(sqlite3_value*); double sqlite3_value_double(sqlite3_value*); int sqlite3_value_int(sqlite3_value*); long long int sqlite3_value_int64(sqlite3_value*); const unsigned char *sqlite3_value_text(sqlite3_value*); const void *sqlite3_value_text16(sqlite3_value*); int sqlite3_value_type(sqlite3_value*); |
上面的函數調用以下的API來獲得上下文內容和返回結果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
void *sqlite3_aggregate_context(sqlite3_context*, int nbyte); void *sqlite3_user_data(sqlite3_context*); void sqlite3_result_blob(sqlite3_context*, const void*, int n, void(*)(void*)); void qlite3_result_double(sqlite3_context*, double ); void sqlite3_result_error(sqlite3_context*, const char *, int ); void sqlite3_result_error16(sqlite3_context*, const void*, int ); void sqlite3_result_int(sqlite3_context*, int ); void sqlite3_result_int64(sqlite3_context*, long long int ); void sqlite3_result_null(sqlite3_context*); void sqlite3_result_text(sqlite3_context*, const char *, int n, void(*)(void*)); void sqlite3_result_text16(sqlite3_context*, const void*, int n, void(*)(void*)); void sqlite3_result_value(sqlite3_context*, sqlite3_value*); void *sqlite3_get_auxdata(sqlite3_context*, int ); void sqlite3_set_auxdata(sqlite3_context*, int , void*, void (*)(void*)); |
(4)用戶自定義排序規則
下面的函數用來實現用戶自定義的排序規則:
1
2
3
4
5
6
7
8
|
sqlite3_create_collation(sqlite3*, const char *zName, int eTextRep, void*, int (*xCompare)(void*, int ,const void*, int ,const void*)); sqlite3_create_collation16(sqlite3*, const void *zName, int eTextRep, void*, int (*xCompare)(void*, int ,const void*, int ,const void*)); sqlite3_collation_needed(sqlite3*, void*, void(*)(void*,sqlite3*, int eTextRep,const char *)); sqlite3_collation_needed16(sqlite3*, void*, void(*)(void*,sqlite3*, int eTextRep,const void*)); |
sqlite3_create_collation() 函數用來聲明一個排序序列和實現它的比較函數. 比較函數只能用來做文本的比較. eTextRep 參數可以取如下的預定義值 SQLITE_UTF8, SQLITE_UTF16LE, SQLITE_UTF16BE, SQLITE_ANY,用來表示比較函數所處理的文本的編碼方式. 同一個自定義的排序規則的同一個比較函數可以有 UTF-8, UTF-16LE 和 UTF-16BE 等多個編碼的版本. sqlite3_create_collation16()和sqlite3_create_collation() 的區別也僅僅在于排序名稱的編碼是 UTF-16 還是 UTF-8.
可以使用 sqlite3_collation_needed() 函數來注冊一個回調函數,當數據庫引擎遇到未知的排序規則時會自動調用該函數. 在回調函數中可以查找一個相似的比較函數,并激活相應的sqlite_3_create_collation()函數. 回調函數的第四個參數是排序規則的名稱,同樣sqlite3_collation_needed采用 UTF-8 編碼. sqlite3_collation_need16() 采用 UTF-16 編碼.
五、給數據庫加密
前面所說的內容網上已經有很多資料,雖然比較零散,但是花點時間也還是可以找到的。現在要說的這個——數據庫加密,資料就很難找。也可能是我操作水平不夠,找不到對應資料。但不管這樣,我還是通過網上能找到的很有限的資料,探索出了給sqlite數據庫加密的完整步驟。
這里要提一下,雖然 sqlite 很好用,速度快、體積小巧。但是它保存的文件卻是明文的。若不信可以用 NotePad 打開數據庫文件瞧瞧,里面 insert 的內容幾乎一覽無余。這樣赤裸裸的展現自己,可不是我們的初衷。當然,如果你在嵌入式系統、智能手機上使用 sqlite,最好是不加密,因為這些系統運算能力有限,你做為一個新功能提供者,不能把用戶有限的運算能力全部花掉。
Sqlite為了速度而誕生。因此Sqlite本身不對數據庫加密,要知道,如果你選擇標準AES算法加密,那么一定有接近50%的時間消耗在加解密算法上,甚至更多(性能主要取決于你算法編寫水平以及你是否能使用cpu提供的底層運算能力,比如MMX或sse系列指令可以大幅度提升運算速度)。
Sqlite免費版本是不提供加密功能的,當然你也可以選擇他們的收費版本,那你得支付2000塊錢,而且是USD。我這里也不是說支付錢不好,如果只為了數據庫加密就去支付2000塊,我覺得劃不來。因為下面我將要告訴你如何為免費的Sqlite擴展出加密模塊——自己動手擴展,這是Sqlite允許,也是它提倡的。
那么,就讓我們一起開始為 sqlite3.c 文件擴展出加密模塊。
1 必要的宏
通過閱讀 Sqlite 代碼(當然沒有全部閱讀完,6萬多行代碼,沒有一行是我習慣的風格,我可沒那么多眼神去看),我搞清楚了兩件事:
Sqlite是支持加密擴展的;
需要 #define 一個宏才能使用加密擴展。
這個宏就是 SQLITE_HAS_CODEC。
你在代碼最前面(也可以在 sqlite3.h 文件第一行)定義:
#ifndef SQLITE_HAS_CODEC
#define SQLITE_HAS_CODEC
#endif
如果你在代碼里定義了此宏,但是還能夠正常編譯,那么應該是操作沒有成功。因為你應該會被編譯器提示有一些函數無法鏈接才對。如果你用的是 VC 2003,你可以在“解決方案”里右鍵點擊你的工程,然后選“屬性”,找到“C/C++”,再找到“命令行”,在里面手工添加“/D "SQLITE_HAS_CODEC"”。
定義了這個宏,一些被 Sqlite 故意屏蔽掉的代碼就被使用了。這些代碼就是加解密的接口。
嘗試編譯,vc會提示你有一些函數無法鏈接,因為找不到他們的實現。
如果你也用的是VC2003,那么會得到下面的提示:
error LNK2019: 無法解析的外部符號 _sqlite3CodecGetKey ,該符號在函數 _attachFunc 中被引用
error LNK2019: 無法解析的外部符號 _sqlite3CodecAttach ,該符號在函數 _attachFunc 中被引用
error LNK2019: 無法解析的外部符號 _sqlite3_activate_see ,該符號在函數 _sqlite3Pragma 中被引用
error LNK2019: 無法解析的外部符號 _sqlite3_key ,該符號在函數 _sqlite3Pragma 中被引用
fatal error LNK1120: 4 個無法解析的外部命令
這是正常的,因為Sqlite只留了接口而已,并沒有給出實現。
下面就讓我來實現這些接口。
2自己實現加解密接口函數
如果真要我從一份 www.sqlite.org 網上down下來的 sqlite3.c 文件,直接摸索出這些接口的實現,我認為我還沒有這個能力。
好在網上還有一些代碼已經實現了這個功能。通過參照他們的代碼以及不斷編譯中vc給出的錯誤提示,最終我把整個接口整理出來。
實現這些預留接口不是那么容易,要重頭說一次怎么回事很困難。我把代碼都寫好了,直接把他們按我下面的說明拷貝到 sqlite3.c 文件對應地方即可。我在下面也提供了sqlite3.c 文件,可以直接參考或取下來使用。
這里要說一點的是,我另外新建了兩個文件:crypt.c和crypt.h。
其中crypt.h如此定義:
1
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
|
#ifndef DCG_SQLITE_CRYPT_FUNC_ #define DCG_SQLITE_CRYPT_FUNC_ /*********** 董淳光寫的 SQLITE 加密關鍵函數庫 ***********/ /*********** 關鍵加密函數 ***********/ int My_Encrypt_Func( unsigned char * pData, unsigned int data_len, const char * key, unsigned int len_of_key ); /*********** 關鍵解密函數 ***********/ int My_DeEncrypt_Func( unsigned char * pData, unsigned int data_len, const char * key, unsigned intlen_of_key ); #endif 其中的 crypt.c 如此定義: #include "./crypt.h" #include "memory.h" /*********** 關鍵加密函數 ***********/ int My_Encrypt_Func( unsigned char * pData, unsigned int data_len, const char * key, unsigned int len_of_key ) { return 0; } /*********** 關鍵解密函數 ***********/ int My_DeEncrypt_Func( unsigned char * pData, unsigned int data_len, const char * key, unsigned intlen_of_key ) { return 0; } |
這個文件很容易看,就兩函數,一個加密一個解密。傳進來的參數分別是待處理的數據、數據長度、密鑰、密鑰長度。
處理時直接把結果作用于 pData 指針指向的內容。
你需要定義自己的加解密過程,就改動這兩個函數,其它部分不用動。擴展起來很簡單。
這里有個特點,data_len 一般總是 1024 字節。正因為如此,你可以在你的算法里使用一些特定長度的加密算法,比如AES要求被加密數據一定是128位(16字節)長。這個1024不是碰巧,而是 Sqlite 的頁定義是1024字節,在sqlite3.c文件里有定義:
# define SQLITE_DEFAULT_PAGE_SIZE 1024
你可以改動這個值,不過還是建議沒有必要不要去改它。
上面寫了兩個擴展函數,如何把擴展函數跟 Sqlite 掛接起來,這個過程說起來比較麻煩。我直接貼代碼。
分3個步驟。
首先,在 sqlite3.c 文件頂部,添加下面內容:
1
2
3
|
#ifdef SQLITE_HAS_CODEC #include "./crypt.h" /*********** |
用于在 sqlite3 最后關閉時釋放一些內存
1
2
3
|
***********/ void sqlite3pager_free_codecarg(void *pArg); #endif |
這個函數之所以要在 sqlite3.c 開頭聲明,是因為下面在 sqlite3.c 里面某些函數里要插入這個函數調用。所以要提前聲明。
其次,在sqlite3.c文件里搜索“sqlite3PagerClose”函數,要找到它的實現代碼(而不是聲明代碼)。
實現代碼里一開始是:
1
2
3
4
5
6
7
8
9
10
|
#ifdef SQLITE_ENABLE_MEMORY_MANAGEMENT /* A malloc() cannot fail in sqlite3ThreadData() as one or more calls to ** malloc() must have already been made by this thread before it gets ** to this point. This means the ThreadData must have been allocated already ** so that ThreadData.nAlloc can be set. */ ThreadData *pTsd = sqlite3ThreadData(); assert( pPager ); assert( pTsd && pTsd->nAlloc ); #endif |
需要在這部分后面緊接著插入:
1
2
3
|
#ifdef SQLITE_HAS_CODEC sqlite3pager_free_codecarg(pPager->pCodecArg); #endif |
這里要注意,sqlite3PagerClose 函數大概也是 3.3.17版本左右才改名的,以前版本里是叫 “sqlite3pager_close”。因此你在老版本sqlite代碼里搜索“sqlite3PagerClose”是搜不到的。
類似的還有“sqlite3pager_get”、“sqlite3pager_unref”、“sqlite3pager_write”、“sqlite3pager_pagecount”等都是老版本函數,它們在 pager.h 文件里定義。新版本對應函數是在 sqlite3.h 里定義(因為都合并到 sqlite3.c和sqlite3.h兩文件了)。所以,如果你在使用老版本的sqlite,先看看 pager.h 文件,這些函數不是消失了,也不是新蹦出來的,而是老版本函數改名得到的。
最后,往sqlite3.c 文件下找。找到最后一行:
/************** End of main.c ************************************************/
在這一行后面,接上本文最下面的代碼段。
這些代碼很長,我不再解釋,直接接上去就得了。
唯一要提的是 DeriveKey 函數。這個函數是對密鑰的擴展。比如,你要求密鑰是128位,即是16字節,但是如果用戶只輸入 1個字節呢?2個字節呢?或輸入50個字節呢?你得對密鑰進行擴展,使之符合16字節的要求。
DeriveKey 函數就是做這個擴展的。有人把接收到的密鑰求md5,這也是一個辦法,因為md5運算結果固定16字節,不論你有多少字符,最后就是16字節。這是md5算法的特點。但是我不想用md5,因為還得為它添加包含一些 md5 的.c或.cpp文件。我不想這么做。我自己寫了一個算法來擴展密鑰,很簡單的算法。當然,你也可以使用你的擴展方法,也而可以使用md5 算法。只要修改 DeriveKey 函數就可以了。
在 DeriveKey 函數里,只管申請空間構造所需要的密鑰,不需要釋放,因為在另一個函數里有釋放過程,而那個函數會在數據庫關閉時被調用。參考我的 DeriveKey 函數來申請內存。
這里我給出我已經修改好的 sqlite3.c 和 sqlite3.h 文件。
如果太懶,就直接使用這兩個文件,編譯肯定能通過,運行也正常。當然,你必須按我前面提的,新建 crypt.h 和crypt.c 文件,而且函數要按我前面定義的要求來做。
3 加密使用方法
現在,你代碼已經有了加密功能。
你要把加密功能給用上,除了改 sqlite3.c 文件、給你工程添加 SQLITE_HAS_CODEC 宏,還得修改你的數據庫調用函數。
前面提到過,要開始一個數據庫操作,必須先 sqlite3_open 。
加解密過程就在 sqlite3_open 后面操作。
假設你已經 sqlite3_open 成功了,緊接著寫下面的代碼:
int i;
//添加、使用密碼
i = sqlite3_key( db, "dcg", 3 );
//修改密碼
i = sqlite3_rekey( db, "dcg", 0 );
用 sqlite3_key 函數來提交密碼。
第1個參數是 sqlite3 * 類型變量,代表著用 sqlite3_open 打開的數據庫(或新建數據庫)。
第2個參數是密鑰。
第3個參數是密鑰長度。
用 sqlite3_rekey 來修改密碼。參數含義同 sqlite3_key。
實際上,你可以在sqlite3_open函數之后,到 sqlite3_close 函數之前任意位置調用 sqlite3_key 來設置密碼。
但是如果你沒有設置密碼,而數據庫之前是有密碼的,那么你做任何操作都會得到一個返回值:SQLITE_NOTADB,并且得到錯誤提示:“file is encrypted or is not a database”。
只有當你用 sqlite3_key 設置了正確的密碼,數據庫才會正常工作。
如果你要修改密碼,前提是你必須先 sqlite3_open 打開數據庫成功,然后 sqlite3_key 設置密鑰成功,之后才能用sqlite3_rekey 來修改密碼。
如果數據庫有密碼,但你沒有用 sqlite3_key 設置密碼,那么當你嘗試用 sqlite3_rekey 來修改密碼時會得到SQLITE_NOTADB 返回值。
如果你需要清空密碼,可以使用:
//修改密碼
i = sqlite3_rekey( db, NULL, 0 );
來完成密碼清空功能。
4 sqlite3.c 最后添加代碼段
1
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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
|
/*** 董淳光定義的加密函數 ***/ #ifdef SQLITE_HAS_CODEC /*** 加密結構 ***/ #define CRYPT_OFFSET 8 typedef struct _CryptBlock { BYTE* ReadKey; // 讀數據庫和寫入事務的密鑰 BYTE* WriteKey; // 寫入數據庫的密鑰 int PageSize; // 頁的大小 BYTE* Data; } CryptBlock, *LPCryptBlock; #ifndef DB_KEY_LENGTH_BYTE /*密鑰長度*/ #define DB_KEY_LENGTH_BYTE 16 /*密鑰長度*/ #endif #ifndef DB_KEY_PADDING /*密鑰位數不足時補充的字符*/ #define DB_KEY_PADDING 0x33 /*密鑰位數不足時補充的字符*/ #endif /*** 下面是編譯時提示缺少的函數 ***/ /** 這個函數不需要做任何處理,獲取密鑰的部分在下面 DeriveKey 函數里實現 **/ void sqlite3CodecGetKey(sqlite3* db, int nDB, void** Key, int* nKey) { return ; } /*被sqlite 和 sqlite3_key_interop 調用, 附加密鑰到數據庫.*/ int sqlite3CodecAttach(sqlite3 *db, int nDb, const void *pKey, int nKeyLen); /** 這個函數好像是 sqlite 3.3.17前不久才加的,以前版本的sqlite里沒有看到這個函數 這個函數我還沒有搞清楚是做什么的,它里面什么都不做直接返回,對加解密沒有影響 **/ void sqlite3_activate_see(const char* right ) { return; } int sqlite3_key(sqlite3 *db, const void *pKey, int nKey); int sqlite3_rekey(sqlite3 *db, const void *pKey, int nKey); /*** 下面是上面的函數的輔助處理函數 ***/ // 從用戶提供的緩沖區中得到一個加密密鑰 // 用戶提供的密鑰可能位數上滿足不了要求,使用這個函數來完成密鑰擴展 static unsigned char * DeriveKey(const void *pKey, int nKeyLen); //創建或更新一個頁的加密算法索引.此函數會申請緩沖區. static LPCryptBlock CreateCryptBlock(unsigned char* hKey, Pager *pager, LPCryptBlock pExisting); //加密/解密函數, 被pager調用 void * sqlite3Codec(void *pArg, unsigned char *data, Pgno nPageNum, int nMode); //設置密碼函數 int __stdcall sqlite3_key_interop(sqlite3 *db, const void *pKey, int nKeySize); // 修改密碼函數 int __stdcall sqlite3_rekey_interop(sqlite3 *db, const void *pKey, int nKeySize); //銷毀一個加密塊及相關的緩沖區,密鑰. static void DestroyCryptBlock(LPCryptBlock pBlock); static void * sqlite3pager_get_codecarg(Pager *pPager); void sqlite3pager_set_codec(Pager *pPager,void *(*xCodec)(void*,void*,Pgno,int),void *pCodecArg ); //加密/解密函數, 被pager調用 void * sqlite3Codec(void *pArg, unsigned char *data, Pgno nPageNum, int nMode) { LPCryptBlock pBlock = (LPCryptBlock)pArg; unsigned int dwPageSize = 0; if (!pBlock) return data; // 確保pager的頁長度和加密塊的頁長度相等.如果改變,就需要調整. if (nMode != 2) { PgHdr *pageHeader; pageHeader = DATA_TO_PGHDR(data); if (pageHeader->pPager->pageSize != pBlock->PageSize) { CreateCryptBlock(0, pageHeader->pPager, pBlock); } } switch(nMode) { case 0: // Undo a "case 7" journal file encryption case 2: //重載一個頁 case 3: //載入一個頁 if (!pBlock->ReadKey) break; dwPageSize = pBlock->PageSize; My_DeEncrypt_Func(data, dwPageSize, pBlock->ReadKey, DB_KEY_LENGTH_BYTE ); /*調用我的解密函數*/ break; case 6: //加密一個主數據庫文件的頁 if (!pBlock->WriteKey) break; memcpy(pBlock->Data + CRYPT_OFFSET, data, pBlock->PageSize); data = pBlock->Data + CRYPT_OFFSET; dwPageSize = pBlock->PageSize; My_Encrypt_Func(data , dwPageSize, pBlock->WriteKey, DB_KEY_LENGTH_BYTE ); /*調用我的加密函數*/ break; case 7: //加密事務文件的頁 /*在正常環境下, 讀密鑰和寫密鑰相同. 當數據庫是被重新加密的,讀密鑰和寫密鑰未必相同. 回滾事務必要用數據庫文件的原始密鑰寫入.因此,當一次回滾被寫入,總是用數據庫的讀密鑰, 這是為了保證與讀取原始數據的密鑰相同. */ if (!pBlock->ReadKey) break; memcpy(pBlock->Data + CRYPT_OFFSET, data, pBlock->PageSize); data = pBlock->Data + CRYPT_OFFSET; dwPageSize = pBlock->PageSize; My_Encrypt_Func( data, dwPageSize, pBlock->ReadKey, DB_KEY_LENGTH_BYTE ); /*調用我的加密函數*/ break; } return data; } //銷毀一個加密塊及相關的緩沖區,密鑰. static void DestroyCryptBlock(LPCryptBlock pBlock) { //銷毀讀密鑰. if (pBlock->ReadKey){ sqliteFree(pBlock->ReadKey); } //如果寫密鑰存在并且不等于讀密鑰,也銷毀. if (pBlock->WriteKey && pBlock->WriteKey != pBlock->ReadKey){ sqliteFree(pBlock->WriteKey); } if(pBlock->Data){ sqliteFree(pBlock->Data); } //釋放加密塊. sqliteFree(pBlock); } static void * sqlite3pager_get_codecarg(Pager *pPager) { return (pPager->xCodec) ? pPager->pCodecArg: NULL; } // 從用戶提供的緩沖區中得到一個加密密鑰 static unsigned char * DeriveKey(const void *pKey, int nKeyLen) { unsigned char * hKey = NULL; int j; if( pKey == NULL || nKeyLen == 0 ) { return NULL; } hKey = sqliteMalloc( DB_KEY_LENGTH_BYTE + 1 ); if( hKey == NULL ) { return NULL; } hKey[ DB_KEY_LENGTH_BYTE ] = 0; if( nKeyLen < DB_KEY_LENGTH_BYTE ) { memcpy( hKey, pKey, nKeyLen ); //先拷貝得到密鑰前面的部分 j = DB_KEY_LENGTH_BYTE - nKeyLen; //補充密鑰后面的部分 memset( hKey + nKeyLen, DB_KEY_PADDING, j ); } else { //密鑰位數已經足夠,直接把密鑰取過來 memcpy( hKey, pKey, DB_KEY_LENGTH_BYTE ); } return hKey; } //創建或更新一個頁的加密算法索引.此函數會申請緩沖區. static LPCryptBlock CreateCryptBlock(unsigned char* hKey, Pager *pager, LPCryptBlock pExisting) { LPCryptBlock pBlock; if (!pExisting) //創建新加密塊 { pBlock = sqliteMalloc(sizeof(CryptBlock)); memset(pBlock, 0, sizeof(CryptBlock)); pBlock->ReadKey = hKey; pBlock->WriteKey = hKey; pBlock->PageSize = pager->pageSize; pBlock->Data = (unsigned char*)sqliteMalloc(pBlock->PageSize + CRYPT_OFFSET); } else //更新存在的加密塊 { pBlock = pExisting; if ( pBlock->PageSize != pager->pageSize && !pBlock->Data){ sqliteFree(pBlock->Data); pBlock->PageSize = pager->pageSize; pBlock->Data = (unsigned char*)sqliteMalloc(pBlock->PageSize + CRYPT_OFFSET); } } memset(pBlock->Data, 0, pBlock->PageSize + CRYPT_OFFSET); return pBlock; } /* ** Set the codec for this pager */ void sqlite3pager_set_codec( Pager *pPager, void *(*xCodec)(void*,void*,Pgno,int), void *pCodecArg ) { pPager->xCodec = xCodec; pPager->pCodecArg = pCodecArg; } int sqlite3_key(sqlite3 *db, const void *pKey, int nKey) { return sqlite3_key_interop(db, pKey, nKey); } int sqlite3_rekey(sqlite3 *db, const void *pKey, int nKey) { return sqlite3_rekey_interop(db, pKey, nKey); } /*被sqlite 和 sqlite3_key_interop 調用, 附加密鑰到數據庫.*/ int sqlite3CodecAttach(sqlite3 *db, int nDb, const void *pKey, int nKeyLen) { int rc = SQLITE_ERROR; unsigned char* hKey = 0; //如果沒有指定密匙,可能標識用了主數據庫的加密或沒加密. if (!pKey || !nKeyLen) { if (!nDb) { return SQLITE_OK; //主數據庫, 沒有指定密鑰所以沒有加密. } else //附加數據庫,使用主數據庫的密鑰. { //獲取主數據庫的加密塊并復制密鑰給附加數據庫使用 LPCryptBlock pBlock = (LPCryptBlock)sqlite3pager_get_codecarg(sqlite3BtreePager(db->aDb[0].pBt)); if (!pBlock) return SQLITE_OK; //主數據庫沒有加密 if (!pBlock->ReadKey) return SQLITE_OK; //沒有加密 memcpy(pBlock->ReadKey, &hKey, 16); } } else //用戶提供了密碼,從中創建密鑰. { hKey = DeriveKey(pKey, nKeyLen); } //創建一個新的加密塊,并將解碼器指向新的附加數據庫. if (hKey) { LPCryptBlock pBlock = CreateCryptBlock(hKey, sqlite3BtreePager(db->aDb[nDb].pBt), NULL); sqlite3pager_set_codec(sqlite3BtreePager(db->aDb[nDb].pBt), sqlite3Codec, pBlock); rc = SQLITE_OK; } return rc; } // Changes the encryption key for an existing database. int __stdcall sqlite3_rekey_interop(sqlite3 *db, const void *pKey, int nKeySize) { Btree *pbt = db->aDb[0].pBt; Pager *p = sqlite3BtreePager(pbt); LPCryptBlock pBlock = (LPCryptBlock)sqlite3pager_get_codecarg(p); unsigned char * hKey = DeriveKey(pKey, nKeySize); int rc = SQLITE_ERROR; if (!pBlock && !hKey) return SQLITE_OK; //重新加密一個數據庫,改變pager的寫密鑰, 讀密鑰依舊保留. if (!pBlock) //加密一個未加密的數據庫 { pBlock = CreateCryptBlock(hKey, p, NULL); pBlock->ReadKey = 0; // 原始數據庫未加密 sqlite3pager_set_codec(sqlite3BtreePager(pbt), sqlite3Codec, pBlock); } else // 改變已加密數據庫的寫密鑰 { pBlock->WriteKey = hKey; } // 開始一個事務 rc = sqlite3BtreeBeginTrans(pbt, 1); if (!rc) { // 用新密鑰重寫所有的頁到數據庫。 Pgno nPage = sqlite3PagerPagecount(p); Pgno nSkip = PAGER_MJ_PGNO(p); void *pPage; Pgno n; for(n = 1; rc == SQLITE_OK && n <= nPage; n ++) { if (n == nSkip) continue; rc = sqlite3PagerGet(p, n, &pPage); if(!rc) { rc = sqlite3PagerWrite(pPage); sqlite3PagerUnref(pPage); } } } // 如果成功,提交事務。 if (!rc) { rc = sqlite3BtreeCommit(pbt); } // 如果失敗,回滾。 if (rc) { sqlite3BtreeRollback(pbt); } // 如果成功,銷毀先前的讀密鑰。并使讀密鑰等于當前的寫密鑰。 if (!rc) { if (pBlock->ReadKey) { sqliteFree(pBlock->ReadKey); } pBlock->ReadKey = pBlock->WriteKey; } else// 如果失敗,銷毀當前的寫密鑰,并恢復為當前的讀密鑰。 { if (pBlock->WriteKey) { sqliteFree(pBlock->WriteKey); } pBlock->WriteKey = pBlock->ReadKey; } // 如果讀密鑰和寫密鑰皆為空,就不需要再對頁進行編解碼。 // 銷毀加密塊并移除頁的編解碼器 if (!pBlock->ReadKey && !pBlock->WriteKey) { sqlite3pager_set_codec(p, NULL, NULL); DestroyCryptBlock(pBlock); } return rc; } /*** 下面是加密函數的主體 ***/ int __stdcall sqlite3_key_interop(sqlite3 *db, const void *pKey, int nKeySize) { return sqlite3CodecAttach(db, 0, pKey, nKeySize); } // 釋放與一個頁相關的加密塊 void sqlite3pager_free_codecarg(void *pArg) { if (pArg) DestroyCryptBlock((LPCryptBlock)pArg); } #endif //#ifdef SQLITE_HAS_CODEC |
五、性能優化
很多人直接就使用了,并未注意到SQLite也有配置參數,可以對性能進行調整。有時候,產生的結果會有很大影響。
主要通過pragma指令來實現。
比如: 空間釋放、磁盤同步、Cache大小等。
不要打開。前文提高了,Vacuum的效率非常低!
1 auto_vacuum
PRAGMA auto_vacuum;
PRAGMA auto_vacuum = 0 | 1;
查詢或設置數據庫的auto-vacuum標記。
正常情況下,當提交一個從數據庫中刪除數據的事務時,數據庫文件不改變大小。未使用的文件頁被標記并在以后的添加操作中再次使用。這種情況下使用VACUUM命令釋放刪除得到的空間。
當開啟auto-vacuum,當提交一個從數據庫中刪除數據的事務時,數據庫文件自動收縮, (VACUUM命令在auto-vacuum開啟的數據庫中不起作用)。數據庫會在內部存儲一些信息以便支持這一功能,這使得數據庫文件比不開啟該選項時稍微大一些。
只有在數據庫中未建任何表時才能改變auto-vacuum標記。試圖在已有表的情況下修改不會導致報錯。
2 cache_size
建議改為8000
PRAGMA cache_size;
PRAGMA cache_size = Number-of-pages;
查詢或修改SQLite一次存儲在內存中的數據庫文件頁數。每頁使用約1.5K內存,缺省的緩存大小是2000. 若需要使用改變大量多行的UPDATE或DELETE命令,并且不介意SQLite使用更多的內存的話,可以增大緩存以提高性能。
當使用cache_size pragma改變緩存大小時,改變僅對當前對話有效,當數據庫關閉重新打開時緩存大小恢復到缺省大小。 要想永久改變緩存大小,使用default_cache_size pragma.
3 case_sensitive_like
打開。不然搜索中文字串會出錯。
PRAGMA case_sensitive_like;
PRAGMA case_sensitive_like = 0 | 1;
LIKE運算符的缺省行為是忽略latin1字符的大小寫。因此在缺省情況下'a' LIKE 'A'的值為真。可以通過打開case_sensitive_like pragma來改變這一缺省行為。當啟用case_sensitive_like,'a' LIKE 'A'為假而 'a' LIKE 'a'依然為真。
4 count_changes
打開。便于調試
PRAGMA count_changes;
PRAGMA count_changes = 0 | 1;
查詢或更改count-changes標記。正常情況下INSERT, UPDATE和DELETE語句不返回數據。 當開啟count-changes,以上語句返回一行含一個整數值的數據——該語句插入,修改或刪除的行數。 返回的行數不包括由觸發器產生的插入,修改或刪除等改變的行數。
5 page_size
PRAGMA page_size;
PRAGMA page_size = bytes;
查詢或設置page-size值。只有在未創建數據庫時才能設置page-size。頁面大小必須是2的整數倍且大于等于512小于等于8192。 上限可以通過在編譯時修改宏定義SQLITE_MAX_PAGE_SIZE的值來改變。上限的上限是32768.
6 synchronous
如果有定期備份的機制,而且少量數據丟失可接受,用OFF
PRAGMA synchronous;
PRAGMA synchronous = FULL; (2)
PRAGMA synchronous = NORMAL; (1)
PRAGMA synchronous = OFF; (0)
查詢或更改"synchronous"標記的設定。第一種形式(查詢)返回整數值。 當synchronous設置為FULL (2), SQLite數據庫引擎在緊急時刻會暫停以確定數據已經寫入磁盤。 這使系統崩潰或電源出問題時能確保數據庫在重起后不會損壞。FULL synchronous很安全但很慢。 當synchronous設置為NORMAL, SQLite數據庫引擎在大部分緊急時刻會暫停,但不像FULL模式下那么頻繁。 NORMAL模式下有很小的幾率(但不是不存在)發生電源故障導致數據庫損壞的情況。但實際上,在這種情況下很可能你的硬盤已經不能使用,或者發生了其他的不可恢復的硬件錯誤。 設置為synchronous OFF (0)時,SQLite在傳遞數據給系統以后直接繼續而不暫停。若運行SQLite的應用程序崩潰, 數據不會損傷,但在系統崩潰或寫入數據時意外斷電的情況下數據庫可能會損壞。另一方面,在synchronous OFF時 一些操作可能會快50倍甚至更多。
在SQLite 2中,缺省值為NORMAL.而在3中修改為FULL.
7 temp_store
使用2,內存模式。
PRAGMA temp_store;
PRAGMA temp_store = DEFAULT; (0)
PRAGMA temp_store = FILE; (1)
PRAGMA temp_store = MEMORY; (2)
查詢或更改"temp_store"參數的設置。當temp_store設置為DEFAULT (0),使用編譯時的C預處理宏 TEMP_STORE來定義儲存臨時表和臨時索引的位置。當設置為MEMORY (2)臨時表和索引存放于內存中。 當設置為FILE (1)則存放于文件中。temp_store_directorypragma 可用于指定存放該文件的目錄。當改變temp_store設置,所有已存在的臨時表,索引,觸發器及視圖將被立即刪除。
經測試,在類BBS應用上,通過以上調整,效率可以提高2倍以上。
六、后記
(原文后記)
寫此教程,可不是一個累字能解釋。
但是我還是覺得欣慰的,因為我很久以前就想寫 sqlite 的教程,一來自己備忘,二而已造福大眾,大家不用再走彎路。
本人第一次寫教程,不足的地方請大家指出。
本文可隨意轉載、修改、引用。但無論是轉載、修改、引用,都請附帶我的名字:董淳光。以示對我勞動的肯定。
(補充后記)