翻譯官方文檔中的同步部分,歡迎指正
實現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保持一致)。