如何在程序版本更新时处理不断变化的数据结构?

我做嵌入式软件,但我认为这不是一个嵌入式问题。 我不(不能出于技术原因)使用像MySQL这样的数据库,只是C或C ++结构。

是否存在如何处理从程序版本到版本的这些结构的布局变化的一般原理?

我们来一本地址簿。 从程序版本x到x + 1,如果:

  • 一个字段被删除(看起来很简单)或添加(确定如果所有可以使用一些新的默认值)?
  • 字符串变长还是变短? 一个int从8位到16位有符号/无符号?
  • 也许我将姓氏/名字或分割名称合并为两个字段?

这只是一些简单的例子; 我不是在寻找那些答案,而是寻找通用的解决方案。

显然,我需要一些硬编码逻辑来处理每个变化。

如果有人不从版本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 ),但可以看到很多潜在的麻烦。

这不是什么新鲜事,所以我只是想知道别人怎么做……

我确实有一些代码,如果有必要,可以从两个较短的段中将较长的字符串拼凑在一起。 呸。 这是我保持一些数据兼容12年后的经验:

确定目标 – 有两个:

  • 新版本应该能够读取旧版本编写的内容
  • 旧版本应该能够读取新版本的内容(更难)

添加版本支持到版本0 – 至少编写版本标头。 保持(可能很多)旧的阅读器代码可以原始地解决第一种情况。 如果您不想实施案例2,请立即开始拒绝新数据!

如果您只需要案例1,并且随着时间的推移而预期的变化相当小,那么您就可以了。 无论如何,在第一次发布之前完成的这两件事可以为您节省许多麻烦。

在序列化期间进行转换 – 在运行时,仅将数据保留在内存中的“新格式”中。 在持久性限制下进行必要的转换和测试(在读取时转换为最新版本,在编写时实现向后兼容性)。 这将版本问题隔离在一个地方,有助于避免难以追踪的错误。

保留所有版本的一组测试数据。

存储可用类型的子集 – 将实际序列化数据限制为几种数据类型,例如int,string,double。 在大多数情况下,额外的存储大小由减少的代码大小组成,支持这些类型的更改。 (但这并不总是可以在嵌入式系统上进行权衡)。

例如,不要存储短于原始宽度的整数。 (当您需要存储长整数数组时, 可能需要这样做)。

添加一个断路器 – 存储一些密钥,允许您故意使旧代码显示一条错误消息,表明此新数据不兼容。 您可以使用作为错误消息一部分的字符串 – 然后您的旧版本可能会显示它不知道的错误消息 – “您可以使用我们网站上的ConvertX工具导入此数据”在本地化方面不是很好应用但仍然比“Ungültiges格式”更好。

不要直接序列化结构 – 这是逻辑/物理分离。 我们合二为一,两者各有利弊。 如果没有一些运行时开销,这些都无法实现,这几乎可以限制您在嵌入式环境中的选择。 无论如何,在持久性期间不要使用固定的数组/字符串长度,这应该已经解决了一半的麻烦。

(A)一个正确的序列化机制 – 我们使用一个bianry序列化器,它允许在存储时启动一个“块”,它有自己的长度头。 在读取时,会跳过额外的数据,并且缺少数据的默认初始化(这简化了序列化代码中大量实现“读取旧数据”。)块可以嵌套。 这就是你在物理方面所需要的一切,但需要一些糖衣来完成常见任务。

(B)使用不同的内存中表示内存中的表示基本上可以是map ,其中id woukld可能是一个整数,并且record可以是

  • 空(未存储)
  • 一个原始类型(字符串,整数,双 – 你用得越少越容易)
  • 一组原始类型
  • 和记录数组

我最初写道,因此这些人不会问我每个格式兼容性问题,虽然实现有许多缺点(我希望我能认识到今天的清晰度问题……)它可以解决

查询非现有值将默认返回默认/零初始化值。 当你在访问数据时记住这一点,并且在添加新数据时,这有很大帮助:想象版本1将自动计算“foo长度”,而在版本2中,用户可以覆盖该设置。 值为零 – 在“计算类型”或“长度”中应表示“自动计算”,并且您已设置。

以下是您可以预期的“更改”方案:

  • 标志(是/否)扩展为枚举(“是/否/自动”)
  • 设置分为两个设置(例如,“添加边框”可以拆分为“偶数天添加边框”/“奇数天添加边框”。)
  • 添加设置,覆盖(或更糟糕地,扩展)现有设置。

为了实现案例2,您还需要考虑:

  • 没有任何价值可以被另一个重新转移或替换。 (但是在新格式中,它可以说“不支持”,并添加了一个新项目)
  • 枚举可能包含未知值,有效范围的其他更改

唷。 那是很多。 但它并不像看起来那么复杂。

关系数据库人们使用了一个巨大的概念。

它被称为将体系结构分解为“逻辑”和“物理”层。

你的结构既是逻辑层又是物理层,它们被混合成一个难以改变的东西。

您希望程序依赖于逻辑层。 您希望您的逻辑层依次映射到物理存储。 这允许您在不破坏事物的情况下进行更改。

您无需重新发明SQL即可完成此任务。

如果您的数据完全存在于内存中,那么请考虑一下。 将物理文件表示与内存中表示分离。 以一些“通用”,灵活,易于解析的格式(如JSON或YAML)编写数据。 这允许您以通用格式读取并构建高度版本特定的内存中结构。

如果您的数据已同步到文件系统,则还有更多工作要做。 再看一下RDBMS的设计理念。

不要编写简单的无脑struct 。 创建一个“记录”,将字段名称映射到字段值。 它是名称 – 值对的链接列表。 这很容易扩展,可以添加新字段或更改值的数据类型。

如果您在C API中讨论结构使用,可以使用一些简单的指南:

  • 在结构的开头有一个结构大小字段 – 这样使用结构的代码总是可以确保它们只处理有效数据(例如,Windows API使用的许多结构都以cbCount字段开头,因此这些API可以处理针对旧SDK编译的代码进行的调用,甚至是添加了字段的更新SDK
  • 切勿移除字段。 如果您不再需要使用它,那是一回事,但为了保持理解处理使用旧版本结构的代码,请不要删除该字段。
  • 包含版本号字段可能是明智的,但计数字段通常可用于此目的。

这是一个例子 – 我有一个引导加载程序,它在程序映像中查找固定偏移量的结构,以获取有关可能已闪存到设备中的图像的信息。

加载器已经过修改,它支持结构中的其他项目以进行一些增强。 但是,较旧的程序映像可能会闪烁,而较旧的映像使用旧的struct格式。 由于上面的规则从一开始就遵循,新的加载器完全能够处理它。 这很容易。

如果进一步修改结构并且新图像在具有旧加载器的设备上使用新的结构格式,那么该加载器也将能够处理它 – 它只是不会对增强做任何事情。 但是由于没有(或将要)删除任何字段,旧的加载器将能够执行其设计的任何操作,并使用具有更新信息的配置结构的较新映像执行此操作。

如果您正在谈论具有关于字段等的元数据的实际数据库,那么这些指南并不真正适用。

您正在寻找的是前向兼容的数据结构。 有几种方法可以做到这一点。 这是低级方法。

 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将告诉你什么时候结束)。 如果您点击了一个您不知道的ID或您不知道如何处理的类型的项目,您只需跳过该数据(从item-> size)跳过它并继续下一个。 这样,如果某人在下一个版本中发明了新数据字段或删除了一个,您的代码就能够处理它。 您的代码应该能够处理有意义的转换(如果员工ID从整数变为字符串,它应该可以将其作为字符串处理),但您会发现这些情况非常罕见,并且通常可以使用公共代码处理。

我过去曾经在资源非常有限的系统中处理过这种情况,通过在PC上进行翻译作为s / w升级过程的一部分。 您是否可以提取旧值,转换为新值,然后更新就地数据库?

对于简化的嵌入式数据库,我通常不直接引用任何结构,但确实在任何参数周围放置了一个非常轻量级的API。 这允许您更改API下面的物理结构,而不会影响更高级别的应用程序。

最近我使用的是bencoded数据。 这是bittorrent使用的格式。 很简单,您可以直观地检查它,因此它比二进制数据更容易调试并且紧凑。 我从高质量的C ++ 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等其他语言也更容易阅读。

在结构中嵌入版本号,或者像Win32那样使用版本号并使用size参数。
如果传递的结构不是最新版本,则修复结构。

大约10年前,我为电脑游戏保存游戏系统编写了类似的系统。 我实际上将类数据存储在一个单独的类描述文件中,如果我发现版本号不匹配,那么我可以通过类描述文件,找到类,然后根据描述升级二进制类。 这显然需要填写新的类成员条目的默认值。 它工作得很好,它也可以用来自动生成.h和.cpp文件。

我同意S.Lott的观点,即最好的解决方案是将您要做的事情的物理和逻辑层分开。 您实际上是将接口和实现组合到一个对象/结构中,这样做就会错过一些抽象的function。

但是,如果您必须使用单个结构,则可以执行一些操作来帮助简化操作。

1)实际上需要某种版本号字段。 如果您的结构发生变化,您将需要一种简单的方法来查看它并知道如何解释它。 沿着这些相同的行,有时将结构的总长度存储在结构域中的某处是有用的。

2)如果要保持向后兼容性,您需要记住,代码将在内部引用结构字段作为结构基址的偏移量(来自结构的“前面”)。 如果要避免破坏旧代码,请确保将所有新字段添加到结构的后面 ,并保留所有现有字段(即使您不使用它们)。 这样,旧代码将能够访问该结构(但最终将忽略额外的数据),并且新代码将能够访问所有数据。

3)由于您的结构可能正在改变大小,因此不要依赖sizeof(struct myStruct)来始终返回准确的结果。 如果您遵循上面的#2,那么您可以看到您必须假设未来结构可能会变大。 对sizeof()调用计算一次(在编译时)。 使用“结构长度”字段可以确保当您(例如) memcpy正在复制整个结构的结构时,包括您不知道的末尾的任何额外字段。

4)永远不要删除或缩小字段; 如果你不需要它们,请将它们留空。 不要改变现有字段的大小; 如果您需要更多空间,请创建一个新字段作为旧字段的“长版本”。 这可能会导致数据重复问题,因此请务必仔细考虑您的结构并尝试规划字段,以便它们足够大以适应增长。

5)不要在结构中存储字符串,除非您知道将它们限制为某个固定长度是安全的。 相反,只存储指针或数组索引并创建一个字符串存储对象来保存可变长度的字符串数据。 这也有助于防止字符串缓冲区溢出覆盖结构的其余数据。

我参与过的几个嵌入式项目已经使用这种方法来修改结构而不会破坏向后/向前兼容性。 它有效,但它远非最有效的方法。 不久之后,你最终会浪费空间与过时/废弃的结构字段,重复的数据,零碎存储的数据(这里的第一个字,那里的第二个字)等等。如果你被迫在现有的框架内工作,那么这可能为你工作。 但是,使用接口抽象出物理数据表示将更加强大/灵活,而且不那么令人沮丧(如果您有使用这种技术的设计自由)。

您可能想看看Boost Serialization库如何处理该问题。