介於 JSON 與 SQLite 的中間選擇 - LiteDB
| | | 0 | |
開發 .NET 桌面小工具時,免不了會有保存資料的需求。若筆數不多查詢不複雜,我會選擇序列化存成 JSON 檔,這是最輕巧簡便的選擇,不需第三方程式庫,資料檔還可直接人工檢視修改;若筆數偏多或涉及複雜查詢、更新或統計,我則會改用 SQLite,免安裝設定就有與 SQL/Oracle/Postgres/MySQL 同等級的關聯式資料庫功能,甚至還能把零散檔案一起放進資料庫,隨資料同步增刪,小缺點是 SQLite 通常要透過 EntityFramework / Dapper 存取,複雜度略高,加上 SQLite 依賴 C 寫的原生程式庫,部署成單一檔案桌面工具或跨平台時要花點心思處理。
介於以上兩種資料規模之間,還有一類需求,偏向文件保存性質,筆數多到存成 JSON 太肥或需要收納零散檔案,但沒有複雜 JOIN 查詢或彙整統計需求,不需動用 SQLite。針對這類情境,有沒有 C# 原生開發的簡單資料庫,提供基本索引功能加速查詢、用單一檔案保存資料及附加檔案、使用起來比 SQLite 簡單且不依賴原生程式庫?
LiteDB 剛好是切中這類情境的優秀選項。
LiteDB 是一套以 C# 開發的 Embedded NoSQL Document Database,從 2010 年代開始發展,定位介於 SQLite 與 MongoDB 之間:跟 SQLite 一樣不需要獨立的資料庫伺服器,以單一檔案儲存資料;但本質上則類似 MongoDB 的 Document 模型,適合 .NET 應用程式保存結構較彈性的資料。LiteDB 從 5.0 版加入 WAL、Transaction、多 Reader、Index 等能力,足以應付日常資料保存需求。
以下是個簡單應用範例,LiteDB 程式寫起來比 SQLite 稍稍簡單一些,但配合 LINQ 查詢時有個重要觀念 - LiteDB 會將資料轉成 BSON 格式儲存,可運用 BSON 查詢語法更有效率地找到資料。但正如有些 LINQ 條件無法被轉成 SQL 語法便不能直接在 IQueryable / EF.Core 使用 參考,LiteDB 有自己一套 Query.EQ、Query.GT... 等方法,若 C# 寫法無法直接轉換成 BSON 查詢,就需改用 BSON 表示式或先載入記憶體再處理境。這部分不難,但要花點時間了解才好上手。參考
補充說明我寫在註解裡,大家直接看 Code 吧!
using LiteDB;
var dbPath = "MyData.db";
if (File.Exists(dbPath)) File.Delete(dbPath); // 每次刪除資料檔重新建立
using var db = new LiteDatabase(dbPath);
var players = db.GetCollection<Player>("players");
var skills = db.GetCollection<Skill>("skills");
// 新增 Skill
skills.Insert(new Skill { Id = "CS", Name = "C#" });
skills.Insert(new Skill { Id = "TS", Name = "TypeScript" });
skills.Insert(new Skill { Id = "JS", Name = "JavaScript" });
skills.Insert(new Skill { Id = "PY", Name = "Python" });
var jeffrey = new Player
{
Name = "Jeffrey",
SkillIds = new string[] { "CS", "JS" },
RegDate = DateTime.Now,
IsActive = true
};
players.Insert(jeffrey);
var retired = new Player {
Name = "Bob",
SkillIds = new string[] { "JS" },
RegDate = DateTime.Now,
IsActive = false
};
players.Insert(retired);
var llm = new Player
{
Name = "LLM",
SkillIds = skills.FindAll().Select(s => s.Id).ToArray(),
RegDate = DateTime.Now,
IsActive = true
};
players.Insert(llm);
// 使用 FindOne 查詢
var found = players.FindOne(p => p.Name == "Jeffrey");
Console.WriteLine($"Found Player: {found.Id}. {found?.Name}");
// 使用 LINQ 查詢所有 Active 玩家姓名
var activePlayers = players.Query().Where(p => p.IsActive).Select(p => p.Name).ToList();
Console.WriteLine($"\nActive Players: {string.Join(", ", activePlayers)}");
// 建立索引
players.EnsureIndex(p => p.SkillIds);
// 使用 Query.EQ 查詢,可善用索引提高查詢效能
var jsPlayers = players.Find(Query.EQ(nameof(Player.SkillIds), "JS")).ToList();
// 注意:不是所有 LINQ 方法都能直接轉換成 LiteDB 的 BSON 查詢,例如:
// players.Query().Where(p => p.SkillIds.Contains("JS")).ToList();
// Contains 不在支援之列,會產生錯誤:
// Method Contains not available to convert to BsonExpression
// (op_Implicit(Convert(p.SkillIds, String[])).Contains("JS")).
// 另個解法是先將資料載入記憶體再查,但效能較差
// players.Query().ToList().Where(p => p.SkillIds.Contains("JS")).ToList();
Console.WriteLine($"\nJS Players: {string.Join(", ", jsPlayers.Select(p => p.Name))}");
// LiteDB 不直接支援 JOIN 查詢,但可在程式端用 LINQ 操作
// LiteDB 會盡可能翻成 BSON 查詢,不行的話會載入記憶體再處理,效能不能跟 SQLite 相提並論
// 建議自己用 Dictionary 處理以確保效能,避免在 LINQ 裡反覆搜尋 LiteDB
var skillNames = skills.FindAll().ToDictionary(s => s.Id, s => s.Name);
var list = players.Query()
.ToList()
.Select(p => new
{
PlayerName = p.Name,
Skills = p.SkillIds
.Where(skillNames.ContainsKey)
.Select(id => skillNames[id])
.ToArray()
})
.ToList();
foreach (var item in list)
{
Console.WriteLine($" - Player [{item.PlayerName}]: {string.Join(", ", item.Skills)}");
}
Console.WriteLine("\nAfter Deletions:");
// 示範刪除及更新
players.Delete(jeffrey.Id);
// 亦可用條件批次刪除
players.DeleteMany(p => p.IsActive == false);
// 若是直接使用 LiteDB 的 Query 物件,效果相同
players.DeleteMany(Query.EQ(nameof(Player.IsActive), false));
var editEnt = players.FindOne(p => p.Name == "LLM");
if (editEnt != null) {
editEnt.Name = "Skynet";
players.Update(editEnt);
}
foreach (var p in players.FindAll())
{
Console.WriteLine($" - Player: {p.Id}. {p.Name}");
}
public class Player
{
// 取名 Id 自動成為 PK,int/long 會自動跳號
public int Id { get; set; }
public required string Name { get; set; }
public required string[] SkillIds { get; set; }
public DateTime RegDate { get; set; }
public bool IsActive { get; set; }
}
public class Skill
{
// LiteDB 支援 int/long/Guid/objectId/string 作為 PK
// 名稱包含 Id 會自動被視為 PK,亦可用 [BsonId] 標註
// 完全不宣告時底層會建立 _id,但無法 FindById/Update/Delete 故不建議
public required string Id { get; set; }
public required string Name { get; set; }
}

【延伸閱讀】
LiteDB is a lightweight, C#-native embedded NoSQL database ideal for .NET desktop tools. It stores documents and attachments in a single file, supports indexes and transactions, and avoids native dependencies. This article demonstrates CRUD operations, BSON queries, LINQ limitations, and application-side joins.
Comments
Be the first to post a comment