Netflix API 实践(二)使用FieldMask 进行数据变更

背景

在上一篇文章中,我们讨论了如何使用FieldMask 作为设计 API 时的解决方案,以便消费者可以通过 gRPC 只获取返回他们需要的数据。在这篇博文中,我们将继续介绍 Netflix Studio Engineering 如何使用 FieldMask 进行更新和删除等变更操作。

Example: Netflix Studio Production

之前我们概述了产品是什么,以及产品服务如何对其他微服务(例如计划服务和脚本服务)进行 gRPC 调用,以检索特定产品(例如 La Casa De Papel)的排期和脚本(又名剧本)。我们可以采用该模型进一步展示我们如何在产品中改变特定字段。

修改产品细节

假设由于我们的制作添加了一些动画元素因此我们想要将格式字段从 LIVE_ACTION 更新为 HYBRID。我们解决这个问题的一个简单方法是添加一个 updateProductionFormatRequest 方法和 gRPC 端点来更新 productionFormat:

message UpdateProductionFormatRequest {
  string id = 1;
  ProductionFormat format = 2;
}

service ProductionService {
  rpc UpdateProductionFormat (UpdateProductionFormatRequest) 
      returns (UpdateProductionFormatResponse);
}

这允许我们更新特定产品的制作格式,但是如果我们想要更新其他字段(例如标题)甚至多个字段(例如 productionFormat、schedule 等)怎么办?在此基础上,我们可以为每个字段实现一个更新方法:一个用于生产格式,另一个用于标题,依此类推:`

// 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 {...}

由于产品中包含大量的字段,在维护我们的 API 时,这可能会变得难以管理。如果我们想更新多个字段并在单个 RPC 中以原子方式进行,该怎么办?为各种字段组合创建额外的方法将导致变更操作 API 的爆炸式增长。此解决方案不可扩展。
与其尝试创建每个可能的组合,另一种解决方案提供一个 UpdateProduction 方法,该方法需要包含消费者的所有字段:

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;
}

这个解决方案带来两个问题,因为消费者必须知道并提供生产中的每一个必填字段,即使他们只想更新一个字段,例如格式。另一个问题是,由于 Production 具有许多字段,因此请求有效负载可能会变得非常大,特别是如果 Production 具有排期或脚本信息
如果我们只发送我们真正想要更新的字段而不是所有字段,不设置其他字段怎么办?在我们的示例中,我们将只设置生产格式字段(以及引用生产的 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);

如果我们永远不需要删除或清空任何字段,这可能会起作用。但是如果我们想去掉title字段的值呢?同样,我们可以引入像 RemoveProductionTitle 这样的一次性方法,但如上所述,该解决方案不能很好地扩展。如果我们想从排期中删除嵌套字段的值,例如计划的启动日期字段,该怎么办?我们最终会为每个可以为空的子字段添加删除 RPC。

使用FieldMask进行数据变更

我们可以使用 FieldMask 来处理我们所有的变更,而不是大量的 RPC 或需要大的有效负载。 FieldMask 将列出我们想要显式更新的所有字段。首先,让我们更新我们的 proto 文件以添加到 UpdateProductionRequest 中,该文件将包含我们想要从生产中更新的数据,以及应该更新的 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;
}

现在,我们可以使用 FieldMask 进行数据变更。我们可以通过使用 FieldMaskUtil.fromStringList() 方法为格式字段创建 FieldMask 来更新格式,该方法为特定类型的字段路径列表构造 FieldMask。在这种情况下,我们将有一种类型,并在后续的例子中进行演示:

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 添加或删除更多字段变得更加容易。在有效负载中提供但未添加到 FieldMask 路径中的数据将不会被更新,并且在操作中也会被忽略。但是,如果我们省略一个值,它将对该字段执行删除突变。让我们修改上面的示例来展示如何更新格式,并删除排期的发布日期,发布日期是 ProductionSchedule 上的一个嵌套字段,为“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);

在此示例中,我们正在执行更新和删除突操作,因为我们已将“format”和“schedule.planned_launch_date”路径添加到我们的 FieldMask。当我们在有效负载中提供此信息时,这些字段将更新为新值,但在构建有效负载时,我们仅提供格式并省略 schedule.planned_launch_date。从有效负载中省略它但在我们的 FieldMask 中定义它将起到删除的效果:


空FiledMask处理

当FieldMask未设置或没有路径时,更新操作适用于所有有效负载字段。这意味着调用者必须发送整个有效负载,或者如上所述,任何未设置的字段都将被删除。
这个约定对模式演变有影响:当一个新字段被添加到消息中时,所有消费者必须开始在更新操作中发送它的值,否则它将被删除。
假设我们要添加一个新字段:生产预算。我们将扩展 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
}

如果有消费者不知道这个新字段或尚未更新客户端方法,它可能会因未在更新请求中发送 FieldMask 而意外地将预算字段清空。
为避免此问题,生产者应考虑要求所有更新操作的字段掩码。另一种选择是实现版本控制协议:强制所有调用者发送他们的版本号并实现自定义逻辑以跳过旧版本中不存在的字段。

总结

API 设计者应该遵循简单 开放 可扩展和发展的设计原则。保持 API 简单且面向未来通常并不容易。在 API 中使用 FieldMask 有助于我们实现简单性和灵活性。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容