用SQLite數(shù)據(jù)庫(kù)源碼:VS2012下增刪改查與密碼設(shè)置實(shí)戰(zhàn))
簡(jiǎn)介這份資源面向使用C# 2012與VS2012開(kāi)發(fā)64位Windows應(yīng)用的開(kāi)發(fā)者聚焦SQLite嵌入式數(shù)據(jù)庫(kù)的調(diào)用與密碼設(shè)置適合需要輕量級(jí)本地?cái)?shù)據(jù)存儲(chǔ)方案的中級(jí)學(xué)習(xí)者。包內(nèi)共42個(gè)文件以cs源碼、config配置、dll程序集、resx資源、exe可執(zhí)行文件及sln解決方案為主壓縮包約2.48MB結(jié)構(gòu)完整可直接編譯運(yùn)行。項(xiàng)目圍繞SQLiteConnection連接字符串、SQLiteCommand執(zhí)行建表與增刪改查、SQLiteDataReader讀取結(jié)果等核心操作展開(kāi)并演示在連接串中加入密碼參數(shù)實(shí)現(xiàn)數(shù)據(jù)庫(kù)加密保護(hù)幫助讀者理解SQLite本身不直接支持庫(kù)級(jí)密碼時(shí)的替代思路。已有850人學(xué)習(xí)下載可作為C#桌面端數(shù)據(jù)管理、安全存儲(chǔ)與VS2012工程配置的參考范例便于快速遷移到實(shí)際項(xiàng)目中。1. 從一次老項(xiàng)目維護(hù)說(shuō)起這套 64 位 C# 調(diào)用 SQLite 的源碼到底能干什么前陣子幫朋友收拾一個(gè) 2013 年上線的工控上位機(jī)機(jī)器是 Win7 64 位代碼是 VS2012 寫(xiě)的 C#數(shù)據(jù)庫(kù)用的是 SQLite。需求很樸素把歷史報(bào)警記錄從本地文件挪進(jìn)數(shù)據(jù)庫(kù)還要給數(shù)據(jù)庫(kù)加個(gè)密碼防止現(xiàn)場(chǎng)人員直接拿工具打開(kāi)改數(shù)據(jù)。問(wèn)題來(lái)了——網(wǎng)上搜到的 C# SQLite 示例十有八九是 32 位的System.Data.SQLite.dll扔進(jìn) 64 位項(xiàng)目里一跑就報(bào)BadImageFormatException折騰半天才發(fā)現(xiàn)是位數(shù)不匹配。這套「64 位 C#2012 調(diào)用 SQLite 數(shù)據(jù)庫(kù)源碼含設(shè)置密碼」正好踩在這個(gè)點(diǎn)上它給的是能在 VS2012 下直接編譯的 64 位工程封裝了增刪改查還帶了一個(gè)給 SQLite 數(shù)據(jù)庫(kù)設(shè)置密碼的完整流程。適合誰(shuí)手上還有 VS2012 老項(xiàng)目要維護(hù)的 C# 上位機(jī)開(kāi)發(fā)者、需要給本地庫(kù)加訪問(wèn)口令的工控/醫(yī)療設(shè)備軟件維護(hù)人員以及想搞清楚 SQLite 加密到底怎么落地的人。下面我按「先跑通、再講參數(shù)、最后說(shuō)坑」的順序拆一遍。2. 環(huán)境搭起來(lái)VS2012 64 位 SQLite 的依賴(lài)選擇與工程配置2.1 為什么必須是 64 位以及依賴(lài)包怎么選先說(shuō)清楚一個(gè)容易翻車(chē)的點(diǎn)SQLite 本身是 C 寫(xiě)的但 .NET 這邊用的是System.Data.SQLite這個(gè)托管封裝它內(nèi)部要 P/Invoke 一個(gè)原生SQLite.Interop.dll。這個(gè)原生 DLL 分 x86 和 x64 兩個(gè)版本托管 DLL 會(huì)根據(jù)當(dāng)前進(jìn)程位數(shù)去加載對(duì)應(yīng)目錄下的 interop。如果你的 C# 工程編譯目標(biāo)是 AnyCPU在 64 位系統(tǒng)上默認(rèn)跑成 64 位進(jìn)程但引用的卻是 32 位 interop運(yùn)行時(shí)就會(huì)拋BadImageFormatException或者Unable to load DLL SQLite.Interop.dll。所以第一件事不是寫(xiě)代碼是把位數(shù)對(duì)齊。選包的時(shí)候我一般直接去System.Data.SQLite的官方發(fā)布頁(yè)拿帶bundle的包比如System.Data.SQLite.dll加上x(chóng)64、x86兩個(gè)子目錄的 interop。bundle 版的好處是托管和原生打包在一起省得自己配dll搜索路徑。VS2012 對(duì)應(yīng)的是 .NET Framework 4.5選包時(shí)注意別拿只支持 .NET Standard 的新版本那個(gè)在 VS2012 里根本引不進(jìn)來(lái)。工程配置上右鍵項(xiàng)目 → 屬性 → 生成 → 目標(biāo)平臺(tái)明確選x64不要留 AnyCPU。這一步是血淚經(jīng)驗(yàn)AnyCPU 在開(kāi)發(fā)機(jī)上可能碰巧能跑一到客戶(hù)現(xiàn)場(chǎng) 32 位老機(jī)器上就崩。2.2 把 interop 放到正確位置一個(gè)能直接抄的目錄結(jié)構(gòu)很多人引完 DLL 就以為完事了結(jié)果一運(yùn)行就找不到 interop。原因是System.Data.SQLite默認(rèn)按x64\SQLite.Interop.dll和x86\SQLite.Interop.dll的相對(duì)路徑去找。所以你的輸出目錄里必須有這兩個(gè)子文件夾。我一般這樣組織# 工程根目錄下的 libs 文件夾放 SQLite 相關(guān)依賴(lài) libs/ ├── System.Data.SQLite.dll ├── x64/ │ └── SQLite.Interop.dll └── x86/ └── SQLite.Interop.dll然后在 VS2012 里對(duì)這三個(gè) DLL 分別設(shè)置「復(fù)制到輸出目錄 如果較新則復(fù)制」。注意x64和x86兩個(gè)文件夾要作為內(nèi)容保留目錄結(jié)構(gòu)不能只復(fù)制文件本身。如果你只做 64 位可以只留x64但保留x86對(duì)以后兼容沒(méi)壞處。配置完編譯一次去bin\x64\Debug下確認(rèn)SQLite.Interop.dll確實(shí)在x64子目錄里。這一步驗(yàn)證過(guò)了后面 90% 的「找不到 DLL」問(wèn)題都不會(huì)出現(xiàn)。2.3 連接字符串怎么寫(xiě)密碼參數(shù)放哪SQLite 的連接字符串和 SQL Server 那種不一樣它沒(méi)有獨(dú)立的賬號(hào)密碼概念密碼是通過(guò)連接字符串里的Password關(guān)鍵字傳的?;A(chǔ)寫(xiě)法// 普通無(wú)密碼連接 string connStr Data Source.\data\history.db;Version3;; // 帶密碼連接數(shù)據(jù)庫(kù)已加密時(shí) string connStrPwd Data Source.\data\history.db;Version3;PasswordMyPass123;;參數(shù)說(shuō)明Data Source是庫(kù)文件路徑相對(duì)路徑以程序工作目錄為基準(zhǔn)工控項(xiàng)目里我建議一律用AppDomain.CurrentDomain.BaseDirectory拼絕對(duì)路徑避免服務(wù)方式啟動(dòng)時(shí)工作目錄不對(duì)。Version3表示 SQLite 3 格式現(xiàn)在基本都是 3。Password就是加密口令注意這個(gè)口令一旦設(shè)置用普通 SQLite 工具比如 DB Browser for SQLite打開(kāi)會(huì)提示「file is not a database」這是正常的因?yàn)槲募^已經(jīng)被加密了。提示Password只在數(shù)據(jù)庫(kù)本身已加密或你要新建加密庫(kù)時(shí)才有意義。對(duì)一個(gè)沒(méi)加密的庫(kù)直接帶Password去連會(huì)報(bào)「file is encrypted or is not a database」別以為是密碼錯(cuò)了。3. 核心代碼拆解增刪改查封裝與設(shè)置密碼的完整流程3.1 一個(gè)夠用的 SQLiteHelper 封裝源碼里通常會(huì)給一個(gè)SQLiteHelper類(lèi)把連接、執(zhí)行、查詢(xún)包起來(lái)。我按實(shí)際項(xiàng)目里最常用的形態(tài)還原一下核心結(jié)構(gòu)public class SQLiteHelper { private readonly string _connStr; public SQLiteHelper(string dbPath, string password null) { // 拼接連接字符串密碼為空時(shí)不帶 Password 關(guān)鍵字 _connStr string.Format(Data Source{0};Version3;, dbPath); if (!string.IsNullOrEmpty(password)) _connStr Password password ;; } // 執(zhí)行增刪改返回受影響行數(shù) public int ExecuteNonQuery(string sql, params SQLiteParameter[] ps) { using (var conn new SQLiteConnection(_connStr)) using (var cmd new SQLiteCommand(sql, conn)) { if (ps ! null ps.Length 0) cmd.Parameters.AddRange(ps); conn.Open(); return cmd.ExecuteNonQuery(); } } // 查詢(xún)返回 DataTable適合小數(shù)據(jù)量 public DataTable ExecuteQuery(string sql, params SQLiteParameter[] ps) { using (var conn new SQLiteConnection(_connStr)) using (var cmd new SQLiteCommand(sql, conn)) using (var adapter new SQLiteDataAdapter(cmd)) { if (ps ! null ps.Length 0) cmd.Parameters.AddRange(ps); var dt new DataTable(); adapter.Fill(dt); return dt; } } }邏輯說(shuō)明構(gòu)造函數(shù)負(fù)責(zé)拼連接串密碼可選ExecuteNonQuery用于 INSERT/UPDATE/DELETE用using保證連接釋放ExecuteQuery用SQLiteDataAdapter填充DataTable適合工控里幾百到幾千行的配置表。參數(shù)說(shuō)明params SQLiteParameter[]是可變參數(shù)調(diào)用時(shí)可以傳零個(gè)或多個(gè)參數(shù)對(duì)象避免字符串拼接 SQL 帶來(lái)的注入風(fēng)險(xiǎn)。調(diào)用示例var helper new SQLiteHelper(D:\app\data\history.db, MyPass123); // 建表 helper.ExecuteNonQuery(CREATE TABLE IF NOT EXISTS Alarm(Id INTEGER PRIMARY KEY, Msg TEXT, Ts DATETIME)); // 插入用參數(shù)占位 helper.ExecuteNonQuery(INSERT INTO Alarm(Msg, Ts) VALUES(msg, ts), new SQLiteParameter(msg, 溫度超限), new SQLiteParameter(ts, DateTime.Now));3.2 給已有數(shù)據(jù)庫(kù)設(shè)置密碼ChangePassword 的用法這是這套源碼的重點(diǎn)。SQLite 加密不是「打開(kāi)一個(gè)開(kāi)關(guān)」而是對(duì)數(shù)據(jù)庫(kù)文件重新加密。System.Data.SQLite提供了SQLiteConnection.ChangePassword方法可以在連接打開(kāi)后修改密碼。流程是先用舊密碼或空密碼連上再調(diào)ChangePassword設(shè)新密碼。public static void SetDatabasePassword(string dbPath, string oldPwd, string newPwd) { // 舊密碼為空時(shí)連接串不帶 Password string connStr string.Format(Data Source{0};Version3;, dbPath); if (!string.IsNullOrEmpty(oldPwd)) connStr Password oldPwd ;; using (var conn new SQLiteConnection(connStr)) { conn.Open(); // 關(guān)鍵在已打開(kāi)的連接上改密碼 conn.ChangePassword(newPwd); conn.Close(); } }邏輯說(shuō)明ChangePassword必須在連接打開(kāi)狀態(tài)下調(diào)用它內(nèi)部會(huì)重寫(xiě)數(shù)據(jù)庫(kù)頁(yè)并更新文件頭。參數(shù)說(shuō)明oldPwd是當(dāng)前密碼沒(méi)設(shè)過(guò)就傳null或空串newPwd是目標(biāo)密碼傳空串表示取消加密。注意這個(gè)方法執(zhí)行時(shí)間跟庫(kù)大小相關(guān)十萬(wàn)條記錄的庫(kù)可能要幾秒別放在 UI 線程里否則界面會(huì)卡成「未響應(yīng)」。調(diào)用// 給一個(gè)沒(méi)加密的庫(kù)設(shè)密碼 SetDatabasePassword(D:\app\data\history.db, null, MyPass123); // 改密碼 SetDatabasePassword(D:\app\data\history.db, MyPass123, NewPass456); // 取消密碼 SetDatabasePassword(D:\app\data\history.db, NewPass456, );3.3 建庫(kù)時(shí)就帶密碼新建加密庫(kù)的寫(xiě)法如果庫(kù)還沒(méi)建可以直接在連接串里帶Password第一次Open時(shí) SQLite 會(huì)按加密格式創(chuàng)建文件string connStr Data SourceD:\app\data\new.db;Version3;PasswordInitPwd;; using (var conn new SQLiteConnection(connStr)) { conn.Open(); using (var cmd new SQLiteCommand(CREATE TABLE Config(K TEXT, V TEXT), conn)) cmd.ExecuteNonQuery(); }這樣建出來(lái)的new.db從第一頁(yè)起就是加密的之后必須用InitPwd才能打開(kāi)。參數(shù)說(shuō)明Password長(zhǎng)度沒(méi)有硬性限制但工控現(xiàn)場(chǎng)我一般建議 8 到 16 位太短容易被猜太長(zhǎng)現(xiàn)場(chǎng)人員記不住會(huì)來(lái)煩你。字符集上避免用中文和特殊符號(hào)老版本 SQLite 對(duì)非 ASCII 口令的處理在不同平臺(tái)上有差異純字母數(shù)字最穩(wěn)。注意加密后的庫(kù)不能用普通 SQLite 工具直接打開(kāi)包括 DB Browser for SQLite。要查看內(nèi)容得用帶加密支持的連接方式或者先用代碼把密碼去掉再導(dǎo)出。4. 避坑與排查64 位 SQLite 在 VS2012 下最容易翻車(chē)的五件事4.1 現(xiàn)象運(yùn)行報(bào) BadImageFormatException 或找不到 SQLite.Interop.dll原因進(jìn)程位數(shù)和 interop 位數(shù)不匹配或者x64子目錄沒(méi)被復(fù)制到輸出目錄。AnyCPU 在 64 位系統(tǒng)上跑成 64 位進(jìn)程卻加載了 32 位 interop就會(huì)報(bào)這個(gè)。解決項(xiàng)目目標(biāo)平臺(tái)明確設(shè)為x64確認(rèn)bin\x64\Debug\x64\SQLite.Interop.dll存在如果用了安裝部署項(xiàng)目把x64文件夾一并打包進(jìn)去。別偷懶只復(fù)制System.Data.SQLite.dll。4.2 現(xiàn)象帶密碼連接報(bào)「file is encrypted or is not a database」原因?qū)σ粋€(gè)沒(méi)加密的庫(kù)傳了Password或者密碼輸錯(cuò)。SQLite 加密后文件頭被改寫(xiě)密碼不對(duì)時(shí)它無(wú)法識(shí)別文件格式就報(bào)這個(gè)含糊的錯(cuò)誤。解決先確認(rèn)這個(gè)庫(kù)到底有沒(méi)有加密。判斷方法是用不帶Password的連接串去連如果能連上說(shuō)明沒(méi)加密那就別傳密碼如果連不上再試密碼。密碼錯(cuò)了沒(méi)有更精確的提示這是 SQLite 加密的固有行為只能靠你自己記牢。4.3 現(xiàn)象ChangePassword 執(zhí)行后程序卡死或超時(shí)原因ChangePassword會(huì)重寫(xiě)整個(gè)數(shù)據(jù)庫(kù)文件數(shù)據(jù)量大時(shí)耗時(shí)明顯放在 UI 線程就卡界面如果此時(shí)還有別的連接開(kāi)著可能觸發(fā)文件鎖。解決改密碼前確保沒(méi)有其他連接在操作這個(gè)庫(kù)把ChangePassword放到后臺(tái)線程執(zhí)行UI 上給個(gè)進(jìn)度提示大庫(kù)改密碼前先備份一份萬(wàn)一中途斷電文件可能損壞。4.4 現(xiàn)象32 位機(jī)器上部署后打不開(kāi)庫(kù)原因開(kāi)發(fā)機(jī)是 64 位編譯成 x64客戶(hù)現(xiàn)場(chǎng)是 32 位 Win7進(jìn)程根本起不來(lái)或加載 interop 失敗。解決如果必須兼容 32 位就編譯兩個(gè)版本或者改回 AnyCPU 并同時(shí)帶上x(chóng)86和x64兩個(gè) interop 目錄讓運(yùn)行時(shí)自己選。但 AnyCPU 有風(fēng)險(xiǎn)我一般直接問(wèn)清楚現(xiàn)場(chǎng)系統(tǒng)位數(shù)編譯對(duì)應(yīng)版本省得運(yùn)行期出玄學(xué)問(wèn)題。4.5 現(xiàn)象數(shù)據(jù)庫(kù)文件被現(xiàn)場(chǎng)人員用工具打開(kāi)改了數(shù)據(jù)原因沒(méi)設(shè)密碼或者密碼在配置文件里明文寫(xiě)著被翻出來(lái)了。解決給庫(kù)設(shè)密碼只是第一層連接串里的密碼別硬編碼在代碼里至少做個(gè)簡(jiǎn)單混淆或放注冊(cè)表更徹底的做法是業(yè)務(wù)層加校驗(yàn)字段比如關(guān)鍵記錄帶 HMAC被改了能發(fā)現(xiàn)。SQLite 加密防的是「隨手打開(kāi)看看」防不住有心人這點(diǎn)要有預(yù)期。5. 進(jìn)階把加密庫(kù)和明文導(dǎo)出打通以及一個(gè)我常用的驗(yàn)證習(xí)慣實(shí)際維護(hù)中經(jīng)常遇到一個(gè)尷尬庫(kù)加密了但要做數(shù)據(jù)遷移、報(bào)表導(dǎo)出又需要明文。這時(shí)候不能直接拿加密庫(kù)去喂第三方工具得先用代碼解密導(dǎo)出。思路是用帶密碼的連接讀數(shù)據(jù)寫(xiě)到另一個(gè)不帶密碼的庫(kù)里。核心代碼如下public static void ExportToPlain(string srcPath, string pwd, string dstPath) { string srcConn string.Format(Data Source{0};Version3;Password{1};, srcPath, pwd); string dstConn string.Format(Data Source{0};Version3;, dstPath); using (var src new SQLiteConnection(srcConn)) using (var dst new SQLiteConnection(dstConn)) { src.Open(); dst.Open(); // 用 ATTACH 把目標(biāo)庫(kù)掛到源連接上直接跨庫(kù)復(fù)制 using (var cmd new SQLiteCommand( string.Format(ATTACH DATABASE {0} AS plain KEY ;, dstPath), src)) { cmd.ExecuteNonQuery(); } // 復(fù)制表結(jié)構(gòu)和數(shù)據(jù) using (var cmd new SQLiteCommand( CREATE TABLE plain.Alarm AS SELECT * FROM main.Alarm;, src)) { cmd.ExecuteNonQuery(); } } }邏輯說(shuō)明ATTACH DATABASE ... AS plain KEY 把目標(biāo)庫(kù)以空密碼掛載CREATE TABLE plain.Alarm AS SELECT * FROM main.Alarm完成跨庫(kù)復(fù)制。參數(shù)說(shuō)明KEY 表示目標(biāo)庫(kù)不加密如果目標(biāo)庫(kù)也要密碼把空串換成目標(biāo)密碼即可。這個(gè)方法比逐行讀出來(lái)再插進(jìn)去快得多十萬(wàn)條記錄基本秒級(jí)。驗(yàn)證方法上我有一個(gè)習(xí)慣每次改完密碼或做完導(dǎo)出一定用兩件事交叉驗(yàn)證——一是用代碼帶密碼連一次能查到數(shù)據(jù)二是用不帶密碼的連接去連加密庫(kù)確認(rèn)它確實(shí)連不上。兩個(gè)方向都對(duì)才說(shuō)明加密狀態(tài)符合預(yù)期。只測(cè)一個(gè)方向很容易出現(xiàn)「以為加密了其實(shí)沒(méi)有」或者「以為沒(méi)加密其實(shí)設(shè)上了」的烏龍。還有個(gè)細(xì)節(jié)System.Data.SQLite的加密實(shí)現(xiàn)和 SQLCipher 不是一回事兩者生成的加密文件互不兼容。如果你的庫(kù)以后可能要用 SQLCipher 生態(tài)的工具打開(kāi)一開(kāi)始就得選 SQLCipher 方案別用這套ChangePassword。反過(guò)來(lái)如果只是自己項(xiàng)目?jī)?nèi)部用這套足夠依賴(lài)也簡(jiǎn)單。從那以后我每次給老項(xiàng)目加 SQLite 加密都強(qiáng)制走一遍「設(shè)密碼 → 帶密碼讀 → 不帶密碼讀失敗 → 導(dǎo)出明文 → 再設(shè)回去」的完整回路確認(rèn)每一步行為都符合預(yù)期才交付。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取