【前端大佬 | Node 連載 2/9】語雀團(tuán)隊(duì) - 小琿:《Node.js ORM 在語雀的探索與實(shí)踐》
第 63 屆早早聊大會將于 2023 年 4 月 15 日(本周六)舉辦 - 低代碼無代碼|無碼有碼 全靠拖拉,6 位講師全天直播,關(guān)鍵詞:低代碼/組件物料/游戲?qū)嵺`/ AB Test /React 玩轉(zhuǎn)。跟早早聊一起,玩轉(zhuǎn)低代碼,上車鏈接:https://www.zaozao.run/conf/c63

本文是 2021 年 12 月 26 日,第三十五屆 - 前端早早聊【前端搞 Node.js】專場,來自螞蟻金服 語雀前端團(tuán)隊(duì) —— 小琿的分享。感謝 AI 的發(fā)展,借助 GPT 的能力,最近我們終于可以非常高效地將各位講師的精彩分享文本化后,分享給大家。(完整版含演示請看錄播視頻和 PPT):https://www.zaozao.run/video/c35

正文如下
大家好,我是來自語雀的小琿,是一名全棧開發(fā)工程師。
在本次分享的內(nèi)容如下,
第一,解釋什么是 ORM,以及它在Node.js web 應(yīng)用中的使用和優(yōu)缺點(diǎn)。
第二,大致介紹目前比較常見的兩種 ORM 模式 - Active Record 和 Data Mapper,并對它們進(jìn)行簡單對比。
第三,用架構(gòu)圖和偽代碼來詳細(xì)介紹 ORM 的結(jié)構(gòu),包括其中的重要部分和相關(guān)實(shí)現(xiàn)。
第四,使用 ORM 時可能遇到的問題以及相應(yīng)的優(yōu)化措施。

什么是 ORM
為了照顧純前端的同學(xué),我將先展示一個簡單的 demo 來演示 ORM 的使用。我們假定有三張表,用戶表、文章表和評論表,它們之間的關(guān)系可以用圖表現(xiàn)出來。每篇文章只能有一個作者,每個文章可以有多條評論,每一條評論只能屬于某一篇文章。接下來我們來看看 ORM 在使用時,如何表達(dá)數(shù)據(jù)庫中的關(guān)系,并使用它進(jìn)行業(yè)務(wù)查詢和展示。

首先,我們會使用 ORM 來描述三個實(shí)體,包括用戶、文章和評論。我們將使用 user 類來對應(yīng)用戶實(shí)體,使用 comment 類對應(yīng)評論實(shí)體,使用 article 類對應(yīng)文章實(shí)體。在 article 類中,我們將描述剛剛提到的兩個關(guān)系,即每篇文章有一個作者,每篇文章有多條評論。我們將根據(jù)本地數(shù)據(jù)庫的設(shè)置,連接到數(shù)據(jù)庫,進(jìn)行初始化操作。在初始化函數(shù)中,我們會首先連接到數(shù)據(jù)庫,然后對這三張表進(jìn)行數(shù)據(jù)清理。接下來,我們將演示如何使用 ORM 進(jìn)行增刪改查操作。
我們將先創(chuàng)建一個用戶,并使用 ORM 功能查詢出該用戶,然后對其進(jìn)行簡單修改,并重新查詢結(jié)果。接著,我們將創(chuàng)建一個文章,并添加兩個評論。然后,我們將使用三種不同的方式來查詢文章以及與之相關(guān)的作者和評論。在執(zhí)行結(jié)果中,我們可以看到每個操作所對應(yīng)的 SQL 語句和調(diào)用,以及查詢到的結(jié)果。在下面的三個不同的 API 調(diào)用方式中,生成的 SQL 語句都是相似的。最終,我們將得到一篇文章及其相關(guān)的作者、評論以及其他信息。通過這個 demo,我們可以看到如何在 Node.js 中使用 ORM 進(jìn)行增刪改查操作。

回到主題什么是 ORM ?ORM 是對象關(guān)系映射(Object Relation Mapping)的縮寫,它將數(shù)據(jù)中表對應(yīng)著的開發(fā)代碼或內(nèi)存中的 model 類與數(shù)據(jù)庫中的某一張表對應(yīng)。數(shù)據(jù)表中的每一條數(shù)據(jù)對應(yīng)著 model 類的一個實(shí)例,數(shù)據(jù)表中的某個字段對應(yīng)著 model 類的一個成員變量。使用 ORM 可以將數(shù)據(jù)庫中的數(shù)據(jù)映射到開發(fā)代碼中,從而方便地操作數(shù)據(jù)庫的增刪改查。
使用 ORM 有很多優(yōu)點(diǎn),例如 ORM 會對查詢和更新操作進(jìn)行數(shù)據(jù)預(yù)處理,從而防止 SQL 注入的風(fēng)險。另外,ORM 屏蔽了直接編寫 SQL 的細(xì)節(jié),使得開發(fā)人員不必自己寫 SQL,這對于 SQL 不熟練的人來說是一個好處。此外,由于 ORM 以模型為基礎(chǔ),因此支持 MVC 的開發(fā)架構(gòu),并且可以映射所有數(shù)據(jù)庫表到內(nèi)存的 model 中,這有助于組織和復(fù)用代碼,避免了到處寫 SQL 的尷尬處境。
然而,使用 ORM 也存在一些缺點(diǎn)。例如,由于組合 API 生成的 SQL 的特性,有時自動生成的 SQL 可能不是最優(yōu)的方案,這可能會導(dǎo)致性能問題。此外,為了處理各種復(fù)雜的邏輯,model 也會變得很復(fù)雜,處理查詢結(jié)果可能會有不必要的對象深拷貝,這會影響應(yīng)用的性能。同時,ORM 為了適配 SQL 滿足的各種業(yè)務(wù)場景,有很多 API 需要學(xué)習(xí),這也是一種成本。另外,對于一些奇怪的查詢需求,ORM 可能無法滿足,此時只能手寫 SQL。這些是我總結(jié) ORM 的一些優(yōu)點(diǎn)和缺點(diǎn)。

Active Record & Data Mapper
接下來介紹兩種常見的 ORM 模式:Active Record 和 Data Mapper。Active Record 翻譯成中文就是主動記錄模式,是一種架構(gòu)模式。之前展示的 ORM 是 Active Record 模式的。

Active Record 的簡單總結(jié)是一個對象,同時包含了數(shù)據(jù)庫對應(yīng)的屬性字段和相應(yīng)的業(yè)務(wù)的增刪改查操作,也就是說 model 打包了這一個域該有的所有功能。比如,用戶有了 user model 就可以直接使用它,并對它以及 user model 實(shí)例進(jìn)行一些業(yè)務(wù)的編碼。這種類型的 ORM 幾乎都有一個特點(diǎn),就是所有 CRUD 操作都打包在一個 model 中。業(yè)務(wù)中只需要根據(jù)自己的項(xiàng)目和數(shù)據(jù)庫設(shè)計去派生出對應(yīng)的 base model 的子類。user model 繼承了 base model 所有的 API,同時也會包含自己特有的業(yè)務(wù) API,比如查詢某個性別的用戶、某個年齡段用戶等等。

Active Record 類型的 ORM 使用上更加符合我們的直覺,使用起來更方便。數(shù)據(jù)庫有多少張表,就對應(yīng)多少個 model,每個 model 有哪些操作,都在這個派生出來的 model 中實(shí)現(xiàn)。它代表的是我們的數(shù)據(jù)結(jié)構(gòu)與模型對象高度耦合,因此可能更適合一些業(yè)務(wù)邏輯比較簡單的中小型應(yīng)用。
我們之前已經(jīng)展示了一個屬于 Active Record 類型的 ORM demo,因此在這里就不再多作解釋。接下來,我們將介紹另一種 ORM 類型,即 Data Mapper類型。我們將通過一個 demo 來說明這種類型的 ORM,其中涉及到的模型包括 user、article 和 comment?;仡櫼幌滤鼈冎g的關(guān)系:每篇文章有一個作者,每個用戶可以有多篇文章,每篇文章有多條評論,每條評論只能歸屬于一篇文章。雖然 Data Mapper 類型的 ORM 在 JavaScript 中并不是很流行,但我們將使用一個名為 TypeORM 的常用 ORM 來進(jìn)行演示。

首先,我們需要聲明實(shí)體,分別是用戶(user)、文章(article)和評論(comment)。在每個實(shí)體中,我們聲明可能用到的屬性和實(shí)體之間的關(guān)系。例如,用戶可能會有多篇文章,而一篇文章只能有一個作者和多個評論,每個評論只能屬于一篇文章。這與之前講過的 Active Record ORM 類似,但有一個不同點(diǎn)是這些模型不再包含基礎(chǔ)的數(shù)據(jù)操作(例如增刪改查),而只用于展示數(shù)據(jù),例如名字的展示可能需要加上大寫等特殊的展示。
Data Mapper 的實(shí)現(xiàn)主要是為了適配某個實(shí)體或幾個實(shí)體的一些基礎(chǔ)業(yè)務(wù)操作。我們以文章(article)為例,實(shí)現(xiàn)一個 Data Mapper,里面會有一個 API,用于根據(jù)當(dāng)前文章的 ID 獲取其作者(article)和評論(comment)。在 API 中,我們使用 Data Mapper 提供的基礎(chǔ) API 去做一個簡單的查詢。由于數(shù)據(jù)庫中已經(jīng)有了數(shù)據(jù),我們直接去查詢,然后生成一個circle并查找到想要的文章,其中包含作者和兩個評論。
Data Mapper 模式與 Active Record 模式的不同點(diǎn)在于,它將數(shù)據(jù)存儲層與領(lǐng)域?qū)咏怦睿P筒辉俪袚?dān)增刪改操作的功能。Data Mapper 可以同時處理一個或多個實(shí)體類的應(yīng)用,例如連表查詢和統(tǒng)一的數(shù)據(jù)插入操作等業(yè)務(wù)操作。如果某個業(yè)務(wù)需要對數(shù)據(jù)一致性有較強(qiáng)的要求,并涉及多個實(shí)體,Data Mapper 可以直接在其中進(jìn)行操作。

與之前的 Active Record ORM 不同,如果涉及多個模型,我們可能需要單獨(dú)使用一個服務(wù)(Service)將這些模型結(jié)合起來進(jìn)行處理。因此,Data Mapper ORM 更適合處理多實(shí)體類的應(yīng)用。

無碼有碼,全靠拖拉。跟早早聊一起,玩轉(zhuǎn)低代碼,上車鏈接:https://www.zaozao.run/conf/c63
ORM 的構(gòu)成
我們接下來將講解 ORM 的構(gòu)成,其中我們將重點(diǎn)講解 Active Record ORM,這是我們常用的一種類型。這些例子都是偽代碼。
我將 Active Record ORM 的結(jié)構(gòu)分為兩層,第一層為數(shù)據(jù)抽象層,包含常用的 Base Model,通過繼承 Base Model 來創(chuàng)建業(yè)務(wù) model。
API 都通過 Base Model 進(jìn)行調(diào)用,其他基礎(chǔ)功能依附于 Base Model。
Hooks 是插入到 API 執(zhí)行過程中的鉤子函數(shù),可以對特定 model 的字段在執(zhí)行某個操作時進(jìn)行通用處理。
Validations 是 ORM 進(jìn)行數(shù)據(jù)預(yù)處理的必要部分,使用它可以提高應(yīng)用的安全性和降低數(shù)據(jù)庫執(zhí)行 SQL 時的數(shù)據(jù)類型轉(zhuǎn)換壓力。
Transaction 是對數(shù)據(jù)事務(wù)的抽象和實(shí)現(xiàn),對于一些數(shù)據(jù)一致性要求高的業(yè)務(wù)很有必要。
Relationships 是關(guān)系型數(shù)據(jù)庫的核心,每個 model 與 model 之間的關(guān)系對應(yīng)數(shù)據(jù)庫的 ERD。
Migrations 是 ORM 的一個工具類型的功能,用于同步數(shù)據(jù)表結(jié)構(gòu)以及數(shù)據(jù)訂正。
Dirty Check 是檢查數(shù)據(jù)是否更新的功能,在 Hooks 中使用較多。
Data Transformation 是將查詢結(jié)果轉(zhuǎn)換為 model 實(shí)例,或?qū)⒉樵儣l件轉(zhuǎn)換為數(shù)據(jù)庫能夠識別的數(shù)據(jù)類型的功能。
第二層為數(shù)據(jù)訪問層。
Dialect Adopter 是核心功能,將 model 的 API 調(diào)用轉(zhuǎn)換成對應(yīng)的 SQL,并在轉(zhuǎn)換過程中抹平 ORM 會適配的不同數(shù)據(jù)庫之間的方言差異。
Connection Manager用于管理ORM在應(yīng)用中的數(shù)據(jù)庫連接。
DB Driver是數(shù)據(jù)工具,用于與數(shù)據(jù)庫進(jìn)行交互。
日志模塊是開發(fā)和運(yùn)維中必須的,貫穿整個架構(gòu)。

數(shù)據(jù)抽象層
在 Base Model 中,數(shù)據(jù)表中的某個字段對應(yīng)著 model 類的成員變量,這是對象關(guān)系映射中的重要關(guān)系映射。
DataType(數(shù)據(jù)類型)主要用于表現(xiàn)數(shù)據(jù)類型,作為 JS 基礎(chǔ)類型與數(shù)據(jù)庫數(shù)據(jù)類型的橋梁,記錄了數(shù)據(jù)庫類型和對應(yīng)的 JS 數(shù)據(jù)類型,并能在兩種語言之間轉(zhuǎn)換數(shù)據(jù)類型。它還需要表達(dá)該類型的 SQL,例如,如果數(shù)據(jù)類型是 integer,在 MySQL 中表現(xiàn)是什么樣子的,在 post Grace 或 circulate 中表現(xiàn)又是什么樣子的類型。這樣,最小的 ORM 最小單位類就成型了。
Attribute (屬性)用于表達(dá)數(shù)據(jù)表中的數(shù)據(jù)字段,它能夠與數(shù)據(jù)表中的字段設(shè)置大致一致,包括是否允許為空、是否有默認(rèn)值、數(shù)據(jù)類型等。在 ORM 中,Attribute 還有一個重要的功能,就是數(shù)據(jù)驗(yàn)證,可以設(shè)置一些預(yù)置的規(guī)則或用戶自定義的規(guī)則來驗(yàn)證數(shù)據(jù)的合法性。
在 Active Record ORM 中,Base Model 是其核心組成部分之一。它包含了 CRUD 在內(nèi)的所有基礎(chǔ) API,同時還要能夠讀取用戶派生的 model。在 Base Model 中,還有針對數(shù)據(jù)庫與應(yīng)用開發(fā)語言之間的不同命名習(xí)慣的 name 和 column 成員變量。初始化 model 時,還會有用戶設(shè)置的 Hooks 和 Validation,用戶可以自定義 set/get 方法來對某個字段做一些自定義的操作,在設(shè)置字段值的時候自動執(zhí)行相應(yīng)操作,比如用 text 類型數(shù)據(jù)庫的字段來存儲 JSON 字符串。此外,base model 還會記錄各個 model 之間的關(guān)系,如 has_one、has_many 等關(guān)系。
Hooks 是指一些在函數(shù)執(zhí)行前或執(zhí)行后需要執(zhí)行的操作,Hooks 的使用則可以降低業(yè)務(wù)代碼的復(fù)雜度,減少工作量,它需要注意一下幾點(diǎn)。
Hooks 需要能夠疊加某個操作,以便處理多種邏輯,這些邏輯不會相互影響。
Hooks 需要有一個原函數(shù),它的入?yún)⒑头祷刂殿愋筒荒鼙恍薷?。Hooks 的實(shí)現(xiàn)方式有很多種,其中比較直觀的一種方式是面向切面編程。在 ORM 的 API 中,一些 API 可以配置或需要配置 hooks,例如 create、update、destroy 等。
Hooks 的實(shí)現(xiàn)需要注意不同的 API 入?yún)⒑头祷刂悼赡懿煌?/span>
實(shí)現(xiàn) Hooks 時需要注意靜態(tài)成員方法和成員方法、函數(shù)執(zhí)行上下文等問題。
在使用 AOP 實(shí)現(xiàn) Hooks 時,我們還需要考慮如何跳過 Hooks 的執(zhí)行。
接著我們來看一下 Transaction 的簡單實(shí)現(xiàn)。在 SQL 語法中實(shí)現(xiàn)事務(wù)并不復(fù)雜,一般使用 begin 開始事務(wù),執(zhí)行業(yè)務(wù) SQL,然后使用 commit 提交或 rollback 回滾事務(wù)。在代碼中實(shí)現(xiàn)事務(wù),我們可以提供一些基礎(chǔ)的 API,如 begin、commit 和 rollback。在事務(wù)開始時,可能需要設(shè)置事務(wù)的隔離級別。通過這些 API,我們可以保持業(yè)務(wù)代碼的數(shù)據(jù)一致性。然而,在實(shí)現(xiàn)事務(wù)時,我們需要考慮如何自動 commit 和 rollback,而不需要手動調(diào)用 commit 和 rollback。
數(shù)據(jù)訪問層
在數(shù)據(jù)抽象層中,為了適配常用的數(shù)據(jù)庫類型,需要一個中間態(tài)去表達(dá) ORM API,這個中間態(tài)被稱為 Spell。它可以表達(dá)實(shí)際的 SQL 命令,根據(jù)模型的類型去判斷操作的數(shù)據(jù)表,表達(dá)查詢所需的字段以及常用的 SQL 關(guān)鍵詞表達(dá)式。

Dialect 需要解析 Spell 表達(dá)式,根據(jù)方言類型生成特定的 SQL。例如,ORM 需要官方提供支持的MySQL、Postgres、SQL Server和 SQLite 的 Dialect。因此,需要為每種數(shù)據(jù)庫類型編寫一個dialect。我們可以將 Spell 表達(dá)式的解析抽象成標(biāo)準(zhǔn)的接口,這樣開發(fā)者就可以實(shí)現(xiàn)自己的方言,甚至不僅限于 SQL,還可以是其他類型的查詢語言。這樣我們就可以使用 ORM 的 API 進(jìn)行各種類型的查詢。

整個查詢的執(zhí)行過程是這樣的:假設(shè)我們使用 user model 去查詢某個用戶數(shù)據(jù),我們在 user model 中使用 find 方法并根據(jù)傳參生成對應(yīng)的 Spell 對象。然后我們在 Connection 類中管理數(shù)據(jù)庫連接池和方言(即 Dialect),實(shí)現(xiàn)一個 query 方法,在其中調(diào)用 Dialect 生成 SQL,使用 SQL Driver 執(zhí)行 SQL,最后通過進(jìn)行數(shù)據(jù)轉(zhuǎn)換并返回結(jié)果。

模塊之間的關(guān)系
讓我們再深入了解各個模塊之間的關(guān)系。首先,數(shù)據(jù)層面上最主要的實(shí)體是 Model。在執(zhí)行查詢操作時, Spell 作為一個中間態(tài),它連接了數(shù)據(jù)抽象層和數(shù)據(jù)訪問層。同時,Dialect 負(fù)責(zé)適配數(shù)據(jù)庫方言和生成特定的 SQL。這個結(jié)構(gòu)還可以擴(kuò)展出更多的功能,不僅限于 SQL 查詢。

else
我們思考一下一個相對成熟的 ORM 還需要哪些改進(jìn)?

歡迎報名第 63 屆早早聊大會 - 低代碼無代碼,了解如何通過低代碼平臺提高生產(chǎn)力,玩轉(zhuǎn)低代碼。上車戳:https://www.zaozao.run/conf/c63
ORM 問題 / 優(yōu)化
緩存查詢
通過適當(dāng)?shù)牟僮鱽斫档蛿?shù)據(jù)庫的 QPS,例如在多個 service 方法中重復(fù)調(diào)用某個 Model 的查詢時可以使用緩存技術(shù),多次調(diào)用時只使用一次結(jié)果。
為了避免多次查詢,可以通過緩存來保存查詢結(jié)果,多次調(diào)用同一個查詢時就可以直接使用緩存結(jié)果,從而降低數(shù)據(jù)庫的 QPS。使用這種方法的時候,還需要考慮到緩存的刷新問題,例如,在更新數(shù)據(jù)時,可以在 update 中添加一個 Hooks,更新時自動刷新緩存。
合理使用 Hooks
使用 Hooks 是在使用 ORM 時常見的問題。Hooks 可以大大簡化代碼,例如我們可以根據(jù)文檔內(nèi)容是否更改來更新其更新時間等字段。在某些復(fù)雜情況下,我們可能需要在一個 Model 中的 Hooks 中調(diào)用另一個 Model 的增刪改操作,例如創(chuàng)建文檔后可能需要更新 Book 的操作并觸發(fā)相應(yīng)字段的更新,這時我們需要考慮它們之間的關(guān)聯(lián)關(guān)系和更新順序。
當(dāng)我們的業(yè)務(wù)越來越復(fù)雜時,例如在更新 Repo 的時候可能需要觸發(fā)一些 service 方法,而這些 service 方法可能會在 Hooks 方法中直接調(diào)用,導(dǎo)致調(diào)用鏈越來越長,越來越復(fù)雜。這種不規(guī)范的寫法會使得代碼的復(fù)雜度非常難以控制,甚至可能出現(xiàn)循環(huán)調(diào)用等問題,給新手帶來極大的困擾。因此,在使用 Hooks 時需要非常謹(jǐn)慎,避免出現(xiàn)類似的問題。使用 Hooks 需要謹(jǐn)慎,雖然在平時使用時會感覺很方便,但是當(dāng)需要重構(gòu)或進(jìn)行技術(shù)重構(gòu)時,就可能會遇到困難,甚至引起災(zāi)難級事故。
查詢優(yōu)化
此外,在使用 ORM 工具時,需要進(jìn)行查詢優(yōu)化,因?yàn)?ORM 只能根據(jù)輸入?yún)?shù)做一些簡單的優(yōu)化處理,而對于一些極限情況,需要開發(fā)人員自己去注意。例如,在進(jìn)行 SQL 查詢用 in 時,由于條件長度較長,可能會因?yàn)閿?shù)據(jù)庫引擎的原因?qū)е?SQL 無法執(zhí)行或執(zhí)行效率較低,此時需要將查詢條件進(jìn)行分組,利用 Node.js 進(jìn)行分批查詢,并在內(nèi)存中組裝結(jié)果。
在 ORM 的使用中,需要注意不正確使用 ORM 的 API 調(diào)用可能會導(dǎo)致生成子查詢,從而降低查詢性能。
最后
最后,推薦大家閱讀 ORM 的源碼(https://leoric.js.org/)和《企業(yè)應(yīng)用架構(gòu)模式》(https://martinfowler.com/eaaCatalog/index.html),尤其是其中介紹的兩種架構(gòu)模式。

低代碼是當(dāng)今最熱門的技術(shù)之一,如果你對低代碼感興趣,或者正在研究低代碼,歡迎報名第 63 屆早早聊大會 - 低代碼無代碼,跟早早聊一起,玩轉(zhuǎn)低代碼。
舉辦時間:2023 年 4 月 15 日 ?10:00 ~ 17:00
截至?xí)r間:2023 年 4 月 15 日 ?19:00
舉辦方式:微信群 PPT 推送 + 線上視頻實(shí)時直播 + 會后資料推送
報名方式:https://www.zaozao.run/conf/c63
大會主辦方:前端早早聊
