Small. Fast. Reliable.
Choose any three.

如果OpenDocument使用SQLite怎么办?

介绍

假设围绕SQLite构建了 OpenDocument文件格式,特别是“ ODP” OpenDocument Presentation格式。好处包括:

请注意,这只是一个思想实验。我们不建议更改OpenDocument。本文也不是对当前OpenDocument设计的批评。本文的重点是提出改进未来文件格式设计的方法。

关于OpenDocument和OpenDocument演示

OpenDocument文件格式用于办公应用程序:文字处理器,电子表格和演示文稿。它最初是为OpenOffice套件设计的,但此后已合并到其他桌面应用程序套件中。OpenOffice应用程序已被分叉并重命名了几次。作者对OpenDocument的主要用途是使用Mac上的NeoOffice或 Linux和Windows上的LibreOffice构建幻灯片演示 。

OpenDocument Presentation或“ ODP”文件是一个 ZIP存档,其中包含描述演示幻灯片的XML文件以及作为演示的一部分包含的各种图像的单独图像文件。(OpenDocument文字处理程序和电子表格文件的结构类似,但本文并未考虑。)读者可以使用“ zip -l”命令轻松查看ODP文件的内容。例如,以下是2014年东南LinuxFest 会议有关SQLite的49个幻灯片的演示文稿的“ zip -l”输出 :

存档:self2014.odp
  长度日期时间名称
--------- ---------- ----- ----
       47 2014-06-21 12:34模仿
        0 2014-06-21 12:34 Configurations2 / statusbar /
        0 2014-06-21 12:34 Configurations2 / accelerator / current.xml
        0 2014-06-21 12:34配置2 /浮标/
        0 2014-06-21 12:34 Configurations2 / popupmenu /
        0 2014-06-21 12:34配置2 /进度栏/
        0 2014-06-21 12:34配置2 /菜单/
        0 2014-06-21 12:34配置2 /工具栏/
        0 2014-06-21 12:34配置2 /图像/位图/
    54702 2014-06-21 12:34图片/10000000000001F40000018C595A5A3D.png
    46269 2014-06-21 12:34图片/100000000000012C000000A8ED96BFD9.png
...省略了58张其他图片...
    13013 2014-06-21 12:34图片/10000000000000EE0000004765E03BA8.png
  1005059 2014-06-21 12:34图片/10000000000004760000034223EACEFD.png
   211831 2014-06-21 12:34 content.xml
    46169 2014-06-21 12:34 styles.xml
     1001 2014-06-21 12:34 meta.xml
     9291 2014-06-21 12:34 Thumbnails / thumbnail.png
    38705 2014-06-21 12:34缩略图/thumbnail.pdf
     9664 2014-06-21 12:34 settings.xml
     9704 2014-06-21 12:34 META-INF / manifest.xml
--------- -------
 10961006 78档案

ODP ZIP存档包含四个不同的XML文件:content.xml,styles.xml,meta.xml和settings.xml。这四个文件定义幻灯片的布局,文本内容和样式。这个特殊的演示包含62张图像,从全屏图片到微小的图标,每张图像都作为单独的文件存储在Pictures文件夹中。“ mimetype”文件包含一行文字,内容为:

application / vnd.oasis.opendocument.presentation

目前尚不知道其他文件和文件夹的用途,但可能不难发现。

OpenDocument表示格式的局限性

使用ZIP归档文件封装XML文件和资源是一种处理应用程序文件格式的优雅方法。显然,它优于自定义二进制文件格式。但是,使用SQLite数据库作为容器而不是ZIP会更加优雅。

ZIP存档基本上是一个键/值数据库,针对一次写入/多次读取的情况进行了优化,并且针对相对少量的不同键(几百到几千个)进行了优化,每个键的值都具有较大的BLOB。ZIP归档文件可以视为“文件堆”数据库。这可行,但是相对于SQLite数据库有一些缺点,如下所示:

  1. 增量更新很难。

    很难更新ZIP存档中的单个条目。如果计算机在更新过程中断电和/或崩溃,则以不破坏整个文档的方式来更新ZIP存档中的各个条目特别困难。做到这一点并非不可能,但实际上没有人这样做是非常困难的。相反,无论何时用户选择“文件/保存”,整个ZIP存档都将被重写。因此,“文件/保存”所花费的时间比应该花费的时间长,尤其是在较旧的硬件上。较新的机器速度更快,但是在50 MB的演示文稿中更改单个字符会导致一个人烧掉SSD上有限写入寿命的50 MB,这仍然很麻烦。

  2. 启动很慢。

    与文件堆主题保持一致,OpenDocument将所有幻灯片内容存储在一个名为“ content.xml”的大XML文件中。LibreOffice读取并解析整个文件只是为了显示第一张幻灯片。LibreOffice似乎还可以将所有图像读入内存,这很有意义,因为当用户执行“文件/保存”时,即使它们都没有更改,也必须再次将它们全部写回。最终结果是启动缓慢。双击一个OpenDocument文件会弹出一个进度条,而不是第一张幻灯片。这会导致不良的用户体验。随着文档大小的增加,这种情况变得越来越令人讨厌。

  3. 需要更多的内存。

    因为ZIP存档针对存储大块内容而进行了优化,所以它们鼓励一种编程方式,即在启动时将整个文档读入内存,所有编辑都在内存中进行,然后在“文件/保存”期间将整个文档写入磁盘。OpenOffice及其后代采用了这种模式。

    有人可能会争辩说,在这个数千兆字节的台式机时代,可以将整个文档读入内存中。但这不行。例如,使用的内存量远远超过了磁盘上的(压缩)文件大小。因此,一个50MB的演示文稿可能需要200MB或更多的RAM。如果一个人一次只编辑一个文档,那仍然不是问题。但是,在进行演讲时,该作者通常通常同时拥有10或15个不同的演示文稿(以方便复制/粘贴过去演示文稿中的幻灯片),因此需要GB的内存。添加一个打开的Web浏览器或两个以及一些其他桌面应用程序,突然磁盘旋转,机器正在交换。在使用经Ubuntu改造的廉价Chromebook上工作时,即使只有一个文档也是一个问题。使用更少的内存总是更好。

  4. 崩溃恢复很困难。

    与商业竞争对手相比,OpenOffice的后代往往更容易出现段错误。也许由于这个原因,OpenOffice分支会对其内存中的文档进行定期备份,以便在不可避免的应用程序崩溃时用户不会丢失所有待处理的编辑。每次进行备份时,这都会导致应用程序中令人沮丧的几秒钟暂停。从崩溃中重新启动后,将向用户显示一个对话框,引导他们完成恢复过程。以这种方式管理崩溃恢复涉及许多额外的应用程序逻辑,并且通常会使用户感到烦恼。

  5. 内容不可访问。

    使用通用工具无法轻松查看,更改或提取OpenDocument演示文稿的内容。查看或编辑OpenDocument文档的唯一合理方法是使用专门设计用于读取或写入OpenDocument的应用程序(阅读:LibreOffice或其表亲之一)将其打开。情况可能更糟。只需使用“ zip”存档工具,就可以从演示文稿中提取和查看单个图像(例如)。但是尝试从幻灯片中提取文本是不合理的。请记住,所有内容都存储在单个“ context.xml”文件中。该文件是XML,因此它是文本文件。但这不是可以使用普通文本编辑器管理的文本文件。对于上面的示例演示,content.xml文件正好由两行组成。文件的第一行就是:

    <?xml版本=“ 1.0”编码=“ UTF-8”?>
    

    文件的第二行包含不可穿透XML的211792个字符。是的,同一行上共有211792个字符。对于文本编辑器,此文件是很好的压力测试。值得庆幸的是,该文件不是某种晦涩的二进制格式,但就可访问性而言,它也可能是用Klingon编写的。

第一项改进:用SQLite替换ZIP

让我们假设,OpenDocument不是使用ZIP存档来存储其文件,而是使用具有以下单表模式的非常简单的SQLite数据库:

创建表OpenDocTree(
  filename TEXT PRIMARY KEY,-文件名
  filesize BIGINT,-解压缩后的文件大小
  content BLOB-压缩文件内容
);

对于第一个实验,文件格式没有其他更改。OpenDocument仍然是一堆文件,只是现在每个文件都是SQLite数据库中的一行,而不是ZIP存档中的条目。这个简单的更改没有使用关系数据库的功能。即使这样,此简单的更改仍显示出一些改进。

出乎意料的是,使用SQLite代替ZIP可以使演示文稿文件更小。真的。有人会认为关系数据库文件会比ZIP归档文件大,但至少在NeoOffice情况下不是这样。以下是实际的屏幕抓图,显示了同一NeoOffice演示文稿的大小,它们的大小分别为NeoOffice(self2014.odp)生成的原始ZIP存档格式,以及使用SQLAR实用程序重新打包为SQLite数据库的 大小

-rw-r--r-- 1个drh人员10514994 6月8日14:32 self2014.odp
-rw-r--r-- 1个drh人员10464256 6月8日14:37 self2014.sqlar
-rw-r--r-- 1个drh人员10416644 6月8日14:40 zip.odp

SQLite数据库文件(“ self2014.sqlar”)比等效的ODP文件小大约一半!怎么会这样?显然,NeoOffice中的ZIP存档生成器逻辑效率不如预期,因为当使用命令行“ zip”实用程序重新压缩相同的文件堆时,人们会得到一个文件(“ zip.odp”),该文件如上第三行所示,该值仍然小了百分之五。因此,正如人们所期望的那样,一个编写良好的ZIP存档可能会比等效的SQLite数据库稍小。但是差别很小。关键要点是,SQLite数据库与ZIP存档在大小上具有竞争力。

使用SQLite代替ZIP的另一个优点是,现在可以增量更新文档,而如果在更新过程中发生断电或其他崩溃,则不会损坏文档。(请记住,写入 SQLite数据库是原子的。)确实,所有内容仍保留在一个大XML文件(“ content.xml”)中,如果单个字符发生很大变化,则必须将其完全重写。但是使用SQLite,只需更改一个文件即可。存储库中的其他77个文件可以保持不变。不必全部重写它们,从而使“文件/保存”运行得更快,并节省了SSD的磨损。

第二项改进:将内容分成较小的部分

一堆文件鼓励将内容存储在几个大块中。对于ODP,只有四个XML文件定义演示文稿中所有幻灯片的布局。SQLite数据库允许将信息存储在几个大块中,但是SQLite还是熟练且高效地将信息存储在许多较小的部分中。

因此,假设没有一个单独的表用于单独存储每个幻灯片的内容,而不是将所有幻灯片的所有内容存储在单个超大的XML文件(“ content.xml”)中。表模式可能看起来像这样:

创建表格幻灯片(
  pageNumber INTEGER,-幻灯片页码
  slideContent TEXT-以XML或JSON格式显示内容
);
CREATE INDEX slide_pgnum ON slide(pageNumber);  -  可选的

每张幻灯片的内容仍可以存储为压缩XML。但是现在每个页面都单独存储。因此,当打开一个新文档时,该应用程序可以简单地运行:

从幻灯片WHERE pageNumber = 1中选择slideContent;

该查询将快速有效地返回第一张幻灯片的内容,然后可以对其进行快速解析并将其显示给用户。只需读取和解析一个页面即可呈现第一个屏幕,这意味着第一个屏幕的显示速度更快,并且不再需要烦人的进度条。

如果应用程序希望将所有内容保留在内存中,则可以在绘制第一页后继续使用后台线程读取和解析其他页面。或者,由于从SQLite读取数据是如此高效,因此应用程序可能会选择减少其内存占用量,并且一次只在内存中保留一张幻灯片。或者,它可以将当前幻灯片和下一张幻灯片保留在内存中,以方便快速过渡到下一张幻灯片。

请注意,使用SQLite表将内容分成较小的部分可为实现提供灵活性。应用程序可以选择在启动时将所有内容读入内存。或者它可以只读取几页内存,并将其余页面保留在磁盘上。或者它一次只能将一页读取到内存中。并且不同版本的应用程序可以做出不同的选择,而不必对文件格式进行任何更改。当所有内容都位于ZIP存档中的单个大XML文件中时,此类选项不可用。

将内容分成较小的部分还可以帮助“文件/保存”操作更快地进行。应用程序不必在执行“文件/保存”时写回所有页面的内容,而只需写回实际上已更改的那些页面。

将内容分成较小部分的一个小缺点是,压缩不能在较短的文本上很好地起作用,因此文档的大小可能会增加。但是,由于大量的文档空间用于存储图像,因此文本内容压缩效率的小幅下降几乎不会引起注意,并且为改善用户体验付出了很小的代价。

第三项改进:版本控制

一旦对单独存储每张幻灯片的概念感到满意,这是支持演示文稿版本控制的一小步。考虑以下架构:

创建表格幻灯片(
  slideId整数主键,
  源自INTEGER REFERENCES幻灯片,
  内容文本-XML或JSON或其他内容
);
创建表版本(
  versionId整数主键,
  PriorVersion INTEGER REFERENCES版本,
  checkinTime DATETIME,-保存此版本的时间
  comment TEXT,-此版本的说明
  manifest TEXT-整数slideId的列表
);

在此架构中,不是每张幻灯片都具有确定演示文稿中其顺序的页码,而是每张幻灯片都有一个唯一的整数标识符,该标识符与它顺序出现的位置无关。演示文稿中的幻灯片顺序由slideId列表确定,这些列表以文本字符串的形式存储在VERSION表的MANIFEST列中。由于VERSION表中允许有多个条目,因此这意味着可以将多个演示文稿存储在同一文档中。

在启动时,应用程序首先决定要显示哪个版本。由于versionId会自然增加时间,并且通常会希望查看最新版本,因此适当的查询可能是:

SELECT清单,versionId从版本ORDER BY versionId DESC LIMIT 1;

也许应用程序宁愿使用最新的checkinTime:

SELECT清单,versionId,max(checkinTime)FROM版本;

使用诸如上述的单个查询,应用程序将获得演示文稿中所有幻灯片的slideId列表。然后,该应用程序查询第一张幻灯片的内容,并像以前一样解析和显示该内容。

(此外:是的,上面的第二个查询使用“ max(checkinTime)”确实有效,并且确实在SQLite中返回了定义明确的答案。这样的查询要么返回未定义的答案,要么在许多其他SQL数据库引擎中产生错误。 ,但是在SQLite中它可以实现您所期望的:它返回具有最大checkinTime的条目的清单和versionId。)

现在,当用户执行“文件/保存”时,应用程序可以只为已添加或更改的那些幻灯片在SLIDE表中创建新条目,而不必覆盖修改后的幻灯片。然后,它在VERSION表中创建一个包含已修改清单的新条目。

上面显示的VERSION表的各列用于记录签入注释(大概由用户提供)以及发生File / Save操作的时间和日期。它还记录父版本以记录更改的历史记录。清单可能存储为来自父版本的增量,尽管通常清单会足够小,以至于存储增量可能比它值得的麻烦更多。如果确定将幻灯片内容另存为先前版本的增量是值得进行的优化,则SLIDE表还包含一个derivedFrom列,该列可用于增量编码。

因此,通过此简单更改,ODP文件现在不仅存储了演示文稿的最新编辑,而且还存储了所有历史编辑的历史记录。用户通常只希望查看演示文稿的最新版本,但是如果需要,用户现在可以及时倒退查看同一演示文稿的历史版本。

或者,可以将多个演示文稿存储在同一文档中。

使用这种模式,应用程序将不再需要将未保存的更改定期备份到单独的文件中,以避免在发生崩溃时丢失工作。而是可以分配一个特殊的“挂起”版本,并且可以将未保存的更改写入挂起的版本中。因为只需要写入更改,而不是整个文档,所以保存挂起的更改将只涉及写入几千字节的内容,而不是几兆字节,并且需要几毫秒而不是几秒钟的时间,因此可以在无提示的情况下频繁且安静地完成操作。背景。然后,当发生崩溃并重新引导用户时,将保留其所有(或几乎所有)工作。如果用户决定放弃未保存的更改,则只需返回到以前的版本。

这里有详细信息。也许可以提供一个屏幕来显示历史记录更改(可能带有图形),从而允许用户选择他们要查看或编辑的版本。也许可以提供一些工具来合并在版本历史记录中可能发生的派生。也许应用程序应该提供清除旧版本和不需要版本的方法。关键是,使用SQLite数据库而不是ZIP存档来存储内容,使所有这些功能的实现变得非常容易得多,这增加了最终实现这些功能的可能性。

所以……

在前面的部分中,我们已经看到了从实现为ZIP归档的键/值存储到仅具有三个表的简单SQLite数据库的迁移如何为应用程序文件格式增加重要功能。我们可以继续通过新表来增强架构,添加新的索引以提高性能,使用触发器和视图以方便编程,并且即使在遇到编程错误时也可以使用约束来增强内容的一致性。进一步的增强构想包括:

SQLite数据库具有很多功能,本文才刚刚涉及到。但是希望这种快速的瞥见使一些读者相信,将SQL数据库用作应用程序文件格式值得我们重新审视。

一些读者可能会拒绝使用SQLite作为应用程序文件格式,这是由于先前接触过企业SQL数据库以及其他系统的警告和局限性。例如,许多企业数据库引擎建议不要在数据库中存储大字符串或BLOB,而建议将大字符串和BLOB作为单独的文件存储,而文件名则存储在数据库中。但是SQLite不是那样的。SQLite数据库的任何列都可以容纳最大约1 GB的字符串或BLOB。对于100 KB或更少的字符串和BLOB, I / O性能要比使用单独的文件更好

一些读者可能不愿意将SQLite视为应用程序文件格式,因为他们灌输了所有SQL数据库模式都必须分解为第三范式并且仅存储较小的原始数据类型(例如字符串和整数)的想法。关系理论当然很重要,设计师应该努力理解它。但是,如上所述,将复杂信息以XML或JSON格式存储在数据库的文本字段中通常是可以接受的。做有效的事,而不是数据库教授说的应该做的事。

回顾使用SQLite的好处

总而言之,本文的主张是使用SQLite作为应用程序文件格式(如OpenDocument)的容器,并在该容器中存储许多较小的对象比使用包含几个较大对象的ZIP存档要好得多。以机智:

  1. 与保存相同信息的ZIP存档相比,SQLite数据库文件的大小大约相同,并且在某些情况下更小。

  2. SQLite 的原子更新功能允许将小的增量更改安全地写入文档中。这样可以减少总的磁盘I / O,并提高文件/保存性能,从而改善用户体验。

  3. 通过允许应用程序仅读取初始屏幕中显示的内容,可以减少启动时间。这在很大程度上消除了在打开新文档时显示进度条的需要。该文档会立即弹出,从而进一步增强了用户体验。

  4. 通过仅加载与当前显示相关的内容并将大部分内容保留在磁盘上,可以显着减少应用程序的内存占用量。SQLite的快速查询功能使其成为始终将所有内容保留在内存中的可行选择。而且,当应用程序使用较少的内存时,它将使整个计算机的响应速度更快,从而进一步增强了用户体验。

  5. 与键/值数据库(例如ZIP存档)相比,SQL数据库的模式能够更直接,更简洁地表示信息。这使文档内容更易于第三方应用程序和脚本访问,并促进了高级功能,例如内置文档版本控制和增量保存进行中的工作,以在崩溃后进行恢复。

这些只是将SQLite用作应用程序文件格式的一些好处-这些好处似乎最有可能改善OpenOffice等应用程序的用户体验。其他应用程序可能会以不同的方式受益于SQLite。有关其他建议,请参见“应用程序文件格式” 文档。

最后,让我们重申这篇文章是一个思想实验。OpenDocument格式已经建立并且设计良好。没有人真正相信应该将OpenDocument更改为使用SQLite作为其容器而不是ZIP。本文也不批评OpenDocument没有选择SQLite作为其容器,因为OpenDocument早于SQLite。相反,本文的重点是使用OpenDocument作为如何使用SQLite为将来的项目构建更好的应用程序文件格式的具体示例。