Как выбрать место хранения данных Windows-приложения — таблица решений для SQLite / JSON / реестра / Access

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

«Достаточно ли 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. Подробности — в отдельной статье «Защита конфиденциальной информации в Windows-приложениях — как избежать хранения настроек в открытом виде с помощью DPAPI».

2. Классификация данных на четыре типа

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

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

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

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

Перечислим три подводных камня.

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

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

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

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

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

SQLite — встраиваемая база данных, не требующая сервера, работающая с единственным файлом и распространяемая как общественное достояние (public domain); из .NET её можно использовать через ADO.NET-провайдер Microsoft.Data.Sqlite, который поддерживает сама Microsoft, либо через SQLite-провайдер EF Core.2 Сама Microsoft рекомендует SQLite как способ хранения локальных данных в Windows-приложениях6, так что для «локально растущих структурированных данных» вполне уместно в первую очередь рассматривать 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();
}

// Для вставки обязательны параметры (не собирайте 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; в коде выше. Он повышает параллелизм чтения и записи, поэтому конфигурация, где к одной и той же БД обращаются UI-поток и фоновая обработка, реже упирается в блокировки. Настройка WAL сохраняется в самом файле базы данных, поэтому её не нужно выставлять при каждом подключении.
  • Сводите запись к одному потоку на процесс. Запись в SQLite эксклюзивна на уровне базы данных. Если писать нужно из нескольких потоков, безопаснее спроектировать это так, чтобы запись шла через очередь к единственному «писателю». Также, если выполняется много мелких INSERT-ов, объединение их в явную транзакцию ускоряет работу на порядки по сравнению с фиксацией каждой записи по отдельности.
  • Не размещайте базу на сетевом ресурсе. Блокировки файлов через SMB часто зависят от окружения и нестабильны, и сам проект SQLite называет совместное использование по сетевой файловой системе главной причиной повреждения данных.7 Как только понадобится одновременный доступ с нескольких машин или от нескольких пользователей, это уже область клиент-серверных СУБД (например, SQL Server Express).
  • Учтите, что типов всего четыре. По сути 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 во время выполнения не стоит, поскольку это потребует прав администратора).

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

Если из .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) тоже увеличивает объём того, что приходится поставлять.9

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

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

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

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

Критерий 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-компьютера».

7. Рекомендации для неоднозначных случаев

  • «Это настройки, но, похоже, в будущем их станет больше» — если есть вероятность, что подход «читать всё при запуске» перестанет работать, с самого начала выбирайте SQLite. Создать в SQLite таблицу «settings» — совершенно нормальное решение.
  • «Миграция с INI/XML» — если это просто замена формата, переходите на JSON; если на этом этапе туда же примешаны данные истории, отделите их и перенесите в SQLite. Если оставить на стороне чтения откат к старому формату на одну-две версии, миграция пройдёт безопаснее.
  • «Просят посмотреть в Excel» — вместо того чтобы делать хранилищем Excel или Access, лучше хранить данные в SQLite и добавить функцию экспорта в CSV/Excel — это удовлетворяет и требованиям к надёжности данных, и самому запросу. О том, как реализовать вывод отчётов, см. «Как реализовать вывод отчётов Excel — таблица решений для COM-автоматизации / Open XML / шаблонного подхода».
  • «Хотим, чтобы несколько процессов читали и писали в один и тот же файл» — в пределах одной машины с этим неплохо справляется SQLite (WAL), но нужно спроектировать разрешение конфликтов записи. Если взаимодействие идёт через файлы, используйте паттерны исключительного доступа из статьи «Лучшие практики файлового взаимодействия и блокировок».
  • «Хотим общий доступ с нескольких машин» — это уже выход за рамки локального хранения. Первый вариант — клиент-серверная схема с 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. О механизме, при котором доступ 32-битного процесса к реестру перенаправляется в Wow6432Node на 64-битной Windows.  2

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

  5. Microsoft Learn, Environment.SpecialFolder Enum. О перечислении, используемом для получения известных папок из .NET. 

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

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

  8. Microsoft Learn, Data types (Microsoft.Data.Sqlite). О четырёх примитивных типах SQLite и соглашении о сопоставлении DateTime и Guid с типом TEXT. 

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

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

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

Разбираем, стоит ли превращать резидентную обработку в службу 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, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.

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

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