离线概览¶
尽管当今的现代蜂窝网络可靠且快速,但很多时候用户会遇到由于网络带宽低甚至根本没有网络连接而导致应用无法正常工作的情况。 这种情况可能发生在非城市环境或无法保证网络覆盖的区域, 例如地下室、石油钻塔、零售商店、仓库等。 然而,现今用户的期望是可以随时随地运行重要的业务应用, 而无论网络可用性如何。
此类离线应用程序的一个示例是现场服务人员使用的应用程序, 他们在某天开始时从后端下载服务信息和数据,全天对离线数据进行更改, 然后在这天结束时将修改请求上传到后端。该用例需要一种复杂的离线技术, 该技术可作为可靠且快速的移动应用的一部分,支持应用程序开发人员提供不间断的用户体验, 使应用缓存远远超出了传统缓存。与在线应用相比,已启用离线的移动应用在性能、数据量、并发性和错误处理方面需要更多的思考和计划。 通过提供更加可靠且用户友好的应用, 额外的工作量很快获得了回报。
过程¶
- 将离线功能添加到 SAP 移动服务主控室中的移动应用配置。
- 将 SDK 的日志记录功能集成到您的应用程序。
- 初始化离线存储。
- 离线时使用 OData CRUD 调用。
- 连接到移动服务时下载和上传数据。
- 在 SAP 移动服务主控室中定制离线同步行为和性能。
功能范围¶
| 功能 | 描述 |
|---|---|
| CRUD | 用户离线时,OData 离线功能支持完全 CRUD 操作。 |
| 查看离线配置设置 | 在 SAP 移动服务主控室中查看离线配置设置。 |
| 查看离线 OData 应用程序的使用情况统计 | 管理员可以在 SAP 移动服务主控室中查看离线 OData 应用程序的请求和响应时间使用情况统计。为离线数据存储操作收集统计信息,例如构建、刷新(下载)和清空(上传)。 |
| 离线数据安全性 | 用于离线访问的本地数据存储在设备上进行加密。实施离线存储上传 API 后,用户可以将本地数据库文件安全地上传到服务器。 |
| OData 版本 2 数据源 | 离线 OData 支持按照版本 2 规范(含附加版本 4 元数据注释)访问 OData 服务。 |
| OData 版本 4 数据源 | 离线 OData 允许根据版本 4.0 规范访问 OData 服务,以及版本 4.01 规范中的附加功能。 |
| OData 版本 4 lambda 运算符 | 离线存储支持 OData 版本 4 lambda 运算符 any 和 all。 |
| 冲突检测 | 使用 ETag 可进行冲突检测,使开发人员能够注意到同一实体中的数据修改,并在其应用中作出相应反映。 |
| 失败请求的处理 | 允许开发人员编写可靠的应用,即可从离线时出现的业务逻辑错误中恢复的应用。 |
| 媒体资源 | OData 离线功能支持处理后端服务提供的媒体资源。 |
| 可重复的请求 | 为确保数据一致性,我们支持在离线 OData 中阻止可重复的请求。 |
| 本地存储的上传 | 若要进行根本原因分析,开发人员可以扩展其应用,以将本地数据存储上传到移动服务。 |
| CLI 工具 | 用于排除离线场景故障的本地工具。 |
| 事件日志 | 设置后即可查看过去系统离线系统事件的日志的纯本地实体。 |
| 进度 API | 使开发人员能够通知用户有关正在进行的数据同步的信息。 |
| 请求队列优化 | 可以减少发送到后端的请求数的内置启发式。 |
| 事务构建器 | 允许将 CUD 操作分组到多个事务(OData 更改集)。 |
| 撤销更改 | 在上传到后端之前撤销实体的本地修改。 |
| 复杂的对象图形 | 允许在离线时在本地创建复杂的实体关系。 |
| 仅客户端实体 | 允许客户端应用程序加载仅客户端元数据并对 OData 后端服务器中不存在的实体集执行 OData CRUD 操作。 |
工作原理¶
缓存对比离线优先¶
一个常见的误解是我们的离线技术如何与传统缓存相关联:
- 缓存
- 缓存解决方案将缓存最近最常使用的 (MRU) 数据
- 指定固定的最大缓存大小,根据最近最少使用 (LRU) 驱逐数据
- 如果您最近没有访问过数据,则离线时将无法使用这些数据
- 假设用户离线的时间很短
- 作为一种提高性能的良好方式
- 可能极大地限制了用户在离线时能够执行的操作 (例如,只读)
- 可能会在离线时针对聚合查询给出不正确的答案
- 最适合创建、提高性能和增强在线应用的稳健性
- 离线优先
- 应用程序主动下拉用户最终可能需要的数据子集
- 没有固定缓存大小,根据需要增长(有利有弊)
- 用户离线时需要的所有数据都可用,没有使用限制
- 也可以作为一种提高性能的良好方式,但存在警告:初始下载时间可能很长, 具体取决于数据切片的方式
- 能够准确地表示后端将给出的响应,但存在一些警告
- 最适合在重要时间期间内离线时预期使用的应用
- 涵盖各种本地数据操作(例如,创建、读取、更新、删除 (CRUD))
基础技术¶
移动服务离线 OData 基于内部技术构建,该技术专门设计用于将后端数据同步到移动设备并已使用超过 20 年。 在客户端, SAP 正在使用 UltraLite 数据库, 这是一种专为移动设备设计的专有标准。设备上有两个 UltraLite 数据库, 一个用于存储数据的存储数据库, 一个用于存储客户端和服务器之间请求的请求队列数据库。存储数据库以逐个实体为基础存储用户的数据视图,其中包括服务器提供的数据、客户端生成的数据或两者。
还包括许多其他表,例如存储这些实体之间关系的表。
请求队列数据库存储用户的修改请求(POST、PUT、PATCH、DELETE)。其中还包括许多其他表,例如支持请求优化的表。
与其他供应商相比,这种数据库同步方法使 SAP 离线 OData 更加强大:由于我们能够重放客户端和服务器之间的消息流,因此首先可进行服务器端审计。
将数据下载到客户端¶
第一次将数据下载到客户端设备时,基于所提供的所谓“定义请求”或“定义查询”(两者表示相同的概念)的列表,以声明方式完成。
定义查询是真正的 OData 查询,其指示要同步后端数据的哪些子集。
由于它们是基于 OData 构建的,因此这些查询非常强大:例如,可以同步完整数据集(例如,完整实体集
SalesOpportunities),通过过滤将其限制为某个子集(例如
SalesOpportunities?$filter=Region eq 'EMEA')等。
客户端第一次连接到移动服务时,会将这些查询发送到后端。然后,初始存储数据库在移动服务中填充,随后作为预装配二进制文件发送到客户端。 尽管起初听起来可能令人惊讶,但这使移动服务能够有效地将共享数据同步到多个客户端,而无需进行多余的后端往返。
已填充并下载客户端存储数据库后,便可以在本地发出任何查询。 进行后续下载时,查询将再次发送到后端,这次使用增量令牌,以便仅将修改后的数据发送到客户端而不是完整的数据库。 在此流程中,将放弃所有客户端生成的实体,更新、添加或移除服务器提供的实体,然后重放请求队列,重新创建客户端生成的实体。
从客户端上传数据¶
在移动服务离线 OData 中,请求是事实的一个来源;本地数据仅仅是一种猜测,
尽管是受过高等教育人士的猜测。当用户请求上传时,离线 OData 客户端将这些请求发送给移动服务,其反过来将这些请求中继到后端。在错误响应情况下,在客户端上填充虚拟 ErrorArchive 实体集,允许用户最终查看和/或解决处理请求时在后端发生的错误。
在 POST 请求情况下,处理响应以启用客户端生成的临时密钥与服务器生成的实际密钥之间的映射。
请注意,在发送请求之前,离线 OData 客户端将(如果已这样配置)
运行一种或多种优化算法以重新排序和/或合并请求,
从而有助于减少后端处理时间。
增量同步¶
当下载发生时,需要更新客户端上的数据以与服务器上的数据匹配。执行此操作最简单的方式是再次发送所有数据。 在某些情况下,这可能是必要的,但在正常情况下, 这样速度太慢且成本很高。OData 专家为我们提供了一个更好的解决方案:增量查询。
OData 生产者可以提供有关更改或增量的信息。在初始 GET 请求中,会返回一个增量令牌(通常是时间戳)。
提出后续 GET 请求后,增量令牌将作为查询选项传入。
因此,后端只会发送自原始时间戳以来发生更改的数据。 计算增量的范围可以从相对简单到极其复杂。
有些 OData 生产者没有提供增量,
因此移动服务可以通过回退算法来提供。这远没有那么好,但总比没有增量好。
可重复请求¶
为确保请求已成功到达后端,客户端需要收到返回的响应。有时,可能由于但不限于网络问题而未收到响应。
发生这种情况时,客户端不知道是
a)已发送到后端但响应丢失,还是 b)从未发送到后端。因此,客户必须重新发送。
但是有些请求,尤其是 POST 请求,不是幂等的:
发出两次可能会导致创建重复记录或主键值不唯一异常,
因此多次向后端发送这些请求可能会出现问题。可重复请求按如下方式解决此情况:后端(或者,如果失败,移动服务)将存储响应。如果再次收到请求,其会重新发送原始响应而不是再次处理请求。
事务合并¶
如果长时间离线,用户最终可能会多次重复影响实体的相同逻辑组。他们可能对 WorkOrder(101) 进行更改,
然后创建 WorkOrder(102),接着再次处理 WorkOrder(101)。此过程在后端效率很低。
在许多情况下,客户端实体和后端表之间没有一对一的映射。
因此,创建该映射成本很高,并且反复重新创建是非常浪费的。通过添加请求选项,用户可以指示哪些请求属于哪个逻辑组。
离线 OData 随后会将这些分组到尽可能少的批处理和更改集:
理想情况下只有一个,但在某些情况下,可能会更多。请注意,
请求总数并没有减少,请求将重新排序。
请求队列优化¶
用户在上传之前通常会在设备上生成数百甚至数千个请求。在后端同时处理它们的成本可能会很高, 特别是如果每个人同时执行此操作, 例如在班次结束时。但是,许多请求可以合并在一起:
POST、PATCH可以合并到POSTPATCH、PATCH可以合并到PATCHPOST、PATCH、DELETE可完全消除
但是,这比听起来要复杂得多。例如,以下是我们在离线 OData 队列优化中包含的一些注意事项:
- 存在许多不同类型的关系
- 您需要知道实体在请求队列中何时存在,何时不存在
- 您需要尊重批处理和更改集边界
- 离线 OData 允许用户将某些请求标记为不可合并
模式升级¶
移动开发项目通常涉及多个团队,OData 服务通常是客户端和后端开发人员之间的接口。
由于接口通常是协商的,因此服务会发生更改,并且本地数据库模式需要与服务器提供的内容保持同步。
每个下载操作的第一部分都是检查是否对模式进行了任何更改(即 $metadata 路由中提供的 OData 服务元数据)。
如果检测到更改,则会自动向客户端发送新的实体存储。
OData 标准定义了在不引入新服务根的情况下,
可以对服务的元数据进行哪些更改。
不需要新的请求队列数据库,所有本地操作都将应用于新实体存储。
未上传到后端的请求队列中的操作,只要遵循 OData 标准中的规则,
就不应受到模式更改的影响。
共享数据¶
有些数据是用户特定的,有些是全局的:开发人员和/或管理员可以选择将部分或全部请求标记为共享。 对共享请求的响应将存储在移动服务中以减少往返时间和后端负载。 可以配置共享数据的过期时间,过期时间内发出的请求将更加快速。 另一方面,触发共享缓存重建的请求的速度将与它们未标记为共享时的速度相同。
使用共享数据的风险是过期时间范围内的过时数据。因此,将请求标记为共享的决策以及所选的过期时间需要深思熟虑。
错误处理¶
构建离线应用程序时必须考虑错误。
当离线 OData 作为上传的一部分提交的 OData 请求失败时,错误将返回到设备,并且失败请求中涉及的实体将进入错误状态。
刷新完成后,发生的错误将添加到名为
ErrorArchive 的虚拟实体集。 ErrorArchive 与 OData 模型中的任何其他 OData 实体集一样可访问 。 刷新后由客户端来修复任何错误。
此操作通常通过发出修复问题的 OData 请求来完成。
当使用队列中的失败请求调用刷新时,将运行合并算法以将失败请求与引用相同实体的
新请求合并,以尝试创建没有错误的请求。
注释
请注意,这种错误通常与业务规则和相应的输入验证有关:
可通过智能客户端输入验证,也可能通过重新考虑业务流程来避免这类错误。
更新冲突¶
必须考虑的一个特定错误类别是如何处理更新冲突;即当多个客户端对同一数据进行更改时。
OData 通过使用 HTTP ETag(一种乐观锁形式)支持冲突检测:
最简单的后端将使用基于时间戳的方法来生成 ETag。当实体更新时,客户端指定正在更改的实体的时间戳 (ETag)。
如果时间戳与请求的当前服务器版本不匹配,则拒绝。
离线 OData 应用程序可以使用 ETag 来检测和防止冲突发生:
已拒绝请求将添加到 ErrorArchive 以供客户端修复。 离线应用程序中另一种可考虑的常用方法是
“最后一个获胜”:后端不会检测冲突并允许执行最后一次修改。
但是,不建议使用这种方法,因为这种方法可能会导致数据丢失。
注释
请注意,这种错误通常与流程问题有关。例如,首先, 允许多个员工同时访问和编辑相同的数据记录
可能就不是一个好主意,因为这 会引发如何合并更改的问题。最终用户能否聚在一起找出解决方案?是否需要附加后台资源进行调解? 重要的是要了解这不是技术 问题,而是流程问题,在计划任何类型的已启用离线的应用时都需要全面考虑。
仅客户端实体¶
离线 OData 客户端是一个成熟的 OData 客户端,能够处理客户端存储中所有实体上的所有 OData 操作。现代移动设备比以往更加强大,而仅客户端实体功能是离线 OData 的扩展,使客户端应用程序能够加载客户端定义的元数据,并对其执行 OData CRUD 操作,从而充分发挥设备的潜力。有关详细信息,请参阅仅客户端实体概览。
示例导览¶
以下是虚构的请求导览,用于说明上述离线 OData 的内部工作原理。
- 初始下载期间,将
WorkOrder(101)添加到本地存储。- 在
WorkOrder表中,添加了新行,并标记为服务器行和已激活
- 在
- 在设备上,用户发出
WorkOrder(101)的PATCH请求- 将
PATCH请求添加到请求队列表 - 在
WorkOrder中,添加了新行,并标记为本地行和已激活 - 在
WorkOrder表中,服务器行标记为未激活
- 将
- 在设备上,用户发出
WorkOrder(101)的GET请求- 将
GET请求转换为WorkOrder表上已激活行的SELECT - 该行将转换为 OData 响应并返回
- 将
-
用户发出另一个下载
- 后端注意到另一用户已更新
WorkOrder(101)并进行发送 WorkOrder表中的服务器行已更新- 放弃本地行,然后重放
PATCH请求,从而重新创建
- 后端注意到另一用户已更新
-
用户创建新的工作订单
WorkOrder(102)- 将
POST请求添加到请求队列表 - 在
WorkOrder中,添加了新行,并标记为本地行和已激活 - 请注意,没有服务器行;还要注意实体标识是临时的
- 将
- 在设备上,用户发出上传
POST请求已发送到后端- 检查响应,并从中提取实际实体标识
- 临时实体标识和实际实体标识之间的映射已建立
POST请求标记为已发送
- 在设备上,用户发出下载
- 后端发送新实体
- 将
WorkOrder(102)的服务器行添加到WorkOrder表并标记为已激活 - 已移除本地行
- 现已解决的
POST请求已移除