2001 年,我還在上大二時(shí),微軟發(fā)布了 .Net framework 的第一個(gè) RC。當(dāng)時(shí) .Net 聲勢(shì)浩大,微軟在各個(gè)主要高校組織了夏令營,來選拔來年 Microsoft Asia 開發(fā)者大會(huì)的團(tuán)隊(duì)。當(dāng)時(shí)我印象深刻的是,跟隨著 .Net 一起新鮮出爐的 WSDL(Web Service Description Language),它「第一次」(也許)以公開協(xié)議的方式來描述通用的HTTP 客戶端和服務(wù)器之間的 API。在 WSDL 的約定下,API 的請(qǐng)求和響應(yīng)以 XML SOAP 的形式封裝。

在那個(gè)狂野的,沒有 API 的概念的時(shí)代,WSDL 簡直就是一股清流??上枷胩埃?wù)描述太繁雜,使得一個(gè)非常簡單的 API 動(dòng)輒生成成百上千行 XML 格式的 WSDL。在那個(gè)客戶端和服務(wù)器能力還十分有限的年代,WSDL 幾乎沒有激出任何水花,就被扔到了歷史的故紙堆中。

可能早期微軟把太多的賭注放在了曲高和寡的 WSDL(以及服務(wù)發(fā)現(xiàn)協(xié)議 UDDI)上,其主打做 web 開發(fā)的 ASP.Net 一直不溫不火,根本無法與紅遍天的 PHP 相提并論。在 2005 年之前,可以說,(在 web 世界里),PHP 是宇宙中最好的語言。

2005-2010:從混沌到有序 — Ruby on Rails 橫空出世

然而,成也蕭何敗也蕭何,脫胎于 Web 開發(fā)的 PHP,與 Web 的親和性是其優(yōu)勢(shì),也是其后續(xù)沒落的原因 —— 畢竟,當(dāng) Web 軟件越來越復(fù)雜,需要跟越來越多的 web 以外的世界(比如操作系統(tǒng))打交道時(shí),跟其他通用的腳本語言,如 Python/Ruby 相比,PHP 就盡顯劣勢(shì)。

尤其是,當(dāng) Ruby on Rails(以下簡稱 rails)這個(gè)引領(lǐng)一個(gè)時(shí)代的 web 框架橫空出世后,PHP 尷尬的發(fā)現(xiàn),自己的優(yōu)勢(shì),可能就只剩下多年來積攢的生態(tài)系統(tǒng),以及在這個(gè)生態(tài)下滋養(yǎng)著的一大堆開發(fā)者了。

rails 是一個(gè)足以載入史冊(cè)的框架:它把軟件開發(fā)中的很多非常有益的概念、模式和思想(包括但不限于 ORM,CoC,MVC 等)糅合在自己體內(nèi),構(gòu)建了一個(gè)強(qiáng)大同時(shí)非常易用的 web 開發(fā)系統(tǒng)。在 rails 下,哪怕你是個(gè) web 開發(fā)的小白,在學(xué)習(xí)了 rails 的開發(fā)文檔后,也能很快撰寫出一套讓很多 web 開發(fā)老鳥艷羨的系統(tǒng)。在 rails 諸多創(chuàng)新之中,要數(shù) ActiveRecord 最為經(jīng)驗(yàn),它以簡潔優(yōu)雅的表述,顛覆了人們傳統(tǒng)上對(duì)數(shù)據(jù)庫的認(rèn)知,并且?guī)缀鯌{借一己之力,把 ORM 捧上了神壇。

隨著 rails 一起成長的還有 XMLHttp object (俗稱 Ajax)的標(biāo)準(zhǔn)化,以及 JSON 的廣泛使用。其中,Google 通過其旗下的 gmail / google maps 大大促進(jìn)了人們對(duì) Ajax 的認(rèn)知,而 PHP5 和 rails 3 則將 JSON 在廣大開發(fā)者中推廣開來,使其逐漸取代笨拙低效的 XML。有意思的是,Ajax 最初是 Asynchronous Javascript And XML,JSON 普及后,這個(gè) XML 再也沒人提及。

rails 的成功催生了一系列迷弟迷妹 —— 各個(gè)語言的,無論是高仿 rails,或者受 rails 啟發(fā)的框架如雨后春筍般冒出,好不熱鬧。這其中,光是我深度使用過的框架就有:symfony,django 和 Phoenix framework。由 rails 刮起的 ORM 之風(fēng)愈演愈烈,它幾乎成為了 web 開發(fā)者訪問數(shù)據(jù)庫的唯一標(biāo)準(zhǔn)。漸漸的,存儲(chǔ)過程(stored procedure / function)被雪藏,觸發(fā)器(trigger)被遺忘,數(shù)據(jù)庫復(fù)雜而迷人的權(quán)限管理被棄之不顧,取而代之的是用一個(gè)幾乎具有 root 權(quán)限的用戶來連接數(shù)據(jù)庫,而權(quán)限的管理全部被前移到了應(yīng)用層。這和 ORM 所倡導(dǎo)的「一套代碼處理多種數(shù)據(jù)庫」有莫大的聯(lián)系。事實(shí)上,ORM 帶給大家切換數(shù)據(jù)庫的好處,可能僅限于開發(fā)環(huán)境用 sqlite,生產(chǎn)環(huán)境用 postgres 這樣的便利。但從管理的角度,ORM 讓開發(fā)者繞過 DBA(或者干脆不要 DBA)進(jìn)行快速開發(fā),對(duì)于小型項(xiàng)目,可以高效開發(fā),且不需要構(gòu)建數(shù)據(jù)庫領(lǐng)域的專有技能,畢竟培養(yǎng)一個(gè) web 工程師,兩三個(gè)月的訓(xùn)練營就可以讓一個(gè)素人很好掌握開發(fā)框架,進(jìn)行「高效」 CRUD 開發(fā);而培養(yǎng)一個(gè)合格的 DBA,需要整個(gè)計(jì)算機(jī)體系知識(shí)的沉淀。早年間 DBA 還是個(gè)熱門的職位,后來在 rails 以及其一眾小弟的推波助瀾下,DBA 幾乎在中小型企業(yè)中銷聲匿跡。

2010-2015:移動(dòng)互聯(lián)網(wǎng) — API 飛上枝頭變鳳凰

現(xiàn)在回過頭來看,2010年前后,也就是我創(chuàng)業(yè)做途客圈前后,算是近年來少有的互聯(lián)網(wǎng)創(chuàng)新領(lǐng)域集中爆發(fā)的幾年。這其中,最大的功臣要數(shù)喬幫主 2007 年推出的驚世駭俗的「三個(gè)」產(chǎn)品:初代 iPhone。它完全顛覆了我們對(duì)手機(jī)的認(rèn)知,顛覆了對(duì)輸入的認(rèn)知,以及,顛覆了我們的生活。

隨后,大獲成功的 iPhone 4(及 4s)真正把我們的生活扯入了移動(dòng)互聯(lián)網(wǎng)時(shí)代 —— 作為當(dāng)時(shí)最成功最流行的 3G 手機(jī),iPhone 4讓移動(dòng)應(yīng)用進(jìn)入到主流用戶的視野。由于移動(dòng)應(yīng)用擁有自己的 UI 層,不像瀏覽器那樣,UI 層是由服務(wù)器返回的 HTML 渲染出來,因而移動(dòng)端和服務(wù)器之間有著強(qiáng)烈的對(duì)簡潔高效且標(biāo)準(zhǔn)化的 API 層的需求。在這種需求的催生下,REST(Representational state transfer)這個(gè)當(dāng)時(shí)已不新鮮的概念漸漸從象牙塔走入了工業(yè)界。人們發(fā)現(xiàn),與其自己隨機(jī)指定一套 HTTP API 的規(guī)約,不如遵循 HTTP/1.1 規(guī)范,讓 API 的表述和規(guī)范靠攏。這個(gè)時(shí)期,各個(gè)框架要么開始內(nèi)建對(duì) RESTful API 的支持,要么在框架之上,獨(dú)立出一套專門為 API 優(yōu)化的框架,比如 2012 年就比較成熟的 django REST framework:

也許是受到了移動(dòng)互聯(lián)網(wǎng)的沖擊,也許是看到了客戶端和服務(wù)器彼此隔離帶來的巨大好處,web 開發(fā)也漸漸向 REST API 靠攏。在早期的 backbone.js 的引領(lǐng)下,web app 的 API 化在 react 發(fā)布后迅速升溫,并在后續(xù)的幾年得到了主流開發(fā)者的認(rèn)可。到目前為止,純服務(wù)器渲染返回 HTML 的 web 應(yīng)用可能只剩下半壁江山。

這個(gè)時(shí)期,如雨后春筍般綻放的眾多 REST API framework 給開發(fā)者帶來的巨大好處是,你即便不掌握 HTTP/1.1 協(xié)議的細(xì)節(jié),也可以做出像樣的 API,來處理客戶端和服務(wù)器間的交互。

然而,并不是所有的 API 框架都足夠嚴(yán)謹(jǐn),足夠遵循協(xié)議本身。很多 API 框架,在處理復(fù)雜的協(xié)議流程時(shí),要么會(huì)有自相矛盾的處理,要么把這些細(xì)節(jié)完全交由開發(fā)者處理。然而,你如何保證只熱衷于進(jìn)行 CRUD 的開發(fā)者能夠正確使用 ETag 作為樂觀鎖(optimistic locking)進(jìn)行條件更新(conditional update)呢?

在 web 世界不為人知的角落,Erlang 的 webmachine 盡著最大的努力來確保 API 的處理符合 HTTP 協(xié)議。得益于 erlang 強(qiáng)大的 pattern matching 的能力,webmachine 在內(nèi)部構(gòu)建了一張龐大的決策樹,涵蓋了 API 處理的每一個(gè)細(xì)節(jié),連每個(gè)錯(cuò)誤返回的狀態(tài)碼都精益求精。

我曾經(jīng)一度把玩過 liberator,相對(duì)于我當(dāng)時(shí)在生產(chǎn)環(huán)境使用的比較流行的 eve 和 django rest framework 來說, liberator 真的是優(yōu)秀很多。

然而,移動(dòng)互聯(lián)網(wǎng)不是小眾語言和小眾框架的戰(zhàn)場。何況,API 畢竟是客戶端和服務(wù)器共同的約定,在那個(gè)年代,服務(wù)端的嚴(yán)謹(jǐn)會(huì)給客戶端帶來不小的困惑:相較于 412 Preconditional failed 而言,客戶端工程師更鐘情于一招鮮吃遍天的 400 Bad request。

?-2016:我的第一次 API 工具的探索

由于在途客圈和 Juniper web security team 有了不少對(duì) API 開發(fā)的思考和沉淀,我一直有心做一個(gè)自己的 API 開發(fā)框架。在加入 Tubi,理順我們當(dāng)下的 API 結(jié)構(gòu)后,我便以 eve 和 liberator 為藍(lán)圖,nodejs restify 為基石,嘗試著構(gòu)建了一個(gè) UAPI 系統(tǒng),目的是以 pipeline 的形式處理 API 的流程,讓公司的 nodejs 開發(fā)者只需要專注在業(yè)務(wù)邏輯,其它的交由框架完成:

UAPI 算是個(gè)成功的 API 系統(tǒng),它在 Tubi 一直使用了六年多,直到現(xiàn)在還在局部使用。對(duì)客戶端而言,它最大的好處是輸入和輸出都可以強(qiáng)制類型(如果定義了 validators 的話),這樣,不符合要求的輸入會(huì)在 API 處理流程很早的時(shí)候就被捕獲,進(jìn)而返回詳盡的錯(cuò)誤。

在 UAPI 演進(jìn)的過程中,我也感受到了它的諸多局限和問題。其中最大的問題是:框架的使用者是開發(fā)者,而開發(fā)者如果沒有得到充足的培訓(xùn),會(huì)遺漏、誤用、濫用框架的某些能力。比如在 UAPI 中,API 的類型安全不是強(qiáng)制的,因而有的 API 在一開始對(duì) Request 中的各個(gè)部分做了類型檢查,但隨著 API 的迭代,往往新添加的 HTTP 頭,并沒有妥善定義相應(yīng)的類型檢查,于是開發(fā)者在業(yè)務(wù)邏輯中東一塊西一塊做各種校驗(yàn),最終導(dǎo)致不優(yōu)雅的,甚至混亂的表達(dá)。

UAPI 的詳情我就不展開了,感興趣的可以參考我之前的系列文章:再談 API 的撰寫 – 架構(gòu)。

也許在 UAPI 上我犯下的最大的錯(cuò)誤,就是沒有強(qiáng)制類型檢查,把是否需要類型安全的選擇交給了開發(fā)者。

2015-2020:類型安全 — 新的共識(shí)

并不只有我自己有類型安全的切膚之痛,似乎整個(gè)行業(yè)都發(fā)現(xiàn)了 RESTful API 在這一點(diǎn)上的不完善。2015 年,facebook 首先用開源的內(nèi)部項(xiàng)目 GraphQL 向業(yè)界打出了意圖取代 RESTful API 的一記重拳。GraphQL 從輸入和輸出入手,在 HTTP 協(xié)議之上定義了一套查詢語言 —— 客戶端和服務(wù)器之間需要定義好支持的 query / mutation / subscription 的 schema,以及輸入和輸出數(shù)據(jù)結(jié)構(gòu)的 type。

GraphQL 提出了一個(gè)看待 API 的全新視角:客戶端使用者可以根據(jù)需要靈活定義他們想查詢的數(shù)據(jù),而不需要看服務(wù)端老爺們的臉色。在固執(zhí)的 RESTful API 的原教旨主義者眼里,API 應(yīng)該嚴(yán)格對(duì)應(yīng)資源,因而一個(gè) app 頁面如果包含三種不同的資源,那么它就要訪問三個(gè)不同的 API 來獲得結(jié)果。對(duì)客戶端來說,這額外多了兩個(gè)浪費(fèi)用戶寶貴等待時(shí)間的 roud trip,為什么不能一個(gè)查詢就獲得我想要的數(shù)據(jù),且僅包含我想要的數(shù)據(jù)呢?

這個(gè)想法很有創(chuàng)意,但它忽視了靈活性帶來的可能并不值得的復(fù)雜性。GraphQL 的理想情況一直沒有很好地達(dá)成,因?yàn)榉?wù)端不可能為一個(gè)多層隨意嵌套的查詢?nèi)?zhǔn)備數(shù)據(jù)。同時(shí) GraphQL 還有其他很多設(shè)計(jì)上考慮不周的問題,其中最讓人詬病的是,對(duì) HTTP 協(xié)議的無視,也就導(dǎo)致整個(gè) HTTP 生態(tài)和 GraphQL 工作地很別扭,還有查詢時(shí) n+1 的問題(data loader 只是個(gè)特定場景的解決辦法)。

其實(shí)仔細(xì)想想,GraphQL 并沒有領(lǐng)先到足以完全讓大家告別 RESTful API 的地步。其實(shí) RESTful 服務(wù)器可以構(gòu)建 proxy API 來訪問若干其它 API,來解決一個(gè) round trip 就能滿足客戶端的需求,同時(shí)也可以使用 partial response 來讓客戶端精確指定它想要的數(shù)據(jù)。這樣下來,GraphQL 最重要的優(yōu)勢(shì)便蕩然無存。

2016 年,google 開源了 gRPC。它使用 protobuf IDL 來解決輸入輸出的類型安全問題,并且采用 HTTP/2 來支持應(yīng)用層的多路復(fù)用(multiplex)。gRPC 在設(shè)計(jì)時(shí)瞄準(zhǔn)的就是 server-server 的使用場景,因而它可以使用二進(jìn)制數(shù)據(jù)來達(dá)到最好的效率。由于我們這里只著重談 client/server 的 API 演進(jìn),就不展開談 gRPC。

2017 年,OpenAPI v3 問世,REST 的世界終于也有了自己的類型安全。然而 OpenAPI 并不強(qiáng)制輸入輸出的類型安全,這跟 UAPI 有同樣的問題:隨著公司 OpenAPI spec 的不斷迭代,API 中某些新添加的字段,很容易被忽略,日積月累下來,問題會(huì)越來越多。

類型安全對(duì) API 系統(tǒng)的意義不僅僅是輸入輸出有更加嚴(yán)格的校驗(yàn),錯(cuò)誤的輸入能在很早的時(shí)候就被發(fā)現(xiàn)這么簡單。它還打開了一扇新的大門:代碼生成。無論是 GraphQL / gRPC,還是 OpenAPI,它們都可以根據(jù) schema 生成客戶端 SDK,甚至服務(wù)端的 stub 代碼。

當(dāng)然,寫 schema 本身是一件很痛苦的事情,尤其是對(duì)于我們這些能寫代碼就不想寫文檔的開發(fā)者。于是,人們開始追尋取巧的辦法:可不可以只寫代碼,然后通過代碼來生成相應(yīng)的 schema?

schema first, or code first, this is a question.

大部分支持 GraphQL 或者 OpenAPI 的框架遵從程序員的本性,讓你可以專注于寫代碼,順帶生成相應(yīng)的 schema。這是典型的 code first 的思維。

而 schema first 的代表要數(shù) gRPC —— 你撰寫 protobuf 定義,相應(yīng)的編譯器會(huì)替你生成代碼。

這兩種方案的背后,實(shí)際上是框架思維和編譯器思維的較量。

在我看來,code first 背后的框架思維,就像地心說,它一開始很簡單,很容易上手,但隨后你就不得不添加越來越多的本輪和均輪來對(duì)模型不斷校正,使其適應(yīng)在發(fā)展變化中的正確性的保證。

而 schema first 背后的編譯器思維,就像日心說,是「少有人走的路」,(因?yàn)橐獙懡馕銎骰蛘呔幾g器)開頭異常艱難,但一旦成型,日后會(huì)越來越輕松,只需在不斷拓展編譯器的邊界。

2018:我的第二次 API 工具的探索

在使用過多種 code first 的框架來構(gòu)建 GraphQL / OpenAPI 的系統(tǒng)后,我開始構(gòu)思自己的下一個(gè) API 開發(fā)工具:goldrin。

這一次,我的目標(biāo)是:

  1. 定義一門「語言」,來描述我們的 API
  2. 撰寫不同方向上的 Parser(Code generator),將其轉(zhuǎn)換成特定場景的代碼
  3. 將 Parser 構(gòu)建在 build pipeline 中,可以一次 build,生成各種結(jié)果
  4. 生成的結(jié)果要能很方便地?cái)U(kuò)展,以及和系統(tǒng)里的其他部分整合

這可能是我在 arcblock 的征途中,除了 forge 框架外,另一個(gè)很有意義的成就。這個(gè)項(xiàng)目的目標(biāo)如此宏大,某種意義上說也是為了彌補(bǔ)開發(fā)人員的不足,所以很多時(shí)候,受限的資源反倒更能驅(qū)動(dòng)創(chuàng)新。

在這個(gè)目標(biāo)的驅(qū)動(dòng)下,goldrin 實(shí)現(xiàn)了從一個(gè)類似 ansible 的,用來描述數(shù)據(jù)類型以及在數(shù)據(jù)類型上允許進(jìn)行的操作的 schema,構(gòu)建出相應(yīng)的數(shù)據(jù)庫表的定義,GraphQL server 端實(shí)現(xiàn),以及文檔的定義。這套系統(tǒng)最大的好處是:無論是客戶端開發(fā)者,還是后端開發(fā)者,都可以撰寫幾十行 YAML 就得到一個(gè)可以運(yùn)行的,和數(shù)據(jù)庫緊密連接的 API playground。然后你可以在此基礎(chǔ)上不斷調(diào)整,讓 API 從原型一步步走到令人滿意的,可發(fā)布的版本,期間幾乎不用撰寫代碼(可能需要簡單的 mock resolver)。當(dāng) API 的接口成型后,我們可以再撰寫代碼,重載特定的 resolver,使其擁有更高效,更優(yōu)雅的實(shí)現(xiàn)。

如果大家對(duì)此感興趣,可以 google:Use goldorin to build absinthe ecto enabled GraphQL APIs,看看我在 2018 年 ElixirConf 上的 lightening talk。很遺憾的是,由于當(dāng)時(shí)我還想在 goldrin 中提供對(duì) gRPC 的支持后再開源,導(dǎo)致這一項(xiàng)目一直沒有開源,直到我離開。對(duì)于這個(gè)項(xiàng)目,我沒有像 UAPI 那樣留下一個(gè)系列文章,只有一篇短文:思考,問題和方法。

2020:我的第三次 API 工具的探索

如果說 goldrin 是一個(gè)被外部環(huán)境倒逼出來的急中生智,quenya,則更多像是我在無拘無束的條件下,把我之前做過的諸多系統(tǒng)回溯一下,集大成的找樂子項(xiàng)目。這一次,我試圖從 OpenAPI v3 spec 出發(fā),構(gòu)建一切可以自動(dòng)化生成的代碼,甚至包括 API 的測試。

quenya 說實(shí)話,在思路上并沒有比 goldrin 進(jìn)步多少,它的核心還是一個(gè)編譯器,從 OpenAPI 中挖掘各種有用的信息,生成相應(yīng)的代碼。對(duì)此感興趣的同學(xué),可以看我的這個(gè)系列文章:構(gòu)建下一代 HTTP API – 總覽。

2020-至今:低代碼時(shí)代 — API 何去何從?

讓我們快進(jìn)到 2020 年。

低代碼開發(fā)平臺(tái)(Low-code development platform)雖然在 2014 年就被作為一個(gè)正式的名稱被提出,但其開始打開局部市場,獲得大規(guī)模融資,以及進(jìn)一步的發(fā)展,也就是近兩年的事情。低代碼的概念其實(shí)一直挺模糊,但大家的共識(shí)是:用戶可以無需太多編碼,通過「描述」其需求(可以在 GUI 上操作,也可以是撰寫某種簡單易懂的 schema),就能夠構(gòu)建完全可使用的應(yīng)用軟件。低代碼描繪了一個(gè)程序員之外的更廣泛的人群可以構(gòu)建應(yīng)用程序的美好世界。

然而,有應(yīng)用程序的地方,就需要 API,而構(gòu)建 API,則離不開開發(fā)者的參與。雖然過去二十年,API 開發(fā)的自動(dòng)化程度已經(jīng)大大提升,但我們還沒有到達(dá)一個(gè)可以完全自動(dòng)生成 API 的階段。這還怎么低代碼?

如果我們重新審視 API 的作用,我們會(huì)發(fā)現(xiàn),作為客戶端和服務(wù)端數(shù)據(jù)的橋梁,API 解析客戶端的請(qǐng)求,從服務(wù)端某個(gè) data store(可能是數(shù)據(jù)庫,也可能是其他服務(wù)的數(shù)據(jù)等),獲取相應(yīng)的數(shù)據(jù),然后按照 API 的約定返回合適的結(jié)果。

既然 API 的目的是提供數(shù)據(jù),而數(shù)據(jù)往往有其嚴(yán)苛的 schema,同時(shí) API 的 schema 大多數(shù)時(shí)候就是數(shù)據(jù) schema 的子集,那么,我們是不是可以從數(shù)據(jù) schema 出發(fā),反向生成 API 呢?

乍一看,這個(gè)思路和我之前做的 goldrin 類似,但 goldrin 定義了新的「語言」,由外及內(nèi)地生成 API 以及數(shù)據(jù)的 schema,而這個(gè)想法是,以數(shù)據(jù)庫 schema 為單一數(shù)據(jù)來源,由內(nèi)及外地生成 API schema,甚至 API 本身。

這并不是一個(gè)新的思想,早在 2015 年,postgREST 就開展了類似的嘗試,只是這種離經(jīng)叛道的思路和那個(gè) ORM 還如日中天的時(shí)代格格不入。在 DBA 幾乎絕跡于江湖后,有哪個(gè)初創(chuàng)企業(yè)會(huì)把自己的后端圍繞著一個(gè)特定的數(shù)據(jù)庫(postgres)構(gòu)建,并且?guī)缀跤帽M這個(gè)數(shù)據(jù)庫每一個(gè)非標(biāo)準(zhǔn)的功能,完全不考慮可遷移性呢?

再加上 postgREST 是用 haskell 這樣一門小眾的語言開發(fā),更使得好奇它的人多,而使用它的人少之又少。

簡單介紹一下 postgREST 的思路。使用 postgREST,開發(fā)者只需正常定義數(shù)據(jù)庫中的表,視圖,函數(shù),觸發(fā)器等,并為它們的使用權(quán)限賦予相應(yīng)的角色即可。postgREST 可以根據(jù)數(shù)據(jù)庫的 infoschema,掌握詳細(xì)的 metadata,并用這些 metadata 來驗(yàn)證 API 的輸入,也就是 Request,如果驗(yàn)證通過,會(huì)根據(jù) Request 生成相應(yīng)的 SQL 查詢,然后把結(jié)果序列化成客戶端需要的結(jié)構(gòu),以 Response 返回。舉個(gè)例子,對(duì)于這樣一個(gè) API 請(qǐng)求:GET /people?age=gte.18&student=is.true ,postgREST 會(huì)驗(yàn)證數(shù)據(jù)庫中包含 people 表或者視圖,并且其含有 age / student 這兩個(gè)字段,前者是整型,后者是布爾型。如果一切符合,并且用戶具備 people 表或者視圖的 SELECT 權(quán)限,那么它就會(huì)生成 select * from people where age >= 18 and student = true 這樣一條查詢,返回相應(yīng)的 JSON(默認(rèn)客戶端 accept: application/json)。

postgREST 還跟 postgres 的 RLS(Row Level Security)深度綁定,來解決用戶個(gè)人信息安全訪問和更新的需求。比如用戶只能修改自己的帖子,但可以讀別人的帖子這樣的業(yè)務(wù)需求,如果沒有 RLS,很難從數(shù)據(jù)庫級(jí)別直接安全地實(shí)現(xiàn)。

postgREST 這樣一個(gè)小眾的工具進(jìn)入到很多人的視野,還要?dú)w功于 supabase B 輪八千萬美金的巨額融資。它為 postgREST 提供了 GUI,搖身一變成為 firebase 的挑戰(zhàn)者,DBaaS 新生代的翹楚。

另一個(gè)有著同樣思路,但采取了不同路徑的產(chǎn)品 Hasura,今年早些時(shí)候 C 輪融了一億美金。與 supabase 背后的 postgREST 不同的是,Hasura 把寶押在了 GraphQL。Hasura 試圖回答一個(gè)問題:有沒有可能把 GraphQL 的 query 一對(duì)一轉(zhuǎn)換成 SQL 語句?

我們知道 GraphQL 查詢會(huì)被編譯成 Graph AST,而 SQL 查詢會(huì)被編譯成 SQL AST,所以上述那個(gè)問題就變?yōu)椋篏raph AST 可以被安全高效地轉(zhuǎn)換成 SQL AST 么?

看看 Hasura 的天量融資,你就可以猜到,這條路走得通。撰寫自己的編譯器雖然是一條「少有人走的路」,但一旦走通,其迸發(fā)的能量是巨大的,而且有意想不到的效果。前面提到的 GraphQL 令人詬病的 n+1 的問題,在 Hasura 面前都不是是個(gè)事,因?yàn)橐l(fā) n+1 問題的嵌套查詢,翻譯成 SQL 就是一個(gè) INNER JOIN,于是 n+1 問題就這么被悄無聲息地解決了。

那么,Hasura 是如何實(shí)現(xiàn)這一切的呢?我并沒有深入研究,然而當(dāng)我打開 Hasura graphql-engine 的源碼,驚奇的發(fā)現(xiàn),除了 20 多萬行 typescript/javascript 代碼,和 3 萬多行 golang 代碼外,它還有 13 萬行的 Haskell 代碼。莫非,Hasura 也從 postgREST 那里「偷師」?稍稍查詢一下,發(fā)現(xiàn)代碼中確實(shí)有一些 postgREST 的痕跡。

2022:我的第四次 API 工具的探索(頭腦風(fēng)暴)

在仔細(xì)研讀了 postgREST 的用戶文檔后,我大概摸清了它的產(chǎn)品思路。于是我一時(shí)技癢,展開頭腦風(fēng)暴,思考如果做一個(gè)類似的工具,我該怎么做?

首先,我并不喜歡 postgREST 的查詢方式,它的 DSL 在我看來有些蹩腳。我希望通過 x-fields 和 x-filter 這兩個(gè) HTTP 頭,來實(shí)現(xiàn) postgREST 里 querystring 所表達(dá)的內(nèi)容:

對(duì)于 x-fields,它有略微復(fù)雜的,但繼承自 postgREST 的字段選擇語法,我可以使用一個(gè) parser combinator(比如 Rust 下的 nom)來解析它,這樣就可以清晰地知道,字段名如何重命名,以及字段來自于哪張表(如果有 JOIN 的話)。x-filter 我還沒想好如何表述,但我覺得 SQL 中的表達(dá)式就夠用了。對(duì)于 x-filter,我們可以也用 parser combinator 來解析,或者干脆使用某個(gè)SQL 解析器(比如 Rust 下的 sqlparser)解析。解析出來的 metadata 可以和數(shù)據(jù)庫中的 infoschema 比對(duì),來驗(yàn)證請(qǐng)求的合法性,這一點(diǎn)和 postgREST 完全一致。最終,從 x-fields / x-filter 中解析出來的內(nèi)容,連同 rang 頭(用于分頁)一起,就可以構(gòu)建出一個(gè)完整的,合法的 SQL 查詢,最終得到返回的結(jié)果。

到目前為止,這個(gè)系統(tǒng)和 postgREST 如出一轍,沒什么了不起的。

平心而論,我覺得這樣的 API 系統(tǒng),用于內(nèi)部系統(tǒng),還說得過去,但用于外部系統(tǒng),就過于暴露數(shù)據(jù) schema 的細(xì)節(jié),同時(shí)讓 API 的接口和數(shù)據(jù)本身過于耦合。這樣一來帶有安全隱患(很容易被嗅探),二來不利于在 API 接口保持不變的情況下升級(jí)數(shù)據(jù) schema。對(duì)此,postgREST 給出的答案是使用 view 來隔離 table schema 的細(xì)節(jié),但我覺得還不夠完善。我需要一個(gè)能夠在外部看來,更加自然,更加簡單的 API。

在計(jì)算機(jī)的世界里,這樣的問題往往可以通過添加一個(gè)新的層級(jí)來實(shí)現(xiàn)。我并不需要改動(dòng)已有的設(shè)計(jì) —— 它對(duì)于內(nèi)部系統(tǒng)來說還是相當(dāng)不錯(cuò)的設(shè)計(jì),我只需要在這個(gè)設(shè)計(jì)之上,迭加一層。于是我有了這樣的思路:

開發(fā)者可以使用 CREATE API(我胡謅的新 SQL 語法) 來創(chuàng)建一個(gè) API 的描述。這個(gè) todos API,包含兩個(gè)參數(shù):來自 auth header 的 jwt token,以及來自 querystring 里的 completed。API 的 metadata 中包含了一些詳盡的配置,以及 API 的參數(shù)如何作用到配置中。有了這樣的一種 API 配置,用戶可以用圖中更自然地方式訪問 API,而 API 自身沒有暴露任何數(shù)據(jù)庫的邏輯。

整個(gè)過程,比之前的方案多了個(gè) API 的定義過程,由于使用的是描述性語言,所以,很難誤用,并且以后還能很方便的用 GUI 來表述,也算是一種低代碼了。

看到這里,有經(jīng)驗(yàn)的同學(xué)可能會(huì)質(zhì)疑:API 的數(shù)據(jù)源又不止于數(shù)據(jù)庫,如果數(shù)據(jù)來源于 gRPC 服務(wù)器,那又該如何?

好問題!此刻我們需要修改 CREATE API 的描述,使其明確表達(dá)其數(shù)據(jù)源是什么。在下圖的例子里,數(shù)據(jù)源是 grpc_todos:

而 grpc_todos 由 CREATE SOURCE 來定義:

CREATE SOURCE grpc_todos WITH JSON({
"source": "wasm",
"wasm": {
"lib": "todo.wasm",
"fn": {
"name": "get_todos",
"args": [...],
"return": ".."
}
}
});

再一次地,我們看到,使用編譯器的思路去解決問題,是多么地舒服:我們可以不斷擴(kuò)展新的語法,撰寫新的解析器去處理問題。這非常符合 open-close 原則。

這里 source 我使用 webassembly,并不是為了裝 B,而是我希望這樣的工具就像 postgREST 一樣,你不需要,也無法對(duì)其二次開發(fā)。如果需要擴(kuò)展,那么 webassembly 或者 JS 就是最佳的選擇。它可以集成 wasmtime 來處理 webassembly,也可以集成 deno_core 來安全地支持 typescript/javascript 擴(kuò)展。

以上關(guān)于第四次 API 工具的探索的一切不靠譜的想法,都只存在于我的腦海中,我的 excalidraw,以及我的 PPT 里。

本來這篇文章應(yīng)該在上周末發(fā)表出來,可是我一時(shí)技癢,把周末可用的時(shí)間勻給了代碼實(shí)現(xiàn),于是我在撰寫了(主要是通過 psql -E 偷師 psql 命令是如何查詢的)上百行 SQL,從postgres 中獲取關(guān)于 relation / function / columns / constraints 的 infoschema,將它們構(gòu)建成 materialized view,然后利用這一信息,自動(dòng)構(gòu)建了簡單的,沒有任何安全限制的 API

本文章轉(zhuǎn)載微信公眾號(hào)@程序人生

上一篇:

2025年AIAgent開發(fā)框架怎么選?

下一篇:

從0到1搭建本地RAG問答系統(tǒng):Langchain+Ollama+RSSHub技術(shù)全解析
#你可能也喜歡這些API文章!

我們有何不同?

API服務(wù)商零注冊(cè)

多API并行試用

數(shù)據(jù)驅(qū)動(dòng)選型,提升決策效率

查看全部API→
??

熱門場景實(shí)測,選對(duì)API

#AI文本生成大模型API

對(duì)比大模型API的內(nèi)容創(chuàng)意新穎性、情感共鳴力、商業(yè)轉(zhuǎn)化潛力

25個(gè)渠道
一鍵對(duì)比試用API 限時(shí)免費(fèi)

#AI深度推理大模型API

對(duì)比大模型API的邏輯推理準(zhǔn)確性、分析深度、可視化建議合理性

10個(gè)渠道
一鍵對(duì)比試用API 限時(shí)免費(fèi)