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