更新履歴(4件・最終更新 2026年08月02日)
この記事に加えた変更の記録です。アーカイブした更新前のバージョンは、DOI付きの固定URLから読めます。
- 記事の冒頭に「この記事の知識マップ」節を追加しました。本文で扱っている概念とその関係を、要約・図・詳細ページへのリンクにまとめたものです。本文の主張は変えていません。
- 外部レビュー(1283件)への対応として本文を更新しました。個々の変更内容は、この下の履歴を参照してください。
- WALを「先行書き込みログ」と展開し、`-wal`と`-shm`が付随することとネットワークファイルシステムでは機能しないことを追記しました。VirtualStoreに化ける仕組みを、権限とビット数とマニフェストの分岐の図で示し、書き込めるかを実機で確かめる節(Procmonでの確認と`icacls`)を新設しました。分類と形式の早見表にも再構成しています。
- 本文中の関連記事へのリンクの文言が、リンク先の現在のタイトルと食い違っていたのを、実際のタイトルに揃えました。本文の内容は変えていません。
- 初版公開
この記事を引用する(DOI: 10.5281/zenodo.21589899)
この記事はZenodoにアーカイブされています。常に最新版へ解決されるDOIと、いま表示している版に固定されたDOIの両方を下に示します。
小村 豪(2026)「Windowsアプリのデータ保存先の選び方 ── SQLite / JSON / レジストリ / Access 判断表」合同会社小村ソフト. https://doi.org/10.5281/zenodo.21589899 https://staging.comcomponent.com/blog/windows-app-local-data-storage-decision-table/
- DOI(最新版)
- 10.5281/zenodo.21589899
- DOI(この版)
- 10.5281/zenodo.21732894
「設定は INI ファイルでいいのか」「履歴データが増えてきたので Access に入れたい」「レジストリと設定ファイルはどう使い分けるのか」。Windows の業務アプリを作っていると、データの保存先の選択は必ず通る道です。ところが、この選択は最初に何となく決まったまま見直されないことが多く、数年後に「JSON ファイルが数十 MB に育って起動が遅い」「共有フォルダーの Access ファイルが週に一度壊れる」「Program Files 直下に書き込んでいて Windows 11 の環境で動かない」といった形で問題が表面化します。
この記事では、業務 Windows アプリのローカルデータ保存について、「どこに置くか」(フォルダーの選択)と「何で保存するか」(形式・エンジンの選択)を分けて整理します。当ブログで何度か使っている判断表の形式で、SQLite / JSON / レジストリ / Access それぞれの得意分野と落とし穴をまとめます。
1. まず結論
- 保存先選びは「どこに置くか」と「何で保存するか」の 2 つの独立した判断です。前者を間違えると権限・マルチユーザーの事故、後者を間違えると破損・性能・保守の事故になります。
- 置き場所の基本は、ユーザーごとの設定・データなら
%LOCALAPPDATA%(Environment.SpecialFolder.LocalApplicationData)、全ユーザー共有なら%PROGRAMDATA%、そして exe と同じフォルダー(Program Files 配下)には書き込まないです。1 - 形式の第一候補はシンプルに 2 つです。構造化された小さい設定は JSON ファイル、増えていく業務データ・履歴・検索したいデータは SQLite。この 2 つで業務アプリのローカル保存の大半はカバーできます。2
- レジストリは「小さなフラグや Windows との連携情報を置く場所」であって、アプリのデータストアではありません。32bit/64bit のレジストリリダイレクト(
Wow6432Node)を理解せずに使うと、「書いたはずの値が見えない」問題を踏みます。3 - Access(.accdb)を新規開発のデータストアに選ぶ理由はほぼなくなりました。既存資産との連携で使う場合も、ACE プロバイダーのビット数一致という配布上の制約が付きまといます。4
- どの形式でも、機密情報(パスワード・API キー)だけは別扱いです。平文で JSON やレジストリに置かず、DPAPI で保護します。DPAPI(Data Protection API)は、暗号鍵の管理を Windows に任せてしまえる OS の機能だと考えてください。アプリは
ProtectedData.Protectに平文を渡して暗号化された bytes を受け取り、保存するのはその bytes だけです。鍵はログオンしているユーザー(または対象マシン)に紐づいて OS が管理するので、アプリ側に鍵を埋め込む必要がなくなります。裏返せば、同じユーザー・同じマシンでしか復号できないのが基本の性質で、端末移行やバックアップの設計に効いてきます(6.3 節)。5 使い方の詳細は別記事「Windowsアプリの機密情報保存 - DPAPIで平文設定を避ける」を参照してください。
この記事の知識マップ
Windowsアプリのローカルデータ保存は、どこに置くか(LocalAppData・ProgramData・Roaming)と何で保存するか(JSON・SQLite・レジストリ・Access)を分けて判断します。構造化された小さな設定はJSONファイル、増え続ける業務データや履歴はSQLiteが第一候補で、SQLiteはWALモードにより読み書きを両立できますが、ネットワークファイルシステム上では機能せずSMB共有には向きません。レジストリは小さなフラグとWindows連携情報に限られ、32bit/64bitのレジストリリダイレクションを理解せずに使うと値が見えなくなります。Access(.accdb)は新規採用の理由がほぼなく、ACEプロバイダーのビット数一致という制約が付きまといます。機密情報は形式を問わずDPAPIによる暗号化が前提で、複数マシンでの共有が必要になったらSQL Server Expressのようなサーバー型DBへ卒業する判断になります。
flowchart LR
accTitle: Windowsアプリのローカルデータ保存先の知識マップ
accDescr: 設定はJSON、増えるデータはSQLite、レジストリは小さなフラグだけという使い分けと、LocalAppData・ProgramData・Roamingの置き場所、機密情報をDPAPIで守る設計の関係を示す図。
sqlite["SQLite"]
json_file["JSONファイル"]
windows_registry["レジストリ"]
local_appdata["%LOCALAPPDATA%"]
program_data["%PROGRAMDATA%"]
installer["インストーラー"]
roaming_appdata["%APPDATA%(Roaming)"]
wal_mode["WALモード(Write-Ahead Logging)"]
smb_445["SMB(TCP 445)"]
dotnet[".NET(Core以降)"]
dpapi["DPAPI"]
app_secrets["アプリの秘密情報"]
registry_redirection["レジストリリダイレクター(Wow6432Node)"]
access_database["Access(.accdb)"]
ace_provider["ACE(Access Database Engine)プロバイダー"]
bitness_match_requirement["bitness一致要件"]
multi_machine_data_sharing["複数マシンからのデータ共有要件"]
sql_server_express["SQL Server Express"]
partial_write_corruption["書き込み中断によるファイル破損"]
power_loss["電源断・OSクラッシュ"]
atomic_file_replace["アトミックなファイル置き換え(File.Replace)"]
hardware_replacement["ハードウェア交換"]
json_file -.->|"に保存される"| local_appdata
sqlite -.->|"に保存される"| local_appdata
sqlite -.->|"に保存される"| program_data
program_data -->|"で構成できる"| installer
roaming_appdata -.->|"用いるのは非推奨"| sqlite
sqlite -.->|"利用する"| wal_mode
wal_mode -->|"両立しない"| smb_445
sqlite -->|"用いるのは非推奨"| smb_445
dotnet -.->|"利用する"| sqlite
dpapi -->|"推奨される対応"| app_secrets
windows_registry -.->|"利用する"| registry_redirection
access_database -->|"前提とする"| ace_provider
ace_provider -->|"前提とする"| bitness_match_requirement
sqlite -.->|"の後継"| access_database
sqlite -->|"用いるのは非推奨"| multi_machine_data_sharing
sql_server_express -->|"推奨される対応"| multi_machine_data_sharing
sqlite -->|"軽減する"| partial_write_corruption
power_loss -->|"原因になり得る"| partial_write_corruption
atomic_file_replace -->|"防止する"| partial_write_corruption
json_file -.->|"原因になり得る"| partial_write_corruption
dpapi -.->|"両立しない"| hardware_replacement
app_secrets -.->|"に保存される"| json_file
図の実線は常に成り立つ関係、破線は条件付きの関係です(成立条件は詳細ページの各関係の説明に記載)。関係すべての一覧(全22件、根拠・確度つき)と主要概念の定義は知識マップ詳細ページにまとめています。データ: JSON-LD / Turtle
2. 保存するデータを4種類に分類する
何で保存するかを決める前に、保存しようとしているデータの性質を分類します。業務アプリのローカルデータは、だいたい次の 4 種類に分かれます。
| 分類 | 例 | 特徴 |
|---|---|---|
| 設定 | 接続先、画面レイアウト、前回開いたフォルダー | 小さい。起動時に全部読む。ユーザーが直接編集したい場合もある |
| 業務データ・履歴 | 測定結果、処理履歴、マスタのローカルコピー | 増え続ける。検索・集計したい。壊れると業務影響が大きい |
| キャッシュ | サムネイル、ダウンロード済みリソース | 消えても再生成できる。容量管理が必要 |
| 機密情報 | 保存されたパスワード、トークン | 少量。平文で置いてはいけない |
この分類ごとに適切な置き場所と形式が違う、というのがこの記事の骨子です。「設定も履歴も全部ひとつの XML に入っている」ようなアプリは、この分類をやり直すところが改善の第一歩になります。
3. どこに置くか ── フォルダー選択の基本
.NET なら Environment.GetFolderPath で取得できる場所を基準にします。6
| 場所 | 取得方法 | 用途 |
|---|---|---|
%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(User Account Control、ユーザーアカウント制御)の互換機能であるファイル仮想化により
VirtualStoreへ黙ってリダイレクトされることがあり、「管理者で実行したときと標準ユーザーで実行したときで設定ファイルの中身が違う」という不可解な症状の原因になります。 - ProgramData は「書けるが安全ではない」。 既定の ACL では、あるユーザーが作ったファイルを別のユーザーが変更できない構成になり得ます。全ユーザー共有で読み書きするなら、インストーラーでフォルダーを作成し ACL を明示的に設定します。
- Roaming を既定にしない。 ドメインの移動ユーザープロファイル環境では Roaming 配下がログオン・ログオフ時に同期されます。サイズの大きいデータやマシン固有のデータ(キャッシュ、ハードウェア設定)を Roaming に置くと、同期の遅延や別マシンへの「汚染」の原因になります。迷ったら Local です。
3.1 VirtualStoreに化ける仕組み
1 つ目の落とし穴だけ、仕組みを図にしておきます。UAC のファイル仮想化は、マニフェストに requestedExecutionLevel を持たない 32bit アプリが Program Files のような保護された場所へ書き込もうとしたときに、失敗させる代わりに書き込み先をユーザープロファイル内へすり替える互換機能です。読み取りも仮想化された場所が優先されるため、書いた本人には「ちゃんと書けている」ように見えます。7
flowchart TD
A["アプリが Program Files 配下の<br/>settings.ini へ書き込む"] --> B{"書き込み権限があるか"}
B -- "管理者として実行" --> C["本来の場所に書かれる"]
B -- "標準ユーザー" --> D{"64bit か、または<br/>requestedExecutionLevel 指定あり"}
D -- "はい" --> E["アクセス拒否で素直に失敗する"]
D -- "いいえ = 昇格なしの32bitアプリ" --> F["UAC のファイル仮想化が発動"]
F --> G["LOCALAPPDATA の VirtualStore 配下に<br/>ユーザーごとの複製が作られる"]
G --> H["読み取りは仮想化された複製が優先<br/>本人には書けているように見える"]
C --> I["管理者で実行したときと標準ユーザーで実行したときで<br/>読んでいるファイルが違う"]
H --> I
実際の転送先は %LOCALAPPDATA%\VirtualStore\Program Files\... です。仮想化はユーザーごとに別の複製を作るので、「A さんの端末では設定が残るが、共用端末で別の人がログオンすると初期値に戻る」という形でも表面化します。あくまでレガシーアプリ救済の暫定機能であり、64bit プロセスや昇格したプロセス、マニフェストを持つプロセスでは働きません。7 レジストリ側の同じ仕組み(HKLM\Software が HKCU\Software\Classes\VirtualStore へ転送される)は「レジストリの32bit/64bitリダイレクトと仮想化の落とし穴」で詳しく整理しています。
3.2 書き込めるかどうかを実機で確かめる
置き場所の設計は、開発機の管理者アカウントで動かしている限り検証になりません。リリース前に次の 3 つは通しておいてください。
- 標準ユーザーで実行する。 開発機に標準ユーザーのローカルアカウントを 1 つ作り、そのアカウントでログオンしてインストールから起動・保存・再起動までを一周させます。管理者アカウントで「管理者として実行」を外しただけでは、UAC の昇格を伴わないという点は再現できても、そのユーザーが管理者グループに属している事実は変わらないため、ACL の検証としては不十分です。
- どこに書いているかを Process Monitor で確認する。 アプリのプロセスに絞り、操作を
CreateFile/WriteFileに絞ってキャプチャします。目的の場所がACCESS DENIEDになっていないか、Path列にVirtualStoreを含むパスが出ていないかを見ます。Procmon はリダイレクト解決後の実際のパスを表示するので、「書いたつもりの場所」と「実際に書かれた場所」のずれがそのまま見えます。使い方は「Process Monitor(ProcMon)実践ガイド」を参照してください。 - ACL を読む。
icacls "C:\ProgramData\KomuraSoft\MyApp"で、想定したユーザー・グループに書き込みが付いているかを確認します。ProgramData 配下を全ユーザー共有の読み書き領域にする場合は、インストーラーで設定した ACL がここに出ているかまで見ておきます。
この 3 つで引っかからなければ、少なくとも「配布した瞬間に保存できない」類の事故は避けられます。
4. 何で保存するか ── 4つの選択肢の素性
4.1 JSONファイル ── 設定の第一候補
System.Text.Json で素直に読み書きできる、人間が読める、Git 管理や差分比較がしやすい、と設定用途には利点が揃っています。注意点は 2 つです。
破損対策をすること。 書き込み中の電源断で中途半端なファイルが残ると、次回起動時に読めなくなります。「一時ファイルに書いてから置き換える」のが定石で、.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 を試し、それもだめなら既定値で起動して警告する」という縮退動作を最初から入れておくと、設定ファイル破損がサポート案件にならなくなります。
データストアにしないこと。 「起動時に全部読んで、終了時に全部書く」が成立するサイズ(目安として数百 KB まで)が JSON の適用範囲です。追記され続ける履歴やレコード検索が必要なデータを JSON に入れ始めたら、それは次の SQLite のサインです。
4.2 SQLite ── 増えるデータ・検索するデータの第一候補
SQLite はサーバー不要・単一ファイル・パブリックドメインの組み込みデータベースで、.NET からは Microsoft がメンテナンスする ADO.NET プロバイダー Microsoft.Data.Sqlite、または EF Core の SQLite プロバイダーで扱えます。2 Microsoft 自身が Windows アプリのローカルデータ保存手段として推奨しており8、「ローカルで増えていく構造化データ」にはまず 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;です。WAL は Write-Ahead Logging(先行書き込みログ)の略で、変更をデータベース本体に直接書かず、隣に作られる-walファイルへ追記していき、後でまとめて本体へ反映する方式を指します。書き手は末尾に追記するだけなので読み手の邪魔をせず、読み取りと書き込みを同時に進められます。9 UI スレッドとバックグラウンド処理が同じ DB を触る構成でも詰まりにくくなるのはこのためです。WAL の設定はデータベースファイル自体に永続化されるので、接続のたびに発行する必要はありません(一度設定すれば、閉じて開き直しても WAL のままです)。9 副作用として、DB は 1 ファイルで完結しなくなりapp.dbの隣にapp.db-walとapp.db-shmができます。稼働中にapp.dbだけをコピーしたバックアップが危ういのはこれが理由です(6.3 節)。 - 書き込みは 1 プロセス 1 本に寄せる。 SQLite の書き込みはデータベース単位の排他です。複数スレッドから書くなら、キューを介して書き込み役を一本化する設計が安全です。また、細かい INSERT を大量に行うときは明示的なトランザクションでまとめると、1 件ずつコミットするより桁違いに速くなります。
- ネットワーク共有に置かない。 SMB 越しのファイルロックは環境依存の問題が多く、SQLite 公式も破損原因の筆頭としてネットワークファイルシステム上での共有を挙げています。10 そもそも WAL モードは「同じ DB を使うプロセスが同一マシン上にあること」を前提にしており、ネットワークファイルシステム上では機能しません(プロセス間で共有メモリを使うためです)。9 複数台・複数ユーザーから同時に使いたくなったら、それはもうクライアント・サーバー型 DB(SQL Server Express など)の領域です。
- 型は 4 種類しかないことを知っておく。 SQLite の実体は INTEGER / REAL / TEXT / BLOB で、日付や GUID は TEXT として保存されます。
Microsoft.Data.Sqliteの型マッピング規約を一度確認しておくと、日付の比較やソートで悩まなくなります。11 上の例でcreated_atをdatetime('now')(UTC)にしているのも、ローカル時刻を混ぜるとソートとサマータイム跨ぎで揉めるからです。表示時にローカル時刻へ変換する方針が安全です。 - バックアップは「ファイルコピー」ではなく
VACUUM INTOか Backup API で。 稼働中の DB ファイルを単純コピーすると 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開発でハマる登録とbitnessの罠」)と同じ文脈で押さえておいてください。
.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) ── 新規採用はほぼなし、既存連携は割り切って
かつて業務アプリのローカル DB といえば Access(JET/ACE)でしたが、新規開発で選ぶ理由は今はほとんどありません。理由は主に配布です。.accdb にコードからアクセスするには ACE(Access Database Engine)プロバイダーが必要で、アプリのビット数と ACE のビット数が一致していなければ接続できません。4 Office のビット数との共存問題もあり、「開発機では動くが客先で Microsoft.ACE.OLEDB.12.0 プロバイダーがローカルコンピューターに登録されていません になる」は定番のサポート案件です。再頒布可能パッケージ(Access Database Engine 2016 Redistributable)の導入が必要になる点も、配布物を増やします。12
それでも Access が絡む場面は現実にあります。既存の Access 業務システムとのデータ連携、Access で作られたマスタの読み取りなどです。その場合は、
- 読み書きするプロセスのビット数を固定し(x86 固定が現実的なことが多い)、インストーラーで対応する ACE の存在を確認する
- 共有フォルダーに置いた .accdb への多人数同時書き込みは設計として避ける(壊れたときの復旧コストが見合いません)
- 長期的には SQLite またはサーバー型 DB への移行パスを持っておく
という割り切りをおすすめします。Excel/VBA 資産を含めた既存資産の扱いは「VBAとは何か - 制約、将来性、置き換えるべき場面」でも整理しています。
5. 判断表
5.1 分類×形式の早見表
先に、第 2 章の 4 分類と第 4 章の 4 形式を突き合わせた表を置きます。「自分のデータはこの分類だから、この形式・この置き場所」を 1 行で引くための表です。
| 分類(第2章) | JSONファイル | SQLite | レジストリ | Access | 置き場所の既定(第3章) |
|---|---|---|---|---|---|
| 設定 | ◎ 第一候補 | ○ 将来増えるなら最初からこちら | △ ごく小さいフラグと Windows 連携情報だけ | ✕ | %LOCALAPPDATA%(ユーザーに追従させたい設定のみ Roaming) |
| 業務データ・履歴 | ✕ 全読み込み前提が崩れる | ◎ 第一候補 | ✕ | △ 既存Access資産との連携時のみ | %LOCALAPPDATA%(全ユーザー共有なら %PROGRAMDATA%+ACL設計) |
| キャッシュ | △ 小さいものだけ | ○ 件数が多いなら | ✕ | ✕ | %LOCALAPPDATA%(Roamingに置かない) |
| 機密情報 | ○ DPAPIで保護した値を入れる器として | ○ 同左 | △ 同左。ただし小さいものだけ | ✕ | 平文で置かない。DPAPIで保護(第1章) |
読み方は 2 つです。第一に、行をまたいで 1 つの器に詰め込まないこと。「設定も履歴もキャッシュも全部 1 つの JSON」が、後から効いてくる典型的な設計です。第二に、機密情報の行は「どの形式に入れるか」より「保存する前に DPAPI で暗号化してあるか」が本質で、器の選択は残りの 3 分類に従って決めて構いません。
5.2 形式ごとの素性
| 観点 | JSONファイル | SQLite | レジストリ | Access (.accdb) |
|---|---|---|---|---|
| 得意なデータ | 小さな設定 | 増える構造化データ、検索・集計 | 小さなフラグ、Windows連携 | 既存Access資産との連携 |
| データ量の目安 | 〜数百KB | 〜数十GB | 〜数KB | 〜2GB(仕様上限) |
| 検索・集計 | ✕(全読み込み前提) | ◎(SQL) | ✕ | ○(SQL) |
| 人間が直接読める | ◎ | △(ツール要) | △ | △(Access要) |
| 破損への強さ | △(自前で対策) | ○(トランザクション) | ○ | △ |
| 複数プロセス同時アクセス | ✕ | ○(同一マシン内) | ○ | △ |
| 複数マシンからの共有 | ✕ | ✕ | ✕ | ✕(事実上) |
| 追加の配布物 | なし | なし(NuGetに同梱) | なし | ACEプロバイダー必須 |
最後の行が示すとおり、ローカル保存の技術はどれも「複数マシンからの共有」には向きません。共有フォルダーに置けば共有できるように見えますが、JSON は排他がなく、SQLite は SMB 上のロックが信頼できず、Access は破損リスクと共に限界が来ます。複数拠点・複数ユーザーで同じデータを触る要件が出てきたら、SQL Server Express などのサーバー型 DB か 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)
// 新しいバージョンのアプリが作ったDBを旧アプリで開いたケース。
// 知らないスキーマに触らず、ここで止めるのが安全
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();
}
起動時にバージョンを見て差分だけ適用する、いわゆるマイグレーションの最小形です。冒頭で「自分より新しいバージョン」を拒否しているのは、アプリを旧バージョンに戻した(ロールバックした)ときに、旧コードが知らないスキーマへ書き込んで壊す事故を防ぐためです。JSON でも考え方は同じで、ルートに "version": 2 を持たせ、読み込み時に古い形式からの変換を挟み、新しすぎる形式は読み込みを拒否します。「バージョン番号のないデータ形式を世に出さない」、これだけ守れば将来の自分が救われます。
6.2 破損時の縮退動作を決めておく
第 4 章で形式ごとの破損対策(JSON のアトミック書き込み、SQLite のトランザクション)に触れましたが、それでも「読めないデータ」には出会います。ディスク障害、ウイルス対策ソフトの誤検知による隔離、ユーザーによる手編集。そのときアプリがどう振る舞うかを決めていないと、起動すらしないアプリになります。
- 設定が読めない → 既定値で起動し、その旨をユーザーに通知する(黙って既定値にすると「設定が消えた」問い合わせになります)
- 業務データが読めない → 読み取り専用モードやエラー画面で「どのファイルが壊れているか」を提示する。自動で上書き修復しない(証拠が消えます)
- バックアップがある → 復元を提案する。ただし自動復元は「壊れたと誤検知して古いデータに巻き戻す」リスクと表裏一体なので、原則ユーザー操作を挟む
6.3 バックアップは「取れているか」より「戻せるか」
ローカルデータは、サーバーのデータベースと違って誰もバックアップしてくれません。アプリ側で面倒を見るなら、次の 3 点を決めます。
- 何を: 業務データは対象、キャッシュは対象外、機密情報は DPAPI の性質上、同一ユーザー・同一マシンでしか復号できないことを考慮(別マシンへの移行手順が別途必要)
- いつ・どこへ: 起動時や日次で、
%LOCALAPPDATA%内のbackupフォルダーへ世代付きで。さらに共有フォルダーや既存のPCバックアップ対象フォルダーに乗せるかは運用と相談 - どうやって: SQLite は稼働中の単純ファイルコピー禁止。
VACUUM INTO 'backup.db'なら一貫性のあるスナップショットを 1 文で取れます
// 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, DataProtectionScope Enum.
ProtectedData.Protect/Unprotectに指定する保護スコープの定義。CurrentUserでは現在のユーザーコンテキストで動くスレッドだけが復号できること、LocalMachineではそのコンピューター上の任意のプロセスが復号できるため、そのマシンの全アカウントを信頼できる場合に限るべきで、多くの場面ではCurrentUserを使うべきであることについて。 ↩ -
Microsoft Learn, Environment.SpecialFolder Enum. .NET から既知フォルダーを取得するための列挙型について。 ↩
-
Microsoft Learn, UAC Architecture. UAC のファイル/レジストリ仮想化がマシン単位の書き込み要求をユーザー単位の場所へリダイレクトし、読み取りも仮想化された場所を優先すること、Program Files のような保護されたフォルダーへの書き込みに対してユーザープロファイル内の複製が使われユーザーごとに別の複製になること、仮想化が 32bit アプリのみを対象とし、昇格したプロセスや
requestedExecutionLevelを持つマニフェスト付きアプリでは無効になること(昇格していない 64bit アプリはアクセス拒否になること)、そして暫定的な互換機能であり依存すべきでないことについて。 ↩ ↩2 -
Microsoft Learn, Use a SQLite database in a Windows app. Windows アプリのローカルデータ保存に SQLite と Microsoft.Data.Sqlite / EF Core を推奨する公式チュートリアル。 ↩
-
SQLite, Write-Ahead Logging. 変更を本体ではなく WAL ファイルへ追記する仕組み、書き手が追記しかしないため読み手と同時に動けること、
journal_mode=WALが永続的で開き直しても維持されること、-walと-shmの 2 ファイルが付随すること、および同じデータベースを使うプロセスが同一マシン上にある必要があり、ネットワークファイルシステム上では WAL が機能しないことについて。 ↩ ↩2 ↩3 -
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の得意分野と落とし穴を判断表で整理し、定番構成と名前付きパイプの実装例まで解説します。
C#でSQLiteを業務アプリに使う ── WALモード・排他制御・破損対策・EF Coreとの使い分け
Microsoft.Data.SqliteでSQLiteを業務アプリに組み込む実務知識を整理します。WALモード、SQLITE_BUSYと書き込み一本化、型マッピングの罠、VACUUM INTOによるバックアップ、EF Coreとの使い分けまで解説します。
Windowsのセッション分離をどう理解するか ── Session 0・RDP・複数ユーザー同時実行
Windowsアプリ開発者が混乱しがちな「セッション」の概念を整理します。サービスがUIを出せないSession 0分離の理由、RDP接続時のセッションの挙動、名前付きオブジェクトのセッション分離、共有PC・RDS環境でありがちな設計ミスまでを実務目線で解説します。
Windowsサービスの作り方と運用 ── タスクスケジューラとの使い分けからBackgroundServiceのサービス化まで
常駐処理をWindowsサービスにすべきか、タスクスケジューラで足りるのか。判断表と.NET Worker Serviceによるサービスの作り方、実行アカウント・回復オプション・安全な停止処理まで実務目線で整理します。
マルチスレッドの実務ベストプラクティス .NET編 ── スレッドを増やす前に決めておくこと
「スレッドを立てたら、たまに落ちる・固まる」を防ぐ設計の定石を.NET/C#向けに整理。スレッドを自分で作らずTaskに乗る、共有可変状態を減らす、ロックの規律、CancellationTokenでの停止設計、UIスレッドの扱いまで解説します。
関連トピック
このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。
Windows技術トピック
Windows 開発、不具合調査、既存資産活用の技術トピックをまとめた入口です。
このテーマがつながるサービス
この記事は次のサービスページにつながります。近い入口からご覧ください。
Windowsアプリ開発
業務アプリ、装置連携、通信ツールなどの Windows ソフト開発を支援します。
技術相談・設計レビュー
改修方針、設計レビュー、既存資産の扱いを整理するための技術相談です。
よくある質問
この記事のテーマについて、相談時によくある質問をまとめています。
- Windowsアプリの設定ファイルはどこに保存すべきですか?
- ユーザーごとの設定・データなら %LOCALAPPDATA%(Environment.SpecialFolder.LocalApplicationData)配下に「会社名\アプリ名」の階層で置くのが基本です。全ユーザー共有なら %PROGRAMDATA% ですが、既定のACLでは別ユーザーが変更できない構成になり得るため、インストーラーでACLを明示的に設定します。exeと同じフォルダー(Program Files配下)には書き込んではいけません。標準ユーザーには書き込めず、古い32bitアプリではVirtualStoreへ黙ってリダイレクトされる不可解な症状の原因になります。
- 設定はJSONとSQLiteのどちらで保存すべきですか?
- 構造化された小さい設定はJSONファイル、増えていく業務データ・履歴・検索したいデータはSQLiteが第一候補で、この2つで業務アプリのローカル保存の大半はカバーできます。JSONの適用範囲は「起動時に全部読んで終了時に全部書く」が成立する数百KBまでが目安です。追記され続ける履歴やレコード検索が必要なデータをJSONに入れ始めたら、SQLiteへ移行するサインです。設定でも将来増える見込みがあるなら、最初からSQLiteにsettingsテーブルを作る形で問題ありません。
- SQLiteのデータベースをネットワーク共有フォルダーに置いてもよいですか?
- 避けるべきです。SMB越しのファイルロックは環境依存の問題が多く、SQLite公式もネットワークファイルシステム上での共有を破損原因の筆頭に挙げています。JSONは排他がなく、Accessも破損リスクと共に限界が来るため、ローカル保存の技術はどれも複数マシンからの共有には向きません。複数拠点・複数ユーザーで同じデータを触る要件が出てきたら、SQL Server Express(無償、データベース10GBまで)などのサーバー型DBかWeb APIを立てる判断ラインです。
- レジストリにアプリのデータを保存してもよいですか?
- レジストリが適切なのは、スタートアップ登録やファイル関連付けなどWindows自体との連携情報と、ごく小さなユーザー設定までです。数KBを超えるデータや配列的なデータは、バックアップ・移行・診断のどの面でも不利なのでJSONやSQLiteに譲ります。また64bit Windowsでは32bitプロセスから見たHKLM\SoftwareがWow6432Nodeへリダイレクトされるため、「レジストリエディターで見ると値があるのにアプリから読めない」という症状の原因になります。書き込み側と読み取り側のビット数をそろえるのが本筋です。