Как обрабатывать изменяющиеся структуры данных при обновлении версии программы?

Я использую встроенное программное обеспечение, но на самом деле это не вопрос встраивания. Я не (по техническим причинам не могу) использовать базу данных, такую ​​как MySQL, только C или С++ -структуры.

Существует ли общая философия, как обрабатывать изменения в структуре этих структур от версии до версии программы?

Возьмем адресную книгу. Из версии программы x в x + 1, что если:

  • поле удаляется (кажется достаточно простым) или добавлено (нормально, если все могут использовать некоторые новые значения по умолчанию)?
  • строка становится длиннее или короче? Int идет от 8 до 16 бит подписанного /unsigned?
  • Возможно, я совмещаю фамилию/имя файла или разделяю имя на два поля?

Это лишь некоторые простые примеры; Я не ищу ответы на них, а скорее для общего решения.

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

Что делать, если кто-то не обновляется с версии x до x + 1, но ждет x + 2? Должен ли я попытаться объединить изменения или просто применить x → x + 1, а затем x + 1 → x + 2?

Что делать, если версия x + 1 ошибочна, и нам нужно вернуться к предыдущей версии s/w, но уже "обновили" структуры данных?

Я склоняюсь к TLV (http://en.wikipedia.org/wiki/Type-length-value), но может видеть много потенциальных головных болей.

Это ничего нового, поэтому я просто задавался вопросом, как это делают другие.

Ответ 1

У меня есть некоторый код, где более длинная строка озадачена вместе с двумя более короткими сегментами, если это необходимо. Тьфу. Здесь мой опыт после 12 лет хранения некоторых данных:

Определите свои цели - есть два:

  • новые версии должны иметь возможность читать, какие старые версии пишут
  • старые версии должны иметь возможность читать, что новые версии пишут (сложнее)

Добавить поддержку версии для выпуска 0. По крайней мере, напишите заголовок версии. Вместе с хранением (потенциально много) старого кода читателя, который может примитивно решить первый случай. Если вы не хотите реализовать случай 2, начните отклонять новые данные прямо сейчас!

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

Преобразование во время сериализации - во время выполнения сохраняйте данные только в "новом формате" в памяти. Делайте необходимые преобразования и тесты в пределах персистентности (конвертируйте в новейшие при чтении, реализуйте обратную совместимость при написании). Это изолирует проблемы версий в одном месте, что помогает избежать ошибок с ошибками.

Сохраняйте набор тестовых данных из всех версий.

Сохранить подмножество доступных типов - ограничить фактически сериализованные данные несколькими типами данных, такими как int, string, double. В большинстве случаев размер дополнительного хранилища сводится к уменьшению размера кода, поддерживающего изменения этих типов. (Тем не менее, это не всегда компромисс, который вы можете сделать во встроенной системе).

например. не сохраняйте целые числа короче, чем собственный. (вам может понадобиться сделать это, когда вам нужно хранить массивы с длинными целыми числами).

добавить прерывателя - сохранить некоторый ключ, который позволяет преднамеренно превратить старый код в сообщение об ошибке, что эти новые данные несовместимы. Вы можете использовать строку, которая является частью сообщения об ошибке, - тогда ваша старая версия может отображать сообщение об ошибке, о котором она не знает - "вы можете импортировать эти данные с помощью инструмента ConvertX с нашего веб-сайта" не очень хорошо в локализованной но все же лучше, чем "Ungültiges Format".

Не сериализуйте структуры напрямую - это логическое/физическое разделение. Мы работаем с двумя группами, оба имеют свои плюсы и минусы. Ни одно из них не может быть реализовано без каких-либо издержек во время выполнения, что в значительной степени ограничит ваш выбор во встроенной среде. Во всяком случае, не используйте фиксированные длины массива/строки во время сохранения, которые уже должны решить половину ваших проблем.

(A) правильный механизм сериализации - мы используем сериализатор bianry, который позволяет запускать "кусок" при хранении, у которого есть собственный заголовок длины. При чтении лишние данные пропускаются, а отсутствующие данные инициализируются по умолчанию (что значительно упрощает реализацию "чтения старых данных" в коде serializationj.) Куски могут быть вложенными. Это все, что вам нужно с физической стороны, но для покрытия общих задач требуется некоторое покрытие сахара.

(B) использовать другое представление в памяти - перепечатка в памяти в основном может быть map<id, record>, где id woukld вероятно является целым числом, а record может быть

  • пустой (не сохраняется)
  • примитивный тип (строка, целое число, double - чем меньше вы используете, тем легче он получает)
  • массив примитивных типов
  • и массив записей

Я изначально написал, что так ребята не спрашивают меня о каждом вопросе совместимости формата, и хотя реализация имеет много недостатков (хотелось бы, чтобы я узнал проблему с ясностью сегодняшнего дня...), она могла решить

Запрос не существующего значения по умолчанию будет возвращать значение по умолчанию/ноль. когда вы помните об этом при доступе к данным и при добавлении новых данных, это очень помогает: предположим, что версия 1 будет автоматически вычислять длину foo, тогда как в версии 2 пользователь может переопределить эту настройку. Значение нуля - в "типе расчета" или "длине" должно означать "вычислять автоматически", и вы устанавливаете.

Ниже приведены сценарии "изменения", которые вы можете ожидать:

  • флаг (да/нет) распространяется на перечисление ( "да/нет/авто" )
  • параметр разбивается на две настройки (например, "добавить границу" можно разделить на "добавить границу на четные дни" / "добавить границу в нечетные дни".)
  • добавлена ​​настройка, переопределяющая (или, что еще хуже, расширяющая) существующую настройку.

Для реализации случая 2 вам также необходимо рассмотреть:

  • никакое значение никогда не может быть восстановлено или заменено другим. (Но в новом формате он может сказать "не поддерживается", а новый элемент добавлен)
  • перечисление может содержать неизвестные значения, другие изменения допустимого диапазона

уф. это было много. Но это не так сложно, как кажется.

Ответ 2

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

Он назвал разбиение архитектуры на "логические" и "физические".

Ваши структуры являются как логическим, так и физическим слоем, объединенным в сложную вещь.

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

Вам не нужно повторно запускать SQL для выполнения этого.

Если ваши данные полностью хранятся в памяти, подумайте об этом. Разведите физическое представление файла из представления в памяти. Напишите данные в некотором "родовом", гибком, удобном для анализа формате (например, JSON или YAML). Это позволяет вам читать в общем формате и создавать свои высококонвертированные структуры в памяти.

Если ваши данные синхронизированы с файловой системой, у вас есть больше работы. Опять же, посмотрите на конструкторскую идею РСУБД.

Не кодируйте простой безмозглый struct. Создайте "запись", которая отображает имена полей в значения полей. Это связанный список пар имя-значение. Это легко расширяемо для добавления новых полей или изменения типа данных значения.

Ответ 3

Некоторые простые рекомендации, если вы говорите о структуре, как в C API:

  • имеет поле размера структуры в начале структуры - таким образом, код, использующий структуру, всегда может гарантировать, что они имеют дело только с достоверными данными (например, многие из структур, которые использует API Windows, начинаются с поля cbCount, поэтому эти API-интерфейсы могут обрабатывать вызовы, выполненные с помощью кода, составленного из старых SDK или даже более новых SDK, которые добавили поля
  • Никогда не удаляйте поле. Если вам не нужно использовать его больше, это одна вещь, но для того, чтобы сохранить смысл для работы с кодом, который использует более старую версию структуры, не удаляйте это поле.
  • может быть целесообразным включить поле номера версии, но часто поле счетчика может использоваться для этой цели.

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

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

И если структура будет пересмотрена дальше, а новое изображение использует новый формат структуры на устройстве со старшим загрузчиком, этот загрузчик также сможет справиться с ним - он ничего не сделает с улучшениями. Но поскольку никакие поля не были удалены (или будут удалены), старший загрузчик сможет выполнять все, что он был предназначен, и делать это с новым изображением, имеющим структуру конфигурации с более новой информацией.

Если вы говорите о фактической базе данных с метаданными о полях и т.д., то эти рекомендации действительно не применяются.

Ответ 4

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

struct address_book
{
  unsigned int length; // total length of this struct in bytes
  char items[0];
}

где "items" - массив переменной длины структуры, который описывает свой собственный размер и тип

struct item
{
  unsigned int size; // how long data[] is
  unsigned int id;   // first name, phone number, picture, ...
  unsigned int type; // string, integer, jpeg, ...
  char data[0];
}

В вашем коде вы выполняете итерацию по этим элементам (address_book- > length сообщит вам, когда вы попали в конец) с помощью некоторого интеллектуального кастинга. Если вы нажмете элемент, чей идентификатор вы не знаете или чей тип вы не знаете, как обращаться с ним, просто пропустите его, перепрыгнув через эти данные (из item- > size) и перейдите к следующему. Таким образом, если кто-то изобрел новое поле данных в следующей версии или удалит его, ваш код сможет его обработать. Ваш код должен иметь возможность обрабатывать конверсии, которые имеют смысл (если идентификатор сотрудника перешел от целого к строке, он, вероятно, должен обрабатывать его как строку), но вы обнаружите, что эти случаи довольно редки и часто могут обрабатываться с помощью общего кода.

Ответ 5

Я занимался этим в прошлом в системах с очень ограниченными ресурсами, выполняя перевод на ПК в рамках процесса обновления s/w. Можете ли вы извлечь старые значения, перевести на новые значения и затем обновить базу данных на месте?

Для упрощенного встроенного db я обычно не ссылаюсь на какие-либо структуры напрямую, но привожу очень легкий API-интерфейс по всем параметрам. Это позволяет вам изменить физическую структуру ниже API без влияния на приложение более высокого уровня.

Ответ 6

В последнее время я использую bencoded. Это формат, который использует bittorrent. Простой, вы можете легко проверить его визуально, так что легче отлаживать, чем двоичные данные, и плотно упакованы. Я заимствовал некоторый код из высококачественного С++ libtorrent. Для вашей проблемы это так просто, как проверка того, что поле существует, когда вы их читаете. И для сжатого файла gzip это так просто, как делать:

ogzstream os(meta_path_new.c_str(), ios_base::out | ios_base::trunc);
Bencode map(Bencode::TYPE_MAP);
map.insert_key("url", url.get());
map.insert_key("http", http_code);
os << map;
os.close();

Чтобы прочитать его:

igzstream is(metaf, ios_base::in | ios_base::binary);
is.exceptions(ios::eofbit | ios::failbit | ios::badbit);
try {
   torrent::Bencode b;
   is >> b;
   if( b.has_key("url") )
      d->url = b["url"].as_string();
} catch(...) {
}

Я использовал формат Sun XDR в прошлом, но я предпочитаю это сейчас. Кроме того, это намного проще читать с другими языками, такими как perl, python и т.д.

Ответ 7

Вставьте номер версии в структуру или, сделайте как Win32, и используйте параметр размера.
если прошедшая структура не является последней версией, тогда исправьте структуру.

Около 10 лет назад я написал аналогичную систему для вышесказанного для игровой системы сохранения компьютерных игр. Я фактически сохранил данные класса в отдельном файле описания классов и, если я заметил несоответствие номера версии, я купил пробег через файл описания класса, обнаружил класс и затем обновил двоичный класс на основе описания. Это, очевидно, обязательные значения по умолчанию, которые должны быть заполнены в новых элементах членов класса. Он работал очень хорошо, и его можно было использовать для автоматического создания файлов .h и .cpp.

Ответ 8

Я согласен с S.Lott в том, что лучшим решением является разделение физических и логических уровней того, что вы пытаетесь сделать. Вы по существу комбинируете свой интерфейс и свою реализацию в один объект/структуру, и при этом вы упускаете какую-то силу абстракции.

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

1) Требуется какое-то поле номера версии. Если ваша структура меняется, вам понадобится простой способ взглянуть на нее и узнать, как ее интерпретировать. Вдоль этих же строк иногда полезно иметь общую длину структуры, хранящейся в каком-либо структурном поле.

2) Если вы хотите сохранить обратную совместимость, вы захотите запомнить, что код будет внутренне ссылаться на структурные поля как смещения от базового адреса структуры (от "фронта" структуры). Если вы хотите не ломать старый код, обязательно добавьте все новые поля в обратную структуру и оставьте все существующие поля неповрежденными (даже если вы их не используете). Таким образом, старый код сможет получить доступ к структуре (но не будет забывать о дополнительных данных в конце), и новый код будет иметь доступ ко всем данным.

3) Поскольку ваша структура может меняться, не полагайтесь на sizeof(struct myStruct), чтобы всегда получать точные результаты. Если вы следуете за № 2 выше, то вы увидите, что вы должны предположить, что структура может расти в будущем. Вызовы sizeof() вычисляются один раз (во время компиляции). Использование поля "длина структуры" позволяет убедиться, что когда вы (например) memcpy структурируете всю структуру, включая любые дополнительные поля в конце, о которых вы не знаете.

4) Никогда не удалять и не сжимать поля; если они вам не нужны, оставьте их пустыми. Не изменяйте размер существующего поля; если вам нужно больше места, создайте новое поле как "длинную версию" старого поля. Это может привести к проблемам с дублированием данных, поэтому обязательно подумайте о своей структуре и попробуйте спланировать поля, чтобы они были достаточно большими для обеспечения роста.

5) Не храните строки в структуре, если не знаете, что их можно ограничить определенной фиксированной длиной. Вместо этого сохраните только указатель или индекс массива и создайте объект хранения строк для хранения строковых данных переменной длины. Это также помогает защитить от переполнения буфера строк, перезаписывая остальную часть ваших данных структуры.

Несколько встроенных проектов, над которыми я работал, использовали этот метод для изменения структур без нарушения совместимости в обратном направлении. Он работает, но это далеко не самый эффективный метод. Вскоре вы теряете пространство с устаревшими/забытыми структурами, дублирующимися данными, данными, которые хранятся по частям (первое слово здесь, второе слово) и т.д. Если вы вынуждены работать в рамках существующей структуры, это может работа для вас. Однако абстрагирование вашего представления физических данных с использованием интерфейса будет намного более мощным/гибким и менее расстраивающим (если у вас есть свобода дизайна для использования такой техники).