超越 REST

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"娛樂業一直在努力應對 COVID-19 對全球製作的影響衝擊。自 2020 年初以來,Netflix 一直在迭代開發系統,以向內部利益相關方和企業領導者提供有關疫情最新信息的最新工具和儀表盤。這些軟件解決方案使得管理層可以就給定的實體產品是否以及何時能夠安全地開始在全球範圍內創建引人注目的內容而做出最明智的決策。在 Netflix Studio Engineering 內部,一種備受關注的方法是將 GraphQL 微服務(GQLMS)作爲後端平臺來促進應用程序的快速開發。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"許多組織都在擁抱 GraphQL,以其作爲統一企業範圍內數據模型的一種方式,並提供了一個用其相關實體網絡來導航大量結構化數據的單一入口點。這種努力值得稱讚,但往往需要內部組織之間歷經幾個季度的協調,然後將所有相關實體開發並集成到一個單一的單體圖中。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"與“用一張圖來管理所有對象”的方法不同,GQLMS 只是利用 GraphQL 來作爲構建 CRUD 應用程序的豐富 API 規範。我們使用 GQLMS 進行了快速的概念驗證應用,其經驗證實了 GraphQL 宣傳其好處時所提出兩個理論:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"GraphiQLIDE 在模式(schema)旁邊顯示任何可用的 GraphQL 文檔,從而極大地改善了 API 使用者的人機工程學(與同類中最好的 Swagger UI 相比)"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"GraphQL 的強類型系統和多語言客戶端支持,意味着 API 提供者無需關心特定於語言的 API 客戶端的生成、版本控制和維護(比如,那些使用優秀的 Swagger Codegen 生成的客戶端)。GraphQL API 的使用者可以簡單地利用自己喜歡的開源 GraphQL 客戶端。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/wechat\/images\/5c\/5cc573a3927822c6497ab3f6f67915ec.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","text":"GraphiQL:爲 《星球大戰》API 自動生成的測試 GUI"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們的經驗已經爲對 GQLMS 作爲快速開發平臺感興趣的團隊帶來了一個具有許多最佳實踐的架構。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/wechat\/images\/0f\/0f0153bd41407fbfa450b06aa92ff31a.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":false,"pastePass":false}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"Graphile"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在早期的 GraphQL 探索過程中,Netflix 的工程師意識到 Graphile 庫可以將 PostgreSQL 數據庫對象(表、視圖和函數)作爲 GraphQL API 來呈現。Graphile 支持 智能註解,支持通過使用特定格式的 PostgreSQL 註解標記數據庫的表、視圖、列和類型來控制各種特性。文檔甚至可以嵌入到數據庫註解中,以便在 Graphile 生成的 GraphQL 模式中顯示。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們假設有一個 Docker 容器,其上運行了一個帶有 Graphile 庫的非常簡單的 NodeJS Web 服務器(以及一些用於安全、日誌、度量和監控的 Netflix 內部組件),可以爲快速開發工作提供“比 REST 更好的 REST”或“REST++”平臺。使用 Docker,我們定義了一個輕量級的獨立容器,它允許我們將 Graphile 庫及其支持的代碼打包成一個獨立的包,任何團隊都可以在 Netflix 上使用它,而無需額外的編碼。只需下拉定義 Docker 的基礎鏡像,並使用適當的數據庫連接符運行它即可。這種方法被證明是非常成功的,並且對 Graphile 的使用產生了一些深刻洞察。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"具體來說:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"使用數據庫視圖作爲“API 層”來保持靈活性,以允許在不變更現有 GraphQL 模式(構建在數據庫視圖上)的情況下修改表。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"使用 PostgreSQL聚合函數 時,請使用 PostgreSQL複合類型。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過允許 GraphQL 客戶端“所用權限”(“full access”)自動生成的 GraphQL 查詢和 Graphile 生成的突變(在所有表和視圖上公開的 CRUD 操作)來提高靈活性;然後在開發過程的後期,刪除在應用程序投產之前未被 UI 使用到的模式元素。"}]}]}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"數據庫視圖作爲 API"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們決定將數據表放在一個 PostgreSQL 模式中,然後在另一個模式中定義這些表的視圖,同時 Graphile Web 應用程序使用專用的 PostgreSQL 用戶角色連接到數據庫。這最終能實現幾個不同的目標:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以獨立於 GraphQL 模式中公開的視圖來更改底層表。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"視圖可以進行基本的格式化(比如將 TIMESTAMP 字段呈現爲 ISO8601 字符串)。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"底層表上的所有權限必須顯式地授權給 Web 應用程序的 PostgreSQL 用戶,以避免意外的寫操作。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"表和視圖可以在同一個事務中進行修改,這樣就可以原子地對公開的 GraphQL 模式進行更改。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"關於最後一點:更改表中列的類型將會打破關聯的視圖,但是通過封裝在事務中的更改,可以刪除視圖、更新該列,然後可以在提交事務之前重新創建視圖。我們在啓用 pgWatch 的情況下運行 Graphile,只要對數據庫做任何更新,GraphQL 模式就會立即更新以反映所做的更改。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"PostgreSQL 複合類型"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Graphile 在讀取 PostgreSQL 數據庫模式以及將表和基本視圖轉換爲 GraphQL 模式方面做得非常出色,但我們的經驗表明,當視圖中存在 PostgreSQL聚合函數 或 JSON 函數 時,Graphile 在如何描述嵌套類型方面存在侷限性。原生 PostgreSQL 函數,比如"},{"type":"codeinline","content":[{"type":"text","text":"json_build_object"}]},{"type":"text","text":",將被轉換成 GraphQL"},{"type":"codeinline","content":[{"type":"text","text":"JSON"}]},{"type":"text","text":"類型,該類型只是一個"},{"type":"codeinline","content":[{"type":"text","text":"String"}]},{"type":"text","text":",沒有任何內部結構。例如,以這個返回"},{"type":"codeinline","content":[{"type":"text","text":"JSON"}]},{"type":"text","text":"對象的簡單視圖爲例:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"makefile"},"content":[{"type":"text","text":"postgres_test_db=# create view postgraphile.json_object_example as\n select json_build_object(‘hello world’::text, 1, ‘2’::text, 3)\n as json;\npostgres_test_db=# select * from postgraphile.json_object_example;\n json\n— — — — — — — — — — — — -\n{“hello world”: 1, “2”: 3}\n(1 row)\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在生成的模式中,數據類型爲"},{"type":"codeinline","content":[{"type":"text","text":"JSON"}]},{"type":"text","text":":"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/wechat\/images\/65\/651a015c4555abc8c1bcbb321f72bb9e.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"json"}]},{"type":"text","text":"字段的內部結構("},{"type":"codeinline","content":[{"type":"text","text":"hello world"}]},{"type":"text","text":"和"},{"type":"codeinline","content":[{"type":"text","text":"2"}]},{"type":"text","text":"這兩個子字段)在生成的 GraphQL 模式中是不透明的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲了進一步描述"},{"type":"codeinline","content":[{"type":"text","text":"json"}]},{"type":"text","text":"字段的內部結構(將其在生成的模式中公開),定義一個複合類型,並創建一個返回該類型的視圖:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"bash"},"content":[{"type":"text","text":"postgres_test_db=# CREATE TYPE postgraphile.custom_type AS (\n \"hello world\" integer,\n \"2\" integer\n);\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接下來,創建一個返回該類型的函數:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"bash"},"content":[{"type":"text","text":"postgres_test_db=# CREATE FUNCTION postgraphile.custom_type(\n \"hello world\" integer,\n \"2\" integer\n)\nRETURNS postgraphile.custom_type\nAS 'select $1, $2'\nLANGUAGE SQL;\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最後,創建一個返回該類型的視圖:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"makefile"},"content":[{"type":"text","text":"postgres_test_db=# create view postgraphile.json_object_example2 as\n select postgraphile.custom_type(1, 3)\n as json;\npostgres_test_db=# select * from postgraphile.json_object_example2;\n json\n— — — -\n(1,3)\n(1 row)\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"乍一看,這似乎沒有什麼用,但要記住:在查看生成的模式之前,請在視圖、自定義類型和自定義類型的字段上定義註解,以利用 Graphile 的智能註解:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"cs"},"content":[{"type":"text","text":"postgres_test_db=# comment on\n type postgraphile.custom_type\n is E’A description for the custom type’;\npostgres_test_db=# comment on\n view postgraphile.json_object_example2\n is E’A description for the view’;\npostgres_test_db=# comment on\n column postgraphile.custom_type.”hello world”\n is E’A description for hello world’;\npostgres_test_db=# comment on\n column postgraphile.custom_type.field_2\n is E’@name field_two\\nA description for the second field’;\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"現在,當查看模式時,"},{"type":"codeinline","content":[{"type":"text","text":"json"}]},{"type":"text","text":"字段不再顯示爲不透明的類型"},{"type":"codeinline","content":[{"type":"text","text":"JSON"}]},{"type":"text","text":",而是顯示爲"},{"type":"codeinline","content":[{"type":"text","text":"CustomType"}]},{"type":"text","text":":"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/wechat\/images\/1d\/1dba321baa7f570c93e73652ac0be6b1.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"(還要注意,對視圖所做的註解("},{"type":"codeinline","content":[{"type":"text","text":"A description for the view"}]},{"type":"text","text":")顯示在查詢字段的文檔中)。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"單擊"},{"type":"codeinline","content":[{"type":"text","text":"CustomType"}]},{"type":"text","text":"將顯示自定義類型的字段及其註解:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/wechat\/images\/1a\/1a9259df888609e5fc1667bb2c4a906c.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"請注意,在自定義類型中,第二個字段被命名爲"},{"type":"codeinline","content":[{"type":"text","text":"field_2"}]},{"type":"text","text":",但 Graphile 智能註解將該字段重命名爲"},{"type":"codeinline","content":[{"type":"text","text":"field_two"}]},{"type":"text","text":",通過 Graphile 將駝峯式大小寫轉換爲"},{"type":"codeinline","content":[{"type":"text","text":"fieldTwo"}]},{"type":"text","text":"。另外,對這兩個字段的描述都被顯示在生成的 GraphQL 模式中。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"允許 Graphile 生成的模式具有“所有權限”(在開發期間)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最初,當討論使用 Graphile 作爲“一種模式來管理所有模式”架構中的一個選項時,該提議遭到了強烈的反對。關於安全性(如何將其與我們的 IAM 基礎設施集成,以及如何在數據庫中實施行級訪問控制?)和性能(如何限制查詢以避免一次選擇所有行來對數據庫進行 DDoS 攻擊?)的合法性問題引起了人們的關注,提出了使用類似於 SQL 的查詢接口以提供對數據庫表的打開權限(open access)。然而,在小團隊快速開發內部應用程序的 GQLMS 環境中,默認的 Graphile 行爲是讓所有列都可用來過濾,這允許 UI 團隊可以快速迭代大量新特性,而無需後端團隊的參與。這與其他開發模型不同,在其他模型中,UI 和後端團隊首先就初始 API 契約達成一致,後端團隊實現 API,UI 團隊使用 API,然後 API 契約隨着 UI 需求在開發生命週期中的變化而演變。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最初,整個應用程序的性能很差,因爲 UI 通常需要多次查詢才能獲取所需的數據。然而,一旦應用程序的行爲被充實起來,我們就可以快速創建新視圖,以滿足每個 UI 交互的需求,這樣每次交互只需要一個調用即可。因爲這些請求是以本機代碼運行在數據庫上,所以我們可以通過適當地使用索引、去規範化、集羣等來執行復雜的查詢並獲得高性能。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一旦 UI 和後端之間的“公共 API”(“public API”)固化,我們就“加固”了 GraphQL 模式,通過使用智能註解"},{"type":"codeinline","content":[{"type":"text","text":"@omit"}]},{"type":"text","text":"標記表和視圖來刪除所有不必要的查詢(由 Graphile 的默認設置創建)。另外,Graphile 的默認行爲是爲表和視圖生成突變,但是智能註解"},{"type":"codeinline","content":[{"type":"text","text":"@omit create,update,delete"}]},{"type":"text","text":"將從模式中刪除突變。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"結論"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對於那些採用模式優先方法進行 GraphQL API 開發中的用戶來說,Graphile 的自動 GraphQL 模式生成功能可能會對模式設計者造成難以接受的限制。如果需要細粒度的訪問控制,Graphile 可能很難集成到現有的企業 IAM 基礎設施中。向 Graphile 生成的模式中添加自定義查詢和突變(即公開 UI 所需的 gRPC 服務調用)是我們目前在 Docker 鏡像中不支持的。然而,我們最近注意到 Graphile 的 makeExtendSchemaPlugin,它允許將自定義類型、查詢和突變合併到 Graphile 生成的模式中。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"也就是說,在初始需求有限並且有一個臨時的分佈式團隊(之前沒有合作過)的情況下,一個內部應用在 4-6 周內就能成功實現,這引起了整個 Netflix Studio 的極大興趣。Netflix 的其他團隊也正在尋找對應的 GQLMS 方法:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"使用標準的 GraphQL 構造函數和實用程序將數據庫公開爲 API"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"利用自定義的 PostgreSQL 類型構建 GraphQL 模式"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"通過從數據庫自動生成大型 API 來提高靈活性"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"並在 Graphile 生成的業務邏輯和數據類型之外,額外公開其他自定義的業務邏輯和數據類型"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這是一個替代之前使用 REST 實現內部 CRUD 工具的可行解決方案。擁有託管 Graphile 的標準化 Docker 容器爲團隊提供了必要的基礎設施,通過這些基礎設施,他們可以快速迭代新工具的原型以及快速開發應用程序,從而解決全球媒體工作室在這個充滿挑戰時期內不斷變化的需求。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原文鏈接:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https:\/\/netflixtechblog.com\/beyond-rest-1b76f7c20ef6","title":"","type":null},"content":[{"type":"text","text":"https:\/\/netflixtechblog.com\/beyond-rest-1b76f7c20ef6"}]}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章