背景
在上一篇文章中窟她,我們討論了如何使用FieldMask 作為設(shè)計(jì) API 時(shí)的解決方案纳账,以便消費(fèi)者可以通過 gRPC 只獲取返回他們需要的數(shù)據(jù)兄旬。在這篇博文中,我們將繼續(xù)介紹 Netflix Studio Engineering 如何使用 FieldMask 進(jìn)行更新和刪除等變更操作凡纳。
Example: Netflix Studio Production
之前我們概述了產(chǎn)品是什么窃植,以及產(chǎn)品服務(wù)如何對其他微服務(wù)(例如計(jì)劃服務(wù)和腳本服務(wù))進(jìn)行 gRPC 調(diào)用,以檢索特定產(chǎn)品(例如 La Casa De Papel)的排期和腳本(又名劇本)荐糜。我們可以采用該模型進(jìn)一步展示我們?nèi)绾卧诋a(chǎn)品中改變特定字段巷怜。
修改產(chǎn)品細(xì)節(jié)
假設(shè)由于我們的制作添加了一些動畫元素因此我們想要將格式字段從 LIVE_ACTION 更新為 HYBRID。我們解決這個(gè)問題的一個(gè)簡單方法是添加一個(gè) updateProductionFormatRequest 方法和 gRPC 端點(diǎn)來更新 productionFormat:
message UpdateProductionFormatRequest {
string id = 1;
ProductionFormat format = 2;
}
service ProductionService {
rpc UpdateProductionFormat (UpdateProductionFormatRequest)
returns (UpdateProductionFormatResponse);
}
這允許我們更新特定產(chǎn)品的制作格式暴氏,但是如果我們想要更新其他字段(例如標(biāo)題)甚至多個(gè)字段(例如 productionFormat延塑、schedule 等)怎么辦?在此基礎(chǔ)上答渔,我們可以為每個(gè)字段實(shí)現(xiàn)一個(gè)更新方法:一個(gè)用于生產(chǎn)格式关带,另一個(gè)用于標(biāo)題,依此類推:`
// separate RPC for every field, not recommended
service ProductionService {
rpc UpdateProductionFormat (UpdateProductionFormatRequest) {...}
rpc UpdateProductionTitle (UpdateProductionTitleRequest) {...}
rpc UpdateProductionSchedule (UpdateProductionScheduleRequest) {...}
rpc UpdateProductionScripts (UpdateProductionScriptsRequest) {...}
}
message UpdateProductionFormatRequest {...}
message UpdateProductionTitleRequest {...}
message UpdateProductionScheduleRequest {...}
message UpdateProductionScriptsRequest {...}
由于產(chǎn)品中包含大量的字段沼撕,在維護(hù)我們的 API 時(shí)宋雏,這可能會變得難以管理。如果我們想更新多個(gè)字段并在單個(gè) RPC 中以原子方式進(jìn)行务豺,該怎么辦磨总?為各種字段組合創(chuàng)建額外的方法將導(dǎo)致變更操作 API 的爆炸式增長。此解決方案不可擴(kuò)展笼沥。
與其嘗試創(chuàng)建每個(gè)可能的組合舍败,另一種解決方案提供一個(gè) UpdateProduction 方法,該方法需要包含消費(fèi)者的所有字段:
message Production {
string id = 1;
string title = 2;
ProductionFormat format = 3;
repeated ProductionScript scripts = 4;
ProductionSchedule schedule = 5;
// ... more fields
}
service ProductionService {
rpc UpdateProduction (UpdateProductionRequest) returns (UpdateProductionResponse);
}
message UpdateProductionRequest {
Production production = 1;
}
這個(gè)解決方案帶來兩個(gè)問題敬拓,因?yàn)橄M(fèi)者必須知道并提供生產(chǎn)中的每一個(gè)必填字段,即使他們只想更新一個(gè)字段裙戏,例如格式乘凸。另一個(gè)問題是,由于 Production 具有許多字段累榜,因此請求有效負(fù)載可能會變得非常大营勤,特別是如果 Production 具有排期或腳本信息
如果我們只發(fā)送我們真正想要更新的字段而不是所有字段灵嫌,不設(shè)置其他字段怎么辦?在我們的示例中葛作,我們將只設(shè)置生產(chǎn)格式字段(以及引用生產(chǎn)的 ID):
UpdateProduction updateProduction = UpdateProduction.newBuilder()
.setProductionFormat(PRODUCTION_FORMAT_HYBRID)
.build();
// Send the update request
UpdateProductionResponse response = client.updateProduction(LA_CASA_DE_PAPEL_PRODUCTION_ID,
updateProductionRequest);
如果我們永遠(yuǎn)不需要刪除或清空任何字段寿羞,這可能會起作用。但是如果我們想去掉title字段的值呢赂蠢?同樣绪穆,我們可以引入像 RemoveProductionTitle 這樣的一次性方法,但如上所述虱岂,該解決方案不能很好地?cái)U(kuò)展玖院。如果我們想從排期中刪除嵌套字段的值,例如計(jì)劃的啟動日期字段第岖,該怎么辦难菌?我們最終會為每個(gè)可以為空的子字段添加刪除 RPC。
使用FieldMask進(jìn)行數(shù)據(jù)變更
我們可以使用 FieldMask 來處理我們所有的變更蔑滓,而不是大量的 RPC 或需要大的有效負(fù)載郊酒。 FieldMask 將列出我們想要顯式更新的所有字段。首先键袱,讓我們更新我們的 proto 文件以添加到 UpdateProductionRequest 中燎窘,該文件將包含我們想要從生產(chǎn)中更新的數(shù)據(jù),以及應(yīng)該更新的 FieldMask:
message ProductionUpdateOperation {
string production_id = 1;
string title = 2;
ProductionFormat format = 3;
ProductionSchedule schedule = 4;
repeated ProductionScript scripts = 5;
... // more fields
}
message UpdateProductionRequest {
// contains production ID and fields to be updated
ProductionUpdateOperation update = 1;
google.protobuf.FieldMask update_mask = 2;
}
現(xiàn)在杠纵,我們可以使用 FieldMask 進(jìn)行數(shù)據(jù)變更荠耽。我們可以通過使用 FieldMaskUtil.fromStringList() 方法為格式字段創(chuàng)建 FieldMask 來更新格式,該方法為特定類型的字段路徑列表構(gòu)造 FieldMask比藻。在這種情況下铝量,我們將有一種類型,并在后續(xù)的例子中進(jìn)行演示:
FieldMask updateFieldMask = FieldMaskUtil.fromStringList(Production.class,
Collections.singletonList(“format”);
// Update the production format type
ProductionUpdateOperation productionUpdateOperation = ProductionUpdateOperation
.newBuilder()
.setProductionId(LA_CASA_DE_PAPEL_PRODUCTION_ID)
.setProductionFormat(PRODUCTION_FORMAT_HYBRID)
.build();
// Build the UpdateProductionRequest including the updatefieldmask
UpdateProductionRequest updateProductionRequest = UpdateProductionRequest
.newBuilder()
.setUpdate(productionUpdateOperation)
.setUpdateMask(updateFieldMask)
.build();
// Send the update request
UpdateProductionResponse response =
client.updateProduction(LA_CASA_DE_PAPEL_PRODUCTION_ID, updateProductionRequest);
由于我們的 FieldMask 僅指定格式字段银亲,即使我們在 ProductionUpdateOperation 中提供更多字段慢叨,該字段也將是唯一被更新的字段。通過修改路徑务蝠,向我們的 FieldMask 添加或刪除更多字段變得更加容易拍谐。在有效負(fù)載中提供但未添加到 FieldMask 路徑中的數(shù)據(jù)將不會被更新,并且在操作中也會被忽略馏段。但是轩拨,如果我們省略一個(gè)值,它將對該字段執(zhí)行刪除突變院喜。讓我們修改上面的示例來展示如何更新格式亡蓉,并刪除排期的發(fā)布日期,發(fā)布日期是 ProductionSchedule 上的一個(gè)嵌套字段喷舀,為“schedule.planned_launch_date”:
FieldMask updateFieldMask = FieldMaskUtil.fromStringList(Production.class,
Arrays.asList("format", "schedule.planned_launch_date"));
// Update the format, in addition remove schedule.planned_launch_date by not including it in our request
ProductionUpdateOperation productionUpdateOperation = ProductionUpdateOperation
.newBuilder()
.setProductionId(LA_CASA_DE_PAPEL_PRODUCTION_ID)
.setProductionFormat(PRODUCTION_FORMAT_HYBRID)
.build();
UpdateProductionRequest updateProductionRequest = UpdateProductionRequest
.newBuilder()
.setUpdate(productionUpdateOperation)
.setUpdateMask(updateFieldMask)
.build();
// Send the update request
UpdateProductionResponse response =
client.updateProduction(LA_CASA_DE_PAPEL_PRODUCTION_ID, updateProductionRequest);
在此示例中砍濒,我們正在執(zhí)行更新和刪除突操作淋肾,因?yàn)槲覀円褜ⅰ癴ormat”和“schedule.planned_launch_date”路徑添加到我們的 FieldMask。當(dāng)我們在有效負(fù)載中提供此信息時(shí)爸邢,這些字段將更新為新值樊卓,但在構(gòu)建有效負(fù)載時(shí),我們僅提供格式并省略 schedule.planned_launch_date杠河。從有效負(fù)載中省略它但在我們的 FieldMask 中定義它將起到刪除的效果:
空FiledMask處理
當(dāng)FieldMask未設(shè)置或沒有路徑時(shí)碌尔,更新操作適用于所有有效負(fù)載字段。這意味著調(diào)用者必須發(fā)送整個(gè)有效負(fù)載感猛,或者如上所述七扰,任何未設(shè)置的字段都將被刪除。
這個(gè)約定對模式演變有影響:當(dāng)一個(gè)新字段被添加到消息中時(shí)陪白,所有消費(fèi)者必須開始在更新操作中發(fā)送它的值颈走,否則它將被刪除。
假設(shè)我們要添加一個(gè)新字段:生產(chǎn)預(yù)算咱士。我們將擴(kuò)展 Production 消息和 ProductionUpdateOperation:
// update operation with new ‘budget’ field
message ProductionUpdateOperation {
string production_id = 1;
string title = 2;
ProductionFormat format = 3;
ProductionSchedule schedule = 4;
repeated ProductionScript scripts = 5;
ProductionBudget budget = 6; // new field
}
如果有消費(fèi)者不知道這個(gè)新字段或尚未更新客戶端方法立由,它可能會因未在更新請求中發(fā)送 FieldMask 而意外地將預(yù)算字段清空。
為避免此問題序厉,生產(chǎn)者應(yīng)考慮要求所有更新操作的字段掩碼锐膜。另一種選擇是實(shí)現(xiàn)版本控制協(xié)議:強(qiáng)制所有調(diào)用者發(fā)送他們的版本號并實(shí)現(xiàn)自定義邏輯以跳過舊版本中不存在的字段。
總結(jié)
API 設(shè)計(jì)者應(yīng)該遵循簡單 開放 可擴(kuò)展和發(fā)展的設(shè)計(jì)原則弛房。保持 API 簡單且面向未來通常并不容易道盏。在 API 中使用 FieldMask 有助于我們實(shí)現(xiàn)簡單性和靈活性。