Watermelon同步文档
2021-03-25 本文已影响0人
新牛藕
翻译官方文档中的同步部分,欢迎指正
实现pullChanges()
Watermelon会调用这个函数来获取自从上次拉取之后服务端发生的变化。
参数
- lastPulledAt 客户端最后一次从服务端拉取变化的时间戳(如果第一次同步则为null)。
- schemaVersion 客户端本地数据库的当前schema版本。
- migration 表示客户端自从上一次同步之后schema变化的对象(如无变化或不支持,则为null)。
此函数应从服务端获取自lastPulledAt后所有集合中的所有改变列表。
-
你必须传递一个async函数或返回一个promise来resolve或者reject
-
你必须传递lastPulledAt,schemaVersion和migration到遵从Watermelon同步协议的接口
-
你必须通过promise来返回类似如下格式的对象(后端通常已经返回这种格式):
{ changes: {...}, timestamp: 10000, }
-
不要保存使用pullChanges返回的对象,如果需要做一些处理,请在return之前做。Watermelon为了提升性能等原因,会对这个对象做一些改变。
实现pushChanges()
Watermelon会用自从上次push之后发生在本地的一系列改变调用这个函数,你可以把这些改变推送到自己的服务端。
传递的参数:
{
changes: {...},
lastPulledAt: 10000
}
- 必须传递changes和lastPulledAt来推送给遵从Watermelon同步协议的服务端。
- 必须传递一个async函数或在pushChanges()里返回一个Promise。
- pushChanges()必须在服务端确认成功接收客户端变化的情况下才能resolve。
- 如果服务端同步本地上传的变化失败,pushChanges()必须reject。
- 不要过早返回resolve,以免服务端发生错误。
- 不要改变或存储传递给pushChanges()的参数,如果需要做任何处理,在return对象之前做把。Watermelon为了提升性能等原因,会对这个对象做一些改变。
一些提醒
- 不要向任何你不掌握的服务端来调用synchronize()。Watermelon假设pullChanges/pushChanges函数是正确运行的,如果返回的数据损坏,Watermelon不保证行为的安全。
- 当同步在进行时,不要再调用synchronize()。即使再调用,也会安全停止。
- 当同步在进行时,不要重置本地数据库(服务端同步会安全停止,但本地数据库的一致性可能会被破坏)。
- synchronize应在失败时重试一次。这会在推送本地变化发生失败时,重新获取服务端的数据,并再次推送,从而解决这个问题。
- You can use database.withChangesForTables to detect when local changes occured to call sync. If you do this, you should debounce (or throttle) this signal to avoid calling synchronize() too often.
采用Migration同步
为了让Watermelon维护数据库迁移后的一致性,必须支持Migration同步,这允许Watermelon请求后端,来获取完整数据的相应表和列。
- 对于新应用,传递{migrationsEnabledAtVersion: 1}给synchronize()
- 为了启用migration同步,数据库必须按Migration规范来配置。
- 对于现存APP,在改变schema前,设置migrationsEnabledAtVersion为最新的schema版本。
- 无关
- 无关
- 不要删除旧的migrations脚本,否则可能导致APP永远不能同步。
实现同步服务端
理解changes对象
同步的changes(APP在pullChanges中接收的/在pushChanges中发送到服务端的)用带有原始记录的对象来表示。只使用原始的table和column名称,及strings/numbers/booleans类型的原始值,就像在Schema中一样。删除的对象用他们的ID来表示。
正确的changes对象符合以下形式:
Changes = {
[table_name: string]: {
created: RawRecord[],
updated: RawRecord[],
deleted: string[],
}
}
实现pull接口
期待的参数
{
lastPulledAt: Timestamp,
schemaVersion: int,
migration: null | {
from: int,
tables: string[],
columns: {table: string, columns: string[]}[]
}
}
期待的回复
{changes: Changes, timestamp: Timestamp}
- 本接口应该接收上述期待的参数,返回上述形式的回复。这种形式可以与客户端商议来改变,但客户端的pullChanges()必须遵从这种形式。
- 本接口必须返回所有集合中所有记录自lastPulledAt之后的变化,尤其是:
- 服务端自lastPulledAt之后新建的记录
- 服务端自lastPulledAt之后修改的记录
- 服务端自lastPulledAt之后删除的记录ID
- 如果lastPulledAt为null或0,则返回所有记录(初次同步)。
- 服务端返回的timestamp必须保证,如果作为lastPulledAt再次传到pullChanges(),只返回此刻之后变化的记录。
- 本接口必须返回自上次lastPulledAt之后的一致性数据
- 应该同步执行所有查询,或是在写锁中执行,以保证返回的数据一致性
- 应该在所有请求中同步标记服务端时间
- 这是为了保证在获取客户端变化时,数据库中的数据不会发生改变
- 如果没有办法保证上述内容,必须单独查询每个集合,请保证返回的lastPulledAt时间戳,在查询开始之前。但仍然面临不一致的风险,在下一次pull时才能获取完整数据。
- 另一种解决方法是,在查询开始前或结束后,检查是否有新数据改变,若有,则返回错误给前端重试。
- 如果migration不为null,必须包含自前端APP数据库范式迁移后,为了获取一致性数据所需的所有记录
- 特别是,本地数据库在用户上一次同步和schemaVersion之间增加的表中的所有数据必须被包含。
- 在上一次同步和schemaVersion变化之间新增到前端数据库中的所有列,必须包含那些列不为默认值的所有记录
- 有两种方式来确定前端APP中哪些schema发生了变化
- 比较migration.from(上一次同步后APP的schema版本)和schemaVersion(当前APP的schema版本)
- 忽略migration.from,只看migration.tables(表示上一次同步后增加到APP数据库中的表)和migration.columns(表示上一次同步后增加到APP端数据库中表中的列)
- 如果使用migration.tables和migration.columns,则给APP端能访问的数据设置白名单,注意不要泄漏任何内部字段给前端APP。
- 返回的数据记录必须符合前端应用的Schema。
- 返回的数据记录不能包含特别的_status、_changed字段。
- 返回的数据可能包含多余的字段(不存在于当前APP的schemaVersion,可能后续添加的字段),他们会被APP端忽略。
- 返回的记录不能随意使用列名,例如一些可能是javascript的关键字。
- 返回的记录中,ID必须是安全的字符。
- 默认的WatermelonDB的ID范围是
/^[a-zA-Z0-9]{16}$/
。 -
_-
可以在重写默认ID生成器的情况下使用,但'"\/$
是不安全的。
- 默认的WatermelonDB的ID范围是
- 返回的Changes应仅包含在客户端当前schemaVersion中的集合。即时包含了其他数据,也会被前端忽略。
- Changes不应该包含带有任意名字的集合,他们有可能不安全。
实现push接口
- 本接口必须执行客户端发来的更新(changes对象),尤其是:
- 创建changes对象中指定的新记录。
- 更新changes对象中指定的修改的记录。
- 删除changes中指定的待删除记录。
- 如果changes对象中包含的新记录(根据ID来判定),在服务端已经存在,则在服务端更新此记录,且不能返回错误。这种情况发生于,后端成功处理push请求,而前端失败时。
- 如果changes对象中包含的更新记录的ID,在后端不存在,则:
- 如果记录已经被删除,应返回错误给前端(通知前端重新拉取变化)
- 否则创建这条记录,不能返回错误(这种场景很少发生,仅为了防止前端或后端的bug)
- 如果changes对象中包含的删除对象,已经被删除,应该忽略这个错误。(这种情况发生于推送操作仅在后端成功,在前端失败,或者另一个前端用户在本次pull和push操作之间,删除了记录)
- 如果changes对象中包含一条在lastPulledAt之后在服务端被修改了的记录,则停止push并返回错误给前端。这种情形意味着发生了同步冲突,记录在pull和push之间被更新,返回一个错误,让前端重新pull并解决冲突。
- 如果应用前端的所有changes后成功,服务端必须返回一个成功状态码。
- 本接口必须完整使用事务特性,如果遇到错误,所有的服务端更改都必须回退,同时返回相应错误码。
- 必须忽略changes对象中的_status和_changed字段。
- 检查传递到本接口的数据,包括表名、列名、ID的格式,以及用户访问和修改记录的权限。
- 消除传递到本接口的数据中的一些问题,比如用户表的性别字段,值可选范围假设为男、女、其他,若前端传递了其他值,后端应修改为"其他",且不应返回错误给前端。
- 在删除一个记录时,请删除所有关联的记录。
实现服务端改变追踪的意见
如果你困惑于服务端如何实现自上次pull之后一致性拉取所有的变化,或如何检测自lastPulledAt之后用户推送导致改变的记录,那么看这里:
- 添加last_modified字段到所有服务端数据库的表中,并在每一次创建或更新后设置为NOW()。
- 这样子做的话,当你想获取自lastPulledAt之后的所有改变,只需要查询符合last_modified>lastPulledt的记录。
- 时间戳至少要达到毫秒精度(millisecond),
- 当然,如果像上面说的做的话,要忽略来自于客户端的last_modified字段。
- 一种替代时间戳的方案是使用自增的计数序列,需要保证这个序列在所有表中保持一致性。
- 为了区分新建和修改的记录,可以在服务端存储server_created_at字段(如果大于同步请求中的last_pulled_at字段,则记录会在客户端被创建,如果小于,则客户端已经有这条记录,客户端应执行更新操作)。这个server_created_at字段必须与last_modified字段保持一致性,并且不能使用客户端创建的created_at字段(为何?因为不能相信本地的时间戳?)
- 需要实现一种机制来检测记录在服务端的删除时机,否则不能同步记录的删除到客户端。
- 一种实现是不要直接物理删除记录,而是标记deleted字段为true。
- 或者,创建一个deleted_table_name表,记录删除的记录ID和时间戳(与last_modified保持一致)。