Где хранить данные Windows-приложения: таблица решений SQLite / JSON / реестр / Access

· Обновлено: · · SQLite, Windows, .NET, C#, Хранение данных, Реестр, Access, Проектирование, Таблица решений, Техническая консультация

История изменений (1 обновлений, последнее 30 Aug 2026)

Журнал изменений этой статьи. Там, где версия до правки была заархивирована, она остаётся доступной для чтения по постоянной ссылке с DOI.

Русский текст переписан как полноценный технический перевод, а не калька с японского. Утверждения статьи не менялись.
Первая публикация
Цитирование статьи(DOI (зарегистрированный архив): 10.5281/zenodo.21619927)

Приведённые ниже DOI относятся к ранее зарегистрированным архивным версиям, которые могут отличаться от текущего текста. Для ссылки на текущий текст используйте URL этой страницы.

Го Комура (2026). Где хранить данные Windows-приложения: таблица решений SQLite / JSON / реестр / Access. KomuraSoft LLC. https://comcomponent.com/ru/blog/windows-app-local-data-storage-decision-table/

DOI (зарегистрированный архив)
10.5281/zenodo.21619927
DOI (последняя зарегистрированная версия)
10.5281/zenodo.21619928

«Хватит ли INI-файла для настроек?», «История разрослась — хотим сложить её в Access», «Как разделить реестр и файл настроек?». Если вы делаете бизнес-приложение для Windows, выбор, куда складывать данные, почти неизбежен. Часто его делают на старте почти наугад и больше не пересматривают. Через несколько лет это всплывает так: «JSON разросся до десятков мегабайт, запуск тормозит», «Access в общей папке ломается примерно раз в неделю», «пишем прямо в Program Files, на Windows 11 не работает».

В этой статье локальное хранение данных в бизнес-приложении для Windows разбирается двумя отдельными вопросами: «куда класть» (выбор папки) и «в каком виде хранить» (выбор формата и движка). В формате таблицы решений, который уже не раз использовался в этом блоге, собраны сильные стороны и ловушки SQLite, JSON, реестра и Access.

1. Сначала выводы

  • Выбор места хранения — это два независимых решения: «куда класть» и «в каком виде хранить». Ошибка в первом даёт сбои с правами и многопользовательским доступом, ошибка во втором — повреждение, просадки по скорости и дорогую поддержку.
  • Базовое правило расположения: настройки и данные конкретного пользователя — %LOCALAPPDATA% (Environment.SpecialFolder.LocalApplicationData), данные, общие для всех пользователей, — %PROGRAMDATA%, и в папку с exe (внутри Program Files) не писать.1
  • По формату первых кандидатов всего два. Небольшие структурированные настройки — JSON-файл, растущие бизнес-данные, история и всё, что нужно искать, — SQLite. Этими двумя вариантами покрывается большая часть локального хранения в бизнес-приложениях.2
  • Реестр — это «место для мелких флагов и сведений о связи с Windows», а не хранилище данных приложения. Если пользоваться им, не понимая перенаправления реестра 32/64 бит (Wow6432Node), получите проблему «значение вроде записали, а его не видно».3
  • Выбирать Access (.accdb) хранилищем для новой разработки почти незачем. Даже когда к нему обращаются из‑за существующих систем, остаётся ограничение на поставку: разрядность провайдера ACE должна совпадать с разрядностью приложения.4
  • Независимо от формата секреты (пароли, API-ключи) всегда отдельный случай. Не кладите их открытым текстом в JSON или реестр — защищайте через DPAPI. DPAPI (Data Protection API) удобно мыслить как функцию ОС, которой можно отдать управление ключами шифрования. Приложение передаёт открытый текст в ProtectedData.Protect, получает зашифрованные bytes и сохраняет только их. Ключ привязан к вошедшему пользователю (или к конкретной машине) и им управляет ОС, поэтому встраивать ключ в приложение не нужно. Обратная сторона: расшифровать можно только тем же пользователем на той же машине — это базовое свойство, и его нужно учитывать в переносе ПК и в резервном копировании (раздел 6.3).5 Подробности — в статье «Хранение секретов в Windows-приложениях — избегаем настроек в открытом виде с помощью DPAPI».

На схеме сплошная линия обозначает отношение, которое выполняется всегда, а пунктирная — условное отношение (условия указаны в пояснении к каждому отношению на странице сведений). Полный список отношений (всего 22, с доказательствами и степенью уверенности) и определения основных понятий собраны на странице сведений карты знаний (на японском). Данные: JSON-LD / Turtle

2. Четыре типа сохраняемых данных

Прежде чем решать, в каком виде хранить, разберите природу данных. Локальные данные бизнес-приложения обычно делятся на четыре типа.

Тип Примеры Особенности
Настройки Параметры подключения, раскладка экрана, последняя открытая папка Небольшой объём. При запуске читаются целиком. Иногда пользователь хочет править их вручную
Бизнес-данные и история Результаты измерений, история обработки, локальные копии справочников Постоянно растут. Нужны поиск и агрегация. Повреждение сильно бьёт по работе
Кэш Миниатюры, уже скачанные ресурсы Можно заново получить. Нужно следить за объёмом
Секреты Сохранённые пароли, токены Небольшой объём. Открытым текстом класть нельзя

Стержень статьи в том, что у каждого типа свои место и формат. Если у вас «и настройки, и история свалены в один 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;
    }
}

Environment.GetFolderPath вместо склейки строки с %LOCALAPPDATA% нужен потому, что метод возвращает верный путь и при запуске от учётной записи службы, и от другого пользователя, и когда настроено перенаправление папок. Тем самым вы избегаете сбоя, когда путь внезапно меняется, стоит запустить задачу из Планировщика заданий под другой учётной записью (это разновидность проблемы «вручную работает» из главы 5 статьи про Планировщик заданий).

Три ловушки.

  • Не пишите в папку с exe. В Program Files обычному пользователю писать нельзя. В старых 32-битных приложениях совместимость UAC (User Account Control, контроль учётных записей) через виртуализацию файлов может молча перенаправить запись в VirtualStore. Отсюда странный симптом: «файл настроек разный при запуске от администратора и от обычного пользователя».
  • ProgramData «писать можно, но это не значит, что безопасно». При ACL по умолчанию файл, созданный одним пользователем, другой может не суметь изменить. Если нужен общий доступ на чтение и запись для всех пользователей, папку создаёт установщик и явно задаёт ACL.
  • Не делайте Roaming значением по умолчанию. В домене с перемещаемыми профилями содержимое Roaming синхронизируется при входе и выходе. Большие данные или данные, привязанные к машине (кэш, настройки оборудования), в Roaming дают задержки синхронизации и нежелательный перенос на другие компьютеры. Если сомневаетесь — Local.

3.1 Как запись уходит в VirtualStore

Первую ловушку стоит показать схемой. Виртуализация файлов UAC — это совместимость: 32-битное приложение без requestedExecutionLevel в манифесте, которое пытается писать в защищённое место вроде Program Files, не получает отказ. Вместо этого запись подменяют путём внутри профиля пользователя. При чтении виртуализированное место тоже предпочтительнее, поэтому самому автору кажется, что «всё записалось».7

Запуск от имени администратораОбычный пользовательДаНет = 32-бит без повышения правПриложение пишет settings.iniв каталог Program FilesЕсть ли права на записьЗапись идёт в исходное место64-бит или заданrequestedExecutionLevelОтказ в доступе, сбой без подменыСрабатывает виртуализация файлов UACВ VirtualStore под LOCALAPPDATAсоздаётся копия для этого пользователяПри чтении виртуализированная копия важнеесамому автору кажется, что запись удаласьПри запуске от администратора и от обычного пользователячитаются разные файлы

Фактический путь — %LOCALAPPDATA%\VirtualStore\Program Files\.... Виртуализация делает отдельную копию на каждого пользователя, поэтому симптом бывает и таким: «у пользователя A настройки сохраняются, а на общем ПК после входа другого человека всё сбрасывается к значениям по умолчанию». Это временная мера для старых приложений. На 64-битных процессах, на процессах с повышением прав и на процессах с манифестом она не работает.7 Тот же механизм на стороне реестра (HKLM\Software уходит в HKCU\Software\Classes\VirtualStore) разобран в статье «Ловушки перенаправления и виртуализации реестра 32/64 бит».

3.2 Проверьте на реальной машине, что запись проходит

Проектирование расположения не проверяется, пока вы гоняете приложение под администратором на машине разработчика. Перед выпуском пройдите эти три шага.

  1. Запустите под обычным пользователем. Создайте на машине разработчика одну локальную учётную запись обычного пользователя, войдите под ней и пройдите полный цикл: установка → запуск → сохранение → повторный запуск. Если просто снять «Запуск от имени администратора» у учётной записи администратора, вы воспроизведёте отсутствие повышения UAC, но пользователь по-прежнему в группе администраторов — как проверка ACL этого мало.
  2. Посмотрите в Process Monitor, куда идёт запись. Сузьте захват до процесса приложения и до операций CreateFile / WriteFile. Проверьте, нет ли ACCESS DENIED на нужном пути и не появляется ли VirtualStore в столбце Path. ProcMon показывает фактический путь после разрешения перенаправления, поэтому расхождение «куда собирались писать» и «куда записалось» видно напрямую. Как пользоваться инструментом — в «Практическом руководстве по Process Monitor (ProcMon)».
  3. Прочитайте ACL. Командой icacls "C:\ProgramData\KomuraSoft\MyApp" убедитесь, что у задуманных пользователей и групп есть запись. Если ProgramData должна быть общей областью чтения и записи для всех пользователей, проверьте, что ACL, заданный установщиком, здесь действительно виден.

Если эти три шага проходят, по крайней мере сбой вида «сразу после поставки сохранять нельзя» вы отсекаете.

4. В каком виде хранить — четыре варианта

4.1 JSON-файл — первый кандидат для настроек

Через System.Text.Json его просто читать и писать, человек может открыть глазами, удобно держать в Git и смотреть diff — для настроек набор плюсов полный. Два момента, на которые стоит смотреть.

Защититесь от повреждения. Если питание пропадёт во время записи, останется обрезанный файл, и при следующем запуске его не прочитать. Приём по умолчанию — «писать во временный файл, затем подменять основной». В .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 в хранилище данных. Область JSON — размер, при котором работает «прочитать всё при запуске, записать всё при выходе» (ориентир — до нескольких сотен КБ). Как только в JSON начинают класть постоянно дописываемую историю или записи, по которым нужен поиск, это сигнал переходить на SQLite.

4.2 SQLite — первый кандидат для растущих данных и поиска

SQLite — встраиваемая база без сервера, один файл, общественное достояние (public domain). Из .NET её берут через ADO.NET-провайдер Microsoft.Data.Sqlite, который сопровождает Microsoft, либо через SQLite-провайдер EF Core.2 Сама Microsoft рекомендует SQLite как способ хранить локальные данные Windows-приложения8, так что для «локально растущих структурированных данных» SQLite можно рассматривать первым.

Сначала покажем, насколько это просто. Достаточно добавить Microsoft.Data.Sqlite через NuGet: ни сервер настраивать, ни экран строки подключения делать не нужно — указали путь к файлу и можно работать.

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();
}

// Для INSERT обязательны параметры (не собирайте 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, SQLite-провайдер EF Core садится поверх этой же библиотеки.

Дальше — то, что важно на практике.

  • Включите режим WAL. Это PRAGMA journal_mode=WAL; в коде выше. WAL — Write-Ahead Logging (журнал упреждающей записи): изменения не пишут сразу в основной файл базы, а дописывают в соседний файл -wal и позже пакетом переносят в основной. Пишущая сторона только дописывает в конец, поэтому не мешает читающей: чтение и запись идут одновременно.9 Отсюда же меньше затыков, когда UI-поток и фоновая обработка трогают одну и ту же БД. Настройка WAL сохраняется в самом файле базы, выдавать её на каждое подключение не нужно (один раз задали — после закрытия и повторного открытия режим остаётся WAL).9 Побочный эффект: база больше не умещается в один файл — рядом с app.db появляются app.db-wal и app.db-shm. Поэтому резервная копия «скопировали только app.db на ходу» опасна (раздел 6.3).
  • Сведите запись к одному потоку на процесс. Запись в SQLite исключает остальных на уровне всей базы. Если писать нужно из нескольких потоков, безопаснее прогонять запись через очередь к одному писателю. Много мелких INSERT лучше собирать в явную транзакцию: это на порядки быстрее, чем фиксировать каждую строку отдельно.
  • Не кладите базу на сетевой ресурс. Блокировки файлов через SMB сильно зависят от окружения, и сам проект SQLite называет совместное использование на сетевой файловой системе главной причиной повреждения.10 Режим WAL вообще предполагает, что процессы, которые пользуются одной БД, работают на одной машине, и на сетевой файловой системе он не работает (процессам нужна общая память).9 Если понадобился одновременный доступ с нескольких машин или от нескольких пользователей, это уже клиент-серверная СУБД (например, SQL Server Express).
  • Типов по сути четыре. SQLite оперирует INTEGER / REAL / TEXT / BLOB; даты и GUID хранятся как TEXT. Один раз сверьтесь с соглашениями сопоставления типов в Microsoft.Data.Sqlite — и сравнение с сортировкой дат перестанут путать.11 В примере выше created_at заполняется через datetime('now') (UTC) как раз потому, что смесь с локальным временем ломает сортировку и переход на летнее время. Безопаснее переводить в локальное время только при показе.
  • Резервные копии — не копированием файла, а через VACUUM INTO или Backup API. Простое копирование работающего файла БД может схватить расхождение WAL и основного файла (подробнее в главе 6).

4.3 Реестр — только мелкие флаги и связь с Windows

Реестр уместен для сведений о связи с самой Windows — «установлено ли», «регистрация в автозагрузке», «сопоставление типов файлов» — и для совсем мелких пользовательских настроек. Правило: настройки, которыми приложение пользуется само, лежат под HKCU; общесистемные сведения в HKLM пишет установщик (писать в HKLM во время работы не стоит: это потребует прав администратора).

Главная ловушка — разрядность (bitness). На 64-битной Windows раздел HKLM\Software, который видит 32-битный процесс, перенаправляется в HKLM\Software\Wow6432Node.3 Симптомы «в редакторе реестра значение есть, а приложение его не читает» и «значение, записанное 32-битным приложением, не видно из 64-битного инструмента сопровождения» почти всегда об этом. Обычно это всплывает при переходе на AnyCPU или на 64 бита, поэтому держите тот же контекст, что и у 32/64-битных проблем COM и ActiveX (см. «Ловушки регистрации и bitness при разработке COM/OCX/ActiveX»).

Если из .NET всё же нужно прочитать представление другой разрядности (например, приложение, которое по-прежнему сопровождают как 32-битное, читает значение, зарегистрированное на 64-битной стороне), представление задают явно через RegistryView.

using Microsoft.Win32;

// Читаем 64-битное представление HKLM из 32-битного процесса
using var hklm64 = RegistryKey.OpenBaseKey(
    RegistryHive.LocalMachine, RegistryView.Registry64);
using var key = hklm64.OpenSubKey(@"SOFTWARE\KomuraSoft\MyApp");
var installDir = key?.GetValue("InstallDir") as string;

С другой стороны, сама нужда в этом указании — признак, что решение «в какую разрядность правильно писать, 32 или 64» отложили. Правильный путь — согласовать разрядность пишущей и читающей стороны.

Данные больше нескольких килобайт или массивоподобные структуры в реестре невыгодны и для резервного копирования, и для переноса, и для диагностики. Это задача файлов (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) — поставка становится тяжелее.12

Тем не менее Access встречается: обмен данными с существующей бизнес-системой на Access, чтение справочников, сделанных в Access. Тогда разумный компромисс такой:

  • зафиксировать разрядность процесса, который читает и пишет (на практике чаще реалистично зафиксировать x86), и в установщике проверять наличие соответствующего ACE
  • не закладывать одновременную запись многих людей в .accdb в общей папке (стоимость восстановления после поломки этого не стоит)
  • заранее иметь путь миграции на SQLite или серверную СУБД

Обращение с существующими решениями, включая Excel/VBA, разобрано и в «Что такое VBA — ограничения, перспективы, когда стоит заменить и как мигрировать».

5. Таблица решений

5.1 Сводка «тип × формат»

Сначала таблица, которая сводит четыре типа из главы 2 с четырьмя форматами из главы 4. По одной строке видно: «мои данные этого типа, значит этот формат и это расположение».

Тип (глава 2) JSON-файл SQLite Реестр Access Расположение по умолчанию (глава 3)
Настройки ◎ первый кандидат ○ если будут расти — сразу сюда △ только мелкие флаги и связь с Windows %LOCALAPPDATA% (Roaming — только если настройки должны следовать за пользователем)
Бизнес-данные и история ✕ ломается предпосылка «читать всё» ◎ первый кандидат △ только связь с существующим Access %LOCALAPPDATA% (общее для всех пользователей — %PROGRAMDATA% плюс проект ACL)
Кэш △ только мелкое ○ если записей много %LOCALAPPDATA% (не в Roaming)
Секреты ○ как контейнер для значения, защищённого DPAPI ○ то же △ то же, но только мелкое Открытым текстом не класть. Защищать DPAPI (глава 1)

Читать таблицу можно двумя способами. Во-первых, не сваливать разные строки в один контейнер. «Настройки, история и кэш в одном JSON» — типичная конструкция, которая аукается позже. Во-вторых, в строке секретов важнее не «в какой формат класть», а «зашифровано ли через DPAPI до записи»; сам контейнер можно выбрать по остальным трём типам.

5.2 Свойства форматов

Критерий JSON-файл SQLite Реестр Access (.accdb)
Хорошо подходит для Небольших настроек Растущих структурированных данных, поиска и агрегации Мелких флагов, связи с Windows Связи с существующим Access
Ориентир по объёму до нескольких сотен КБ до десятков ГБ до нескольких КБ до 2 ГБ (предел по спецификации)
Поиск и агрегация ✕ (нужно читать всё) ◎ (SQL) ○ (SQL)
Человек читает напрямую △ (нужен инструмент) △ (нужен Access)
Устойчивость к повреждению △ (защиту пишете сами) ○ (транзакции)
Одновременный доступ из нескольких процессов ○ (на одной машине)
Общий доступ с нескольких машин ✕ (по факту)
Дополнительное в поставке Нет Нет (идёт через NuGet) Нет Нужен провайдер ACE

Как видно из последней строки, ни одна технология локального хранения не рассчитана на «общий доступ с нескольких машин». Общая папка создаёт иллюзию общего доступа, но у JSON нет взаимного исключения, блокировки SQLite по SMB ненадёжны, Access рано или поздно упирается в предел вместе с риском повреждения. Если одни и те же данные должны трогать несколько площадок или несколько пользователей, считайте это границей, за которой ставят серверную СУБД вроде SQL Server Express или Web API.

6. Не ломается, переносится, восстанавливается — общее проектирование независимо от формата

Какой бы формат вы ни выбрали, при эксплуатации несколько лет понадобятся три решения. Включить их уже в первый выпуск или нет — сильно меняет стоимость сопровождения.

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();
}

Это минимальная миграция: при запуске смотрим версию и применяем только разницу. Отказ от версии «новее себя» в начале нужен затем, чтобы при откате приложения на старую версию старый код не писал в незнакомую схему и не ломал данные. С JSON идея та же: в корне поле "version": 2, при чтении — преобразование из старых форматов, слишком новый формат отклонять. «Не выпускайте формат данных без номера версии» — одного этого правила хватит, чтобы выручить себя в будущем.

6.2 Заранее решите, как вести себя при повреждении

В главе 4 мы уже касались защиты от повреждения для каждого формата (атомарная запись для JSON, транзакции для SQLite), но «данные, которые нельзя прочитать», всё равно встретятся. Сбой диска, карантин из‑за ложного срабатывания антивируса, правка файла пользователем. Если заранее не решить, как в этот момент должно вести себя приложение, оно может вообще не запуститься.

  • настройки не читаются → запуститься со значениями по умолчанию и сказать об этом пользователю (если молча подставить значения по умолчанию, придут обращения «пропали настройки»)
  • бизнес-данные не читаются → в режиме только чтения или на экране ошибки показать, какой именно файл повреждён. Не чинить автоматической перезаписью (исчезнут улики)
  • есть резервная копия → предложить восстановление. Автоматическое восстановление неотделимо от риска «ложно решить, что данные повреждены, и откатиться к старым», поэтому между ними по умолчанию должно быть действие пользователя

6.3 Резервные копии: важнее не «сняты ли», а «можно ли вернуться»

В отличие от серверной базы, локальные данные никто за вас не копирует. Если этим занимается приложение, нужно решить три вещи.

  • Что: бизнес-данные — да, кэш — нет; для секретов учитывать, что из‑за свойств DPAPI расшифровать можно только тем же пользователем на той же машине (перенос на другой ПК — отдельная процедура)
  • Когда и куда: при запуске или ежедневно, с поколениями, в папку backup внутри %LOCALAPPDATA%. Класть ли копии ещё в общую папку или в каталог, который уже входит в резервное копирование ПК, — вопрос к эксплуатации
  • Как: для 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 поколений, остальное удалить».

И хотя бы раз проведите репетицию восстановления. Классика бизнес-систем: файлы резервных копий есть, а как ими вернуться — никто не знает и никто не пробовал. Если написать инструкцию переноса данных на новый ПК при замене железа, обычно сразу находятся дыры в проектировании копий (учётные данные, защищённые DPAPI, не переезжают; путь содержит имя пользователя и ломается под другой учётной записью). Как стирать данные при утилизации ПК — в «Что сделать перед утилизацией Windows PC — практический чек-лист по стиранию данных, отвязке учётных записей и резервному копированию».

7. Ориентиры для спорных случаев

  • «Это настройки, но они, похоже, разрастутся» — если схема «читать всё при запуске» скорее всего сломается, сразу берите SQLite. Завести в SQLite таблицу settings — нормально.
  • «Миграция с INI/XML» — если меняется только формат, идите в JSON; если к этому моменту туда же примешана история, отделите её в SQLite. Если на стороне чтения оставить откат к старому формату на одну-две версии, переход безопаснее.
  • «Просят смотреть в Excel» — не делайте хранилищем Excel или Access. Храните в SQLite и добавьте выгрузку в CSV/Excel: так закрываются и надёжность данных, и сама просьба. Как строить вывод отчётов — в «Как построить вывод отчётов Excel: COM / Open XML / шаблоны».
  • «Несколько процессов хотят читать и писать один файл» — на одной машине SQLite (WAL) закрывает немало случаев, но конфликты записи всё равно нужно спроектировать. Если связь идёт через файлы, используйте схемы взаимного исключения из «Интеграция через файлы: блокировки, атомарный claim и практические рекомендации».
  • «Нужен общий доступ с нескольких машин» — это уже выход из локального хранения. Первый кандидат — клиент-серверная схема с SQL Server Express (бесплатно, база до 10 ГБ) на машине, которая играет роль файлового сервера. SQL Server «LocalDB» вопреки имени — однопользовательская среда для разработки, для общего доступа её не берите. Если нужны несколько площадок или доступ извне компании, это граница, на которой стоит рассмотреть схему с Web API.

8. Итог

Выбор места хранения почти всегда решается, если разделить его на «куда класть» (LocalAppData / ProgramData, и не в Program Files) и «в каком виде хранить» (настройки — JSON, растущие данные — SQLite, реестр по минимуму, Access только для связи с существующими системами).

Поверх этого, независимо от формата, в первый же выпуск стоит заложить три пункта главы 6: номер версии формата, запасной сценарий при повреждении, резервные копии, из которых можно вернуться. Секреты всегда отдельно, через DPAPI. Если держать таблицу решений и это общее проектирование, почти наверняка не получите дорогих позже конструкций вроде «JSON на десятки мегабайт» или «общий Access, который ломается раз в неделю». Если способ хранения в существующем приложении вызывает сомнения, начните с инвентаризации: что и куда сейчас пишется.

Похожие статьи

Смежные направления консультаций

ООО «КомураСофт» занимается пересмотром способа хранения данных бизнес-приложений (включая проектирование перехода с INI/XML/Access) и разбором причин повреждения данных и падения производительности.

Справочные материалы

  1. Microsoft Learn, KNOWNFOLDERID. Определения известных папок Windows: LocalAppData, RoamingAppData, ProgramData и других. 

  2. Microsoft Learn, Microsoft.Data.Sqlite overview. Обзор ADO.NET-провайдера SQLite, который сопровождает Microsoft, и того, что он лежит в основе SQLite-провайдера EF Core.  2

  3. Microsoft Learn, Registry Redirector. Как на 64-битной Windows доступ 32-битного процесса к реестру перенаправляется в Wow6432Node.  2

  4. Microsoft Learn, Can’t establish a connection to Access Database Engine OLE DB. О том, что разрядность провайдера ACE OLE DB должна совпадать с разрядностью обращающегося процесса.  2

  5. Microsoft Learn, DataProtectionScope Enum. Область защиты, которую передают в ProtectedData.Protect / Unprotect. При CurrentUser расшифровать может только поток в контексте текущего пользователя; при LocalMachine расшифровать может любой процесс на этом компьютере, поэтому так делают, только если всем учётным записям этой машины можно доверять, а в большинстве случаев следует брать CurrentUser

  6. Microsoft Learn, Environment.SpecialFolder Enum. Перечисление, через которое .NET получает известные папки. 

  7. Microsoft Learn, UAC Architecture. Как виртуализация файлов и реестра UAC перенаправляет запись машинного уровня в расположение пользователя и при чтении предпочитает виртуализированное место; как для записи в защищённые папки вроде Program Files используется копия внутри профиля, своя у каждого пользователя; что виртуализация рассчитана только на 32-битные приложения и выключена у процессов с повышением прав и у приложений с манифестом, в котором задан requestedExecutionLevel (у 64-битного приложения без повышения будет отказ в доступе); и что это временная мера совместимости, на которую не стоит опираться.  2

  8. Microsoft Learn, Use a SQLite database in a Windows app. Официальный учебник, который рекомендует SQLite вместе с Microsoft.Data.Sqlite / EF Core для локальных данных Windows-приложения. 

  9. SQLite, Write-Ahead Logging. Как изменения дописываются не в основной файл, а в WAL-файл; почему пишущая сторона только дописывает и поэтому может работать одновременно с читающей; что journal_mode=WAL сохраняется и остаётся после повторного открытия; что рядом появляются файлы -wal и -shm; и что процессы, которые пользуются одной базой, должны быть на одной машине — на сетевой файловой системе WAL не работает.  2 3

  10. SQLite, How To Corrupt An SQLite Database File. О том, что сбои блокировок на сетевой файловой системе — одна из главных причин повреждения базы. 

  11. Microsoft Learn, Data types (Microsoft.Data.Sqlite). Четыре примитивных типа SQLite и соглашение, по которому DateTime и Guid отображаются на TEXT. 

  12. Microsoft, Microsoft Access Database Engine 2016 Redistributable. Распространяемый пакет ACE (32-бит/64-бит) для доступа к .accdb / .mdb. 

Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.

Как создать и эксплуатировать службу Windows — от выбора между Планировщиком заданий и службой до превращения BackgroundService в службу

Стоит ли держать постоянно работающую обработку как службу Windows или хватит Планировщика заданий. Практический разбор: таблица выбора, ...

Эти страницы показывают тему статьи в более широком контексте услуг и решений.

Статья напрямую связана со следующими услугами.

Частые вопросы

Вопросы, которые часто возникают при консультациях по теме статьи.

Куда сохранять файл настроек Windows-приложения?
Для настроек и данных конкретного пользователя базовый вариант — иерархия «Компания\Приложение» внутри %LOCALAPPDATA% (Environment.SpecialFolder.LocalApplicationData). Для данных, общих для всех пользователей, — %PROGRAMDATA%, но при ACL по умолчанию другой пользователь может не суметь изменить файл, поэтому ACL задают явно в установщике. В ту же папку, что и exe (внутри Program Files), писать нельзя: обычному пользователю запись туда запрещена, а в старых 32-битных приложениях это даёт странный симптом — молчаливое перенаправление в VirtualStore.
Настройки хранить в JSON или в SQLite?
Для небольших структурированных настроек первый кандидат — JSON-файл, для растущих бизнес-данных, истории и всего, что нужно искать, — SQLite. Этими двумя вариантами покрывается большая часть локального хранения в бизнес-приложениях. JSON уместен, пока работает схема «прочитать всё при запуске, записать всё при выходе» — ориентир до нескольких сотен КБ. Если в JSON начинают класть постоянно дописываемую историю или записи, по которым нужен поиск, это сигнал переходить на SQLite. Если даже настройки, скорее всего, разрастутся, нормально сразу завести в SQLite таблицу settings.
Можно ли класть базу SQLite в общую сетевую папку?
Лучше не стоит. Блокировки файлов через SMB сильно зависят от окружения, и сам проект SQLite называет совместное использование на сетевой файловой системе главной причиной повреждения базы. У JSON нет взаимного исключения, Access тоже упирается в предел вместе с риском повреждения — ни одна технология локального хранения не рассчитана на доступ с нескольких машин. Если одни и те же данные должны трогать несколько площадок или несколько пользователей, это уже граница, за которой ставят серверную СУБД вроде SQL Server Express (бесплатно, база до 10 ГБ) или Web API.
Можно ли хранить данные приложения в реестре?
Реестр уместен для сведений о связи с самой Windows — регистрация в автозагрузке, сопоставление типов файлов — и для совсем мелких пользовательских настроек. Данные больше нескольких килобайт или массивоподобные структуры невыгодны и для резервного копирования, и для переноса, и для диагностики, поэтому их отдают JSON или SQLite. Кроме того, на 64-битной Windows раздел HKLM\Software, который видит 32-битный процесс, перенаправляется в Wow6432Node: отсюда симптом «в редакторе реестра значение есть, а приложение его не читает». Правильный путь — согласовать разрядность пишущей и читающей стороны.

Об авторе

Страница с профилем автора статьи.

Го Комура

Представитель KomuraSoft LLC

Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.

Публичные ссылки

Вернуться в блог