Windows應用程式資料儲存位置怎麼選 ── SQLite / JSON / 登錄檔 / Access 判斷表
· 小村 豪 · SQLite, Windows, .NET, C#, 資料儲存, 登錄檔, Access, 設計, 判斷表, 技術諮詢
「設定用INI檔案就好了嗎」「歷史資料越來越多,想改用Access儲存」「登錄檔和設定檔該怎麼分工」。開發Windows業務應用程式時,資料儲存位置的選擇是必經之路。然而這個選擇往往在專案初期隨意決定後就不再檢視,數年後才以「JSON檔案膨脹到數十MB,啟動變慢」「共用資料夾裡的Access檔案每週壞一次」「直接寫入Program Files目錄,導致在Windows 11環境下無法執行」等形式浮現問題。
本文將針對業務用Windows應用程式的本機資料儲存,把「放在哪裡」(資料夾的選擇)與「用什麼儲存」(格式・引擎的選擇)分開整理。以本部落格多次採用的判斷表形式,彙整SQLite / JSON / 登錄檔 / Access各自的優勢與陷阱。
1. 先講結論
- 儲存位置的選擇是「放在哪裡」與「用什麼儲存」這兩個獨立的判斷。前者選錯會導致權限、多使用者相關的事故,後者選錯則會導致損毀、效能、維護方面的事故。
- 放置位置的基本原則是:使用者個人的設定與資料放在
%LOCALAPPDATA%(Environment.SpecialFolder.LocalApplicationData),全使用者共用的資料放在%PROGRAMDATA%,而絕不寫入與exe相同的資料夾(Program Files目錄下)。1 - 格式的首選很單純,只有兩種:結構化的小型設定用JSON檔案,會持續增加的業務資料・歷史記錄・需要搜尋的資料用SQLite。這兩者就能涵蓋業務應用程式本機儲存的大部分需求。2
- 登錄檔是「存放小型旗標或與Windows聯動資訊的場所」,並不是應用程式的資料儲存區。若不了解32bit/64bit的登錄檔重新導向機制(
Wow6432Node)就直接使用,會踩到「明明寫入了卻讀不到值」的問題。3 - 在新開發專案中選擇Access(.accdb)作為資料儲存區的理由幾乎已經不存在。即使是為了與既有資產連動而使用,也會伴隨「ACE提供者的位元數必須一致」這項部署上的限制。4
- 不論選用哪種格式,機密資訊(密碼、API金鑰)都必須另外處理:不要以純文字方式放進JSON或登錄檔,而要用DPAPI保護。詳情請參閱另一篇文章「Windows應用程式的機密資訊儲存 - 用DPAPI避免純文字設定」。
2. 將要儲存的資料分成4種類
在決定用什麼儲存之前,先為打算儲存的資料性質做分類。業務應用程式的本機資料,大致可分為以下4種。
| 分類 | 範例 | 特徵 |
|---|---|---|
| 設定 | 連線目標、畫面版面配置、上次開啟的資料夾 | 小。啟動時全部讀取。有時使用者想直接編輯 |
| 業務資料・歷史記錄 | 測量結果、處理歷史記錄、主檔的本機副本 | 持續增加。需要搜尋・彙總。損毀時業務影響很大 |
| 快取 | 縮圖、已下載的資源 | 消失也能重新產生。需要容量管理 |
| 機密資訊 | 儲存的密碼、Token | 少量。不可以純文字放置 |
每種分類適合的存放位置與格式各不相同,這正是本文的核心思路。如果你的應用程式是「設定和歷史記錄全部塞進同一個XML」,重新做這項分類就是改善的第一步。
3. 放在哪裡 ── 資料夾選擇的基本原則
若使用.NET,就以 Environment.GetFolderPath 能取得的位置為基準。5
| 位置 | 取得方式 | 用途 |
|---|---|---|
%LOCALAPPDATA%\公司名稱\應用程式名稱 |
SpecialFolder.LocalApplicationData |
使用者個人資料的預設位置。優先考慮這裡 |
%APPDATA%\公司名稱\應用程式名稱(Roaming) |
SpecialFolder.ApplicationData |
僅限在漫遊使用者設定檔環境中,希望隨使用者移動的設定 |
%PROGRAMDATA%\公司名稱\應用程式名稱 |
SpecialFolder.CommonApplicationData |
全使用者共用的資料。需要設計ACL |
| 文件資料夾下 | SpecialFolder.MyDocuments |
僅限使用者會當成自己的檔案處理的產出物(例如匯出的報表) |
寫成程式碼就是這麼簡單,但如果連「公司名稱\應用程式名稱」這層目錄結構,以及第一次執行時建立資料夾,都包進共用處理裡,就能防止儲存位置隨意增加的情況。
public static class AppPaths
{
public static string DataDir { get; } = CreateDir(
Environment.SpecialFolder.LocalApplicationData);
private static string CreateDir(Environment.SpecialFolder root)
{
var dir = Path.Combine(
Environment.GetFolderPath(root), "KomuraSoft", "MyApp");
Directory.CreateDirectory(dir); // 若已存在則不做任何事
return dir;
}
}
之所以不用字串串接自行組出 %LOCALAPPDATA%,而是使用 Environment.GetFolderPath,是因為即使在服務帳戶、其他使用者身分執行,或設定了資料夾重新導向的環境下,它都能回傳正確的位置。用工作排程器以不同帳戶執行的瞬間路徑就改變的事故(這是工作排程器文章第5章提到的「手動執行才會動」問題的一種),也能藉此避免。
以下列舉3個陷阱。
- 不要寫入exe所在的資料夾。 標準使用者無法寫入Program Files目錄下。舊版32bit應用程式可能因為UAC相容性功能而被悄悄重新導向到
VirtualStore,這會造成「以系統管理員身分執行和以標準使用者身分執行時,設定檔內容卻不一樣」這種難以理解的症狀。 - ProgramData是「可以寫入,但並不安全」。 在預設ACL下,可能形成某使用者建立的檔案,其他使用者無法修改的狀態。若要供全使用者共用讀寫,應在安裝程式中建立資料夾並明確設定ACL。
- 不要把Roaming當成預設選項。 在網域的漫遊使用者設定檔環境中,Roaming目錄下的內容會在登入・登出時同步。若把體積大的資料或機器特有的資料(快取、硬體設定)放在Roaming,會造成同步延遲,或「污染」到其他機器。猶豫不決時就選Local。
4. 用什麼儲存 ── 4種選項的真面目
4.1 JSON檔案 ── 設定的首選
可以用System.Text.Json直接讀寫、人類可讀、方便用Git管理與比對差異——用於設定用途的優點一應俱全。需要注意的地方有兩點。
要做好防止損毀的對策。 若在寫入過程中斷電,殘留下不完整的檔案,下次啟動時就會讀不到。慣用作法是「先寫入暫存檔再置換」,.NET的話File.Replace就提供了附帶備份的置換功能。
var json = JsonSerializer.Serialize(settings, options);
var tmp = path + ".tmp";
File.WriteAllText(tmp, json);
if (File.Exists(path))
File.Replace(tmp, path, path + ".bak");
else
File.Move(tmp, path);
讀取端也一開始就內建「若損毀就嘗試.bak,如果也不行就以預設值啟動並提出警告」這種降級處理,設定檔損毀就不會變成支援案件。
不要把它當成資料儲存區使用。 JSON的適用範圍,是「啟動時全部讀取、結束時全部寫入」這個前提能成立的大小(大約數百KB以內)。如果開始把會持續追加的歷史記錄,或需要搜尋記錄的資料放進JSON,那就是該換成SQLite的訊號。
4.2 SQLite ── 持續增加的資料・需要搜尋的資料的首選
SQLite是不需要伺服器、單一檔案、屬於公共領域的內嵌式資料庫,在.NET中可以透過Microsoft維護的ADO.NET提供者Microsoft.Data.Sqlite,或EF Core的SQLite提供者來操作。2 Microsoft本身也將其推薦為Windows應用程式本機資料儲存的手段6,對於「在本機持續增加的結構化資料」,目前的情況是可以優先考慮SQLite。
首先用程式碼示範它有多方便。只要透過NuGet加入Microsoft.Data.Sqlite,不需要伺服器設定,也不需要連線字串的管理畫面,只要指定檔案路徑就能開始使用。
using Microsoft.Data.Sqlite;
var dbPath = Path.Combine(AppPaths.DataDir, "app.db");
using var conn = new SqliteConnection($"Data Source={dbPath}");
conn.Open();
// 僅第一次執行:啟用WAL模式並建立資料表
using (var cmd = conn.CreateCommand())
{
cmd.CommandText = """
PRAGMA journal_mode=WAL;
CREATE TABLE IF NOT EXISTS measurement (
id INTEGER PRIMARY KEY AUTOINCREMENT,
device_id TEXT NOT NULL,
value REAL NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS ix_measurement_device
ON measurement(device_id, created_at);
""";
cmd.ExecuteNonQuery();
}
// 插入時必須使用參數(不要用字串串接組出SQL)
using (var cmd = conn.CreateCommand())
{
cmd.CommandText =
"INSERT INTO measurement (device_id, value) VALUES ($device, $value)";
cmd.Parameters.AddWithValue("$device", "CAM-01");
cmd.Parameters.AddWithValue("$value", 23.5);
cmd.ExecuteNonQuery();
}
可以看到,只要花費和「往JSON檔案裡追加內容」差不多的力氣,就能取得帶索引的搜尋・彙總,以及數量不受限的歷史記錄。若想套用ORM,EF Core的SQLite提供者就是建構在這個函式庫之上。
在此基礎上,列舉幾個實務要點。
- 啟用WAL模式。 就是上面程式碼中的
PRAGMA journal_mode=WAL;。這能提升讀取與寫入的並行性,即使是UI執行緒與背景處理同時操作同一個資料庫的架構,也比較不容易卡住。WAL的設定會永久保存在資料庫檔案本身,所以不需要每次連線都重新發出。 - 將寫入集中到單一行程的單一路徑。 SQLite的寫入是以資料庫為單位進行互斥。若要從多個執行緒寫入,透過佇列將寫入角色統一為一個,這樣的設計比較安全。此外,大量執行細碎的INSERT時,用明確的交易(transaction)整批處理,速度會比逐筆commit快上好幾個等級。
- 不要放在網路共用資料夾。 透過SMB的檔案鎖定有很多與環境相關的問題,SQLite官方也將網路檔案系統上的共用列為損毀原因的第一位。7 一旦需要多台機器、多使用者同時使用,那就已經是主從式資料庫(如SQL Server Express)的範疇了。
- 要知道型別實際上只有4種。 SQLite的實體型別是INTEGER / REAL / TEXT / BLOB,日期和GUID都是以TEXT形式儲存。事先確認一次
Microsoft.Data.Sqlite的型別對應規則,日後在日期比較與排序上就不會困擾。8 上面範例中把created_at設為datetime('now')(UTC),也是因為混入本地時間會在排序與夏令時間切換時產生問題。顯示時再轉換成本地時間的做法比較安全。 - 備份不要用「複製檔案」,而要用
VACUUM INTO或Backup API。 單純複製運作中的資料庫檔案,有時會拿到WAL與主體不一致的內容(詳見第6章)。
4.3 登錄檔 ── 僅限小型旗標與Windows聯動資訊
登錄檔適合存放的是「是否已安裝」「開機啟動登錄」「檔案關聯」等與Windows自身聯動的資訊,以及極小型的使用者設定。原則是應用程式自己使用的設定放在HKCU下,機器共通的資訊由安裝程式寫入HKLM(若設計成在執行期間寫入HKLM,會需要系統管理員權限,應避免)。
最大的陷阱在於位元數。在64bit Windows中,從32bit行程看到的HKLM\Software會被重新導向到HKLM\Software\Wow6432Node。3 「用登錄檔編輯器看明明有值,應用程式卻讀不到」「32bit應用程式寫入的值,64bit維護工具卻看不到」,這些症狀幾乎都是這個原因。這類問題常在遷移到AnyCPU或轉換為64bit時浮現,請與COM、ActiveX的32bit/64bit問題(「COM/OCX/ActiveX的陷阱 ── Visual Studio的位元數與系統管理員權限」)放在同樣的脈絡下理解。
如果從.NET非得讀取另一種位元檢視不可(例如仍維持32bit的應用程式,需要讀取登錄在64bit側的值),可以用RegistryView明確指定要用哪種檢視。
using Microsoft.Win32;
// 從32bit行程讀取64bit檢視的HKLM
using var hklm64 = RegistryKey.OpenBaseKey(
RegistryHive.LocalMachine, RegistryView.Registry64);
using var key = hklm64.OpenSubKey(@"SOFTWARE\KomuraSoft\MyApp");
var installDir = key?.GetValue("InstallDir") as string;
反過來說,一旦需要這樣指定,也代表「究竟該寫在32bit還是64bit才對」這個設計判斷被延後了。讓寫入端與讀取端的位元數一致,才是正道。
把超過數KB的資料或陣列型資料放進登錄檔,無論在備份、遷移還是診斷方面都很不利。這類用途應該讓給檔案(JSON / SQLite)。
4.4 Access (.accdb) ── 幾乎不會用於新專案,既有系統整合則要有所取捨
過去業務應用程式的本機資料庫首選是Access(JET/ACE),但現在幾乎沒有理由在新開發專案中選它。主要原因在於部署。若要以程式碼存取.accdb,需要ACE(Access Database Engine)提供者,且應用程式的位元數必須與ACE的位元數一致才能連線。4 還存在與Office位元數共存的問題,「在開發機上能動,但客戶端出現Microsoft.ACE.OLEDB.12.0 提供者未在本機電腦上登錄」是經典的支援案件。此外還需要安裝可轉散發套件(Access Database Engine 2016 Redistributable),這也增加了部署物件。9
儘管如此,實務上仍有需要用到Access的場合,例如與既有的Access業務系統做資料整合、讀取用Access製作的主檔等。此時建議做出以下取捨:
- 固定讀寫行程的位元數(實務上多半固定為x86較為現實),並在安裝程式中確認對應的ACE是否存在
- 設計上要避免多人同時寫入放在共用資料夾的.accdb(一旦損毀,復原成本並不划算)
- 長期而言,要保留遷移到SQLite或伺服器型資料庫的路徑
建議做上述取捨。包含Excel/VBA資產在內的既有資產處理方式,也整理在「VBA是什麼 - 限制、未來性、該替換的場合」一文中。
5. 判斷表
| 觀點 | JSON檔案 | SQLite | 登錄檔 | Access (.accdb) |
|---|---|---|---|---|
| 擅長的資料 | 小型設定 | 持續增加的結構化資料、搜尋・彙總 | 小型旗標、Windows聯動 | 與既有Access資產整合 |
| 資料量參考值 | ~數百KB | ~數十GB | ~數KB | ~2GB(規格上限) |
| 搜尋・彙總 | ✕(前提是全部讀入) | ◎(SQL) | ✕ | ○(SQL) |
| 人類可直接閱讀 | ◎ | △(需工具) | △ | △(需Access) |
| 抗損毀能力 | △(需自行處理) | ○(交易) | ○ | △ |
| 多行程同時存取 | ✕ | ○(同一台機器內) | ○ | △ |
| 多機共用 | ✕ | ✕ | ✕ | ✕(實質上) |
| 額外的部署物件 | 無 | 無(NuGet隨附) | 無 | 必須有ACE提供者 |
如同最後一行所示,任何一種本機儲存技術都不適合「多機共用」。放進共用資料夾看起來像是能共用,但JSON沒有互斥機制,SQLite在SMB上的鎖定不可靠,Access則會伴隨損毀風險而遭遇極限。一旦出現多據點、多使用者需要操作同一份資料的需求,請把它當成該建置SQL Server Express之類的伺服器型資料庫,或架設Web API的判斷分界線。
6. 不損毀・可遷移・可還原 ── 不分格式的共通設計
不論選擇哪種格式,只要運用個幾年,就一定會需要以下3項設計。是否在第一次發行時就納入,會大幅影響之後的維護成本。
6.1 為結構・格式附加版本編號
應用程式一旦更新,儲存資料的形式也會跟著改變。「新版本讀取舊版本寫入的資料」的瞬間必定會到來,所以要讓資料本身帶有格式版本資訊。
若是SQLite,PRAGMA user_version正是為這個用途準備的。
int GetVersion(SqliteConnection conn)
{
using var cmd = conn.CreateCommand();
cmd.CommandText = "PRAGMA user_version";
return Convert.ToInt32(cmd.ExecuteScalar());
}
void Migrate(SqliteConnection conn)
{
void Exec(string sql)
{
using var cmd = conn.CreateCommand();
cmd.CommandText = sql;
cmd.ExecuteNonQuery();
}
var v = GetVersion(conn);
if (v > 2)
// 用舊版應用程式開啟了新版應用程式建立的資料庫的情況。
// 不去碰觸不了解的結構,在此中止才安全
throw new InvalidOperationException(
$"這個資料庫(版本 {v})是由更新版本的應用程式建立的。");
using var tx = conn.BeginTransaction();
if (v < 1) Exec("ALTER TABLE measurement ADD COLUMN unit TEXT");
if (v < 2) Exec("CREATE TABLE operator (id INTEGER PRIMARY KEY, name TEXT)");
Exec("PRAGMA user_version = 2");
tx.Commit();
}
這是啟動時檢查版本,只套用差異部分,也就是所謂遷移(migration)的最簡形式。開頭之所以拒絕「比自己更新的版本」,是為了防止把應用程式退回舊版本(rollback)時,舊版程式碼寫入自己不了解的結構而造成損毀的事故。JSON的思路也一樣,在根層放入"version": 2,讀取時插入從舊格式轉換的步驟,並拒絕讀取過新的格式。「不要讓沒有版本編號的資料格式問世」,只要守住這一點,未來的自己就能得救。
6.2 事先決定損毀時的降級行為
第4章提到了各格式的損毀對策(JSON的原子寫入、SQLite的交易),但即便如此,仍會遇到「讀不到的資料」——磁碟故障、防毒軟體誤判而隔離、使用者手動編輯。若沒有事先決定應用程式此時該如何應對,就會變成連啟動都做不到的應用程式。
- 設定讀不到 → 以預設值啟動,並通知使用者此事(若默默套用預設值,會變成「我的設定不見了」的客訴)
- 業務資料讀不到 → 以唯讀模式或錯誤畫面呈現「哪個檔案損毀了」。不要自動覆寫修復(會讓證據消失)
- 有備份 → 提議復原。不過自動復原伴隨著「誤判為損毀而回退到舊資料」的風險,原則上要經過使用者操作確認
6.3 備份的重點不在於「有沒有備」,而在於「能不能還原」
本機資料和伺服器上的資料庫不同,沒有人會幫你備份。若要由應用程式端負責處理,就要決定以下3點。
- 備份什麼:業務資料要備,快取不用備,機密資訊則要考慮到DPAPI的特性——只能在同一使用者、同一台機器上解密(遷移到其他機器需要另外的程序)
- 何時・備到哪裡:可在啟動時或每日,以世代方式備份到
%LOCALAPPDATA%內的backup資料夾。是否再放到共用資料夾或既有的PC備份對象資料夾,則要與營運方協調 - 怎麼備份:SQLite禁止在運作中直接單純複製檔案。用
VACUUM INTO 'backup.db'可以一句話取得具一致性的快照
// VACUUM INTO 不會自動建立父資料夾,且若輸出目的地已存在會發生錯誤。
// 事先完成資料夾建立與不重複檔名的決定
var backupDir = Path.Combine(AppPaths.DataDir, "backup");
Directory.CreateDirectory(backupDir);
var backupPath = Path.Combine(backupDir, $"app-{DateTime.Now:yyyyMMdd-HHmmss}.db");
using var cmd = conn.CreateCommand();
cmd.CommandText = "VACUUM INTO $path";
cmd.Parameters.AddWithValue("$path", backupPath);
cmd.ExecuteNonQuery();
因為世代會持續累積,備份後最好也一併加入「只保留最近N個世代、刪除較舊者」的處理。
而且請務必實際演練一次復原。有備份檔案卻沒人知道怎麼還原、也沒試過,這是業務系統的「常見狀況」。試著寫一份在更換PC時將資料搬到新裝置的操作手冊,通常就能發現備份設計的漏洞(例如用DPAPI保護的認證資訊無法搬移、路徑包含使用者名稱導致換使用者就壞掉等)。關於PC報廢時如何清除資料,也請參閱「報廢Windows PC前該做的事」。
7. 容易猶豫的情境指引
- 「雖然是設定,但將來似乎會增加」 ── 若預期「啟動時全部讀取」的用法遲早會撐不住,一開始就直接用SQLite。在SQLite裡建一個「settings資料表」並沒有什麼不對。
- 「從INI/XML遷移」 ── 若只是單純換格式,改用JSON即可;如果這時混雜著歷史記錄類資料,就分離出來改用SQLite。在讀取端保留1~2個版本的舊格式後備方案,遷移會更安全。
- 「被要求要能用Excel看」 ── 不要把資料儲存區直接改成Excel/Access,而是儲存在SQLite後再加上匯出到CSV/Excel的功能,這樣才能同時滿足資料可靠性與需求。報表輸出的做法請參閱「如何製作Excel報表輸出」。
- 「想用多個行程讀寫同一份檔案」 ── 若在同一台機器內,SQLite(WAL)相當夠用,但需要設計寫入衝突的處理。若要以檔案為基礎進行整合,請使用「檔案整合與鎖定的最佳實踐」中的互斥模式。
- 「想在多台機器間共用」 ── 這就代表要從本機儲存畢業了。首選是在相當於檔案伺服器的機器上架設SQL Server Express(免費,資料庫上限10GB)的主從式架構。另外,SQL Server的「LocalDB」儘管名稱如此,實際上是供開發用途的單使用者環境,請不要為了共用目的而選它。若需要跨據點、也要從公司外部使用,就到了該考慮加入Web API架構的階段。
8. 總結
儲存位置的選擇,只要拆成「放在哪裡」(LocalAppData/ProgramData,且不寫入Program Files)與「用什麼儲存」(設定用JSON、持續增加的資料用SQLite、登錄檔盡量減少使用、Access僅限既有系統整合)來思考,絕大多數情況都能毫不猶豫地決定。
在此之上,不論選用哪種格式,都要在第一次發行時就納入第6章的3項套組——格式版本編號、損毀時的降級行為、可還原的備份。唯獨機密資訊,永遠要另外用DPAPI單獨處理。只要掌握本文的判斷表與共通設計,就能大致避開「膨脹到數十MB的JSON」「每週壞一次的共用Access」這類日後代價高昂的架構。若對既有應用程式的儲存方式感到不安,建議先從「盤點什麼資料存在哪裡」開始著手。
相關文章
- Windows應用程式的機密資訊儲存 - 用DPAPI避免純文字設定
- 檔案整合與鎖定的最佳實踐
- 如何製作Excel報表輸出 - COM自動化/Open XML/範本方式的判斷表
- 用工作排程器安全地執行定期作業
相關諮詢領域
合同會社小村軟體承接業務應用程式資料儲存方式的檢討(包含從INI/XML/Access遷移的設計),以及資料損毀、效能劣化的原因調查。
參考連結
-
Microsoft Learn,KNOWNFOLDERID。關於LocalAppData、RoamingAppData、ProgramData等Windows已知資料夾的定義。 ↩
-
Microsoft Learn,Microsoft.Data.Sqlite overview。關於Microsoft維護的SQLite用ADO.NET提供者概要,以及它是EF Core SQLite提供者的基礎。 ↩ ↩2
-
Microsoft Learn,Registry Redirector。關於在64bit Windows中,32bit行程的登錄檔存取被重新導向到Wow6432Node的機制。 ↩ ↩2
-
Microsoft Learn,Can’t establish a connection to Access Database Engine OLE DB。關於ACE OLE DB提供者必須與存取的行程位元數一致。 ↩ ↩2
-
Microsoft Learn,Environment.SpecialFolder Enum。關於用於從.NET取得已知資料夾的列舉型別。 ↩
-
Microsoft Learn,Use a SQLite database in a Windows app。官方教學,推薦在Windows應用程式的本機資料儲存中使用SQLite與Microsoft.Data.Sqlite / EF Core。 ↩
-
SQLite,How To Corrupt An SQLite Database File。關於網路檔案系統上的鎖定失效是資料庫損毀的主要原因。 ↩
-
Microsoft Learn,Data types (Microsoft.Data.Sqlite)。關於SQLite的4種基本型別,以及DateTime、Guid對應到TEXT的規則。 ↩
-
Microsoft,Microsoft Access Database Engine 2016 Redistributable。關於用於存取.accdb / .mdb的ACE可轉散發套件(32bit/64bit)。 ↩
相關文章
共用相同標籤的最新文章。能以相近的主題延伸理解。
Windows 的行程間通訊該怎麼選 ── 具名管道 / TCP / gRPC / 共享記憶體 / COM 判斷表
整理 Windows 應用程式之間該如何選擇溝通方式。以判斷表整理具名管道、本機 TCP、gRPC、共享記憶體、檔案協作、COM 各自的強項與陷阱,並從實務角度說明 UI+服務分離・32bit/64bit 橋接・權限邊界等典型架構,以及具名管道的實作範例。
在 C# 中將 SQLite 用於業務應用程式 ── WAL 模式・排他控制・損毀對策・與 EF Core 的取捨
整理用 Microsoft.Data.Sqlite 把 SQLite 整合進業務用 Windows 應用程式的實務知識:連線字串與連線池、WAL 模式的原理、SQLITE_BUSY 與寫入路徑單一化、用交易加速、型別對應的陷阱、用 VACUUM INTO 備份,一直到該在什...
如何理解 Windows 的工作階段隔離 ── Session 0・RDP・多使用者同時執行
整理 Windows 應用程式開發者容易混淆的「工作階段」概念。說明服務無法顯示 UI 的 Session 0 隔離原因、RDP 連線時工作階段的行為、具名物件的工作階段隔離,以及共用 PC・RDS 環境常見的設計失誤,從實務角度解說。
Windows 服務的建立與維運 ── 從工作排程器的取捨到 BackgroundService 服務化
常駐處理該做成 Windows 服務,還是工作排程器就足夠?本文整理判斷表,以及用 .NET Worker Service(UseWindowsService)建立服務的方法、執行帳戶、復原選項、事件記錄、安全停止處理,從實務角度梳理上線維運所需的設計。
防止 Windows 應用程式重複啟動 ── 具名 Mutex 與重複執行時的視窗前置
整理業務用 Windows 應用程式的常見需求「不讓同一個應用程式啟動兩次」,如何用具名 Mutex 實作。涵蓋 Global\ 與 Local\ 命名空間差異在 RDP 環境中的陷阱、擁有執行緒限制與 AbandonedMutexException、考量 SetForeg...
相關主題
與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。
Windows 技術主題
彙整 KomuraSoft LLC 關於 Windows 開發、故障調查與既有資產活用文章的主題中心。
與本主題相關的服務
本文連結到以下服務頁面,歡迎從最接近的入口查看。
Windows 應用程式開發
支援包含常駐處理、設備連動、運作日誌與可維護結構的 Windows 桌面應用程式。
技術諮詢 & 設計審查
協助釐清設計方向、架構邊界、生命週期責任,以及既有 Windows 資產的處理方式。
常見問題
整理諮詢這個主題時常見的問題。
- Windows應用程式的設定檔該存放在哪裡?
- 如果是使用者個人的設定・資料,基本原則是放在 %LOCALAPPDATA%(Environment.SpecialFolder.LocalApplicationData)下,以「公司名稱\應用程式名稱」的層級結構存放。若是全使用者共用,則放在 %PROGRAMDATA%,但由於預設ACL可能形成其他使用者無法修改的狀態,需要在安裝程式中明確設定ACL。絕對不可以寫入與exe相同的資料夾(Program Files下)。標準使用者無法寫入該處,舊版32bit應用程式還會因為被悄悄重新導向到VirtualStore而產生難以理解的症狀。
- 設定該用JSON還是SQLite儲存?
- 結構化的小型設定用JSON檔案,會持續增加的業務資料・歷史記錄・需要搜尋的資料則以SQLite為首選,這兩者就能涵蓋業務應用程式本機儲存的大部分需求。JSON的適用範圍大約是「啟動時全部讀取、結束時全部寫入」這個前提能成立的數百KB以內。如果開始把持續追加的歷史記錄或需要搜尋的資料放進JSON,那就是該遷移到SQLite的訊號。即使是設定,只要預期未來會增加,一開始就在SQLite裡建立settings資料表也沒有問題。
- 可以把SQLite資料庫放在網路共用資料夾嗎?
- 應該避免。透過SMB的檔案鎖定有很多與環境相關的問題,SQLite官方也將網路檔案系統上的共用列為損毀原因的第一位。JSON沒有互斥機制,Access也會伴隨損毀風險而遭遇極限,因此任何一種本機儲存技術都不適合多機共用。一旦出現多據點、多使用者需要操作同一份資料的需求,就是該考慮建置SQL Server Express(免費,資料庫上限10GB)之類的伺服器型資料庫,或架設Web API的判斷分界線。
- 可以把應用程式的資料存進登錄檔嗎?
- 登錄檔適合存放的僅限於開機啟動登錄、檔案關聯等與Windows自身聯動的資訊,以及極小型的使用者設定。超過數KB的資料或陣列型資料,無論在備份、遷移還是診斷方面都不利,應該讓給JSON或SQLite。此外,在64bit Windows中,從32bit行程看到的 HKLM\Software 會被重新導向到 Wow6432Node,這會造成「用登錄檔編輯器看明明有值,應用程式卻讀不到」的症狀。讓寫入端與讀取端的位元數一致,才是正道。
作者檔案
本文作者的個人檔案頁面。
Go Komura
小村軟體有限公司 代表
以 Windows 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。