精讀《sqorn 源碼》

1 引言

前端精讀《手寫 SQL 編譯器系列》 介紹了如何利用 SQL 生成語(yǔ)法樹,而還有一些庫(kù)的作用是根據(jù)語(yǔ)法樹生成 SQL 語(yǔ)句野芒。

除此之外赖阻,還有一種庫(kù),是根據(jù)編程語(yǔ)言生成 SQL浦楣。sqorn 就是一個(gè)這樣的庫(kù)袖肥。

可能有人會(huì)問,利用編程語(yǔ)言生成 SQL 有什么意義振劳?既沒有語(yǔ)法樹規(guī)范椎组,也不如直接寫 SQL 通用。對(duì)历恐,有利就有弊寸癌,這些庫(kù)不遵循語(yǔ)法樹,但利用簡(jiǎn)化的對(duì)象模型快速生成 SQL弱贼,使得代碼抽象程度得到了提高蒸苇。而代碼抽象程度得到提高,第一個(gè)好處就是易讀吮旅,第二個(gè)好處就是易操作溪烤。

數(shù)據(jù)庫(kù)特別容易抽象為面向?qū)ο竽P停鴮?duì)數(shù)據(jù)庫(kù)的操作語(yǔ)句 - SQL 是一種結(jié)構(gòu)化查詢語(yǔ)句鸟辅,只能描述一段一段的查詢氛什,而面向?qū)ο竽P蛥s適合描述一個(gè)整體,將數(shù)據(jù)庫(kù)多張表串聯(lián)起來匪凉。

舉個(gè)例子枪眉,利用 typeorm,我們可以用 ab 兩個(gè) Class 描述兩張表再层,同時(shí)利用 ManyToMany 裝飾器分別修飾 ab 的兩個(gè)字段贸铜,將其建立起 多對(duì)多的關(guān)聯(lián)堡纬,而這個(gè)映射到 SQL 結(jié)構(gòu)是三張表,還有一張是中間表 ab蒿秦,以及查詢時(shí)涉及到的 left join 操作烤镐,而在 typeorm 中,一條 find 語(yǔ)句就能連帶查詢處多對(duì)多關(guān)聯(lián)關(guān)系棍鳖。

這就是這種利用編程語(yǔ)言生成 SQL 庫(kù)的價(jià)值炮叶,所以本周我們分析一下 sqorn 這個(gè)庫(kù)的源碼,看看利用對(duì)象模型生成 SQL 需要哪些步驟渡处。

2 概述

我們先看一下 sqorn 的語(yǔ)法镜悉。

const sq = require("sqorn-pg")();

const Person = sq`person`,
  Book = sq`book`;

// SELECT
const children = await Person`age < ${13}`;
// "select * from person where age < 13"

// DELETE
const [deleted] = await Book.delete({ id: 7 })`title`;
// "delete from book where id = 7 returning title"

// INSERT
await Person.insert({ firstName: "Rob" });
// "insert into person (first_name) values ('Rob')"

// UPDATE
await Person({ id: 23 }).set({ name: "Rob" });
// "update person set name = 'Rob' where id = 23"

首先第一行的 sqorn-pg 告訴我們 sqorn 按照 SQL 類型拆成不同分類的小包,這是因?yàn)椴煌瑪?shù)據(jù)庫(kù)支持的方言不同医瘫,sqorn 希望在語(yǔ)法上抹平數(shù)據(jù)庫(kù)間差異侣肄。

其次 sqorn 也是利用面向?qū)ο笏季S的,上面的例子通過 <code>sq`person`</code> 生成了 Person 實(shí)例醇份,實(shí)際上也對(duì)應(yīng)了 person 表稼锅,然后 <code>Person`age < ${13}`</code> 表示查詢:select * from person where age < 13

上面是利用 ES6 模板字符串的功能實(shí)現(xiàn)的簡(jiǎn)化 where 查詢功能,sqorn 主要還是利用一些函數(shù)完成 SQL 語(yǔ)句生成僚纷,比如 where delete insert 等等矩距,比較典型的是下面的 Example:

sq.from`book`.return`distinct author`
  .where({ genre: "Fantasy" })
  .where({ language: "French" });
// select distinct author from book
// where language = 'French' and genre = 'Fantsy'

所以我們閱讀 sqorn 源碼,探討如何利用實(shí)現(xiàn)上面的功能怖竭。

3 精讀

我們從四個(gè)方面入手剩晴,講明白 sqorn 的源碼是如何組織的,以及如何滿足上面功能的侵状。

方言

為了實(shí)現(xiàn)各種 SQL 方言,需要在實(shí)現(xiàn)功能之前毅整,將代碼拆分為內(nèi)核代碼與拓展代碼趣兄。

內(nèi)核代碼就是 sqorn-sql 而拓展代碼就是 sqorn-pg,拓展代碼自身只要實(shí)現(xiàn) pg 數(shù)據(jù)庫(kù)自身的特殊邏輯悼嫉, 加上 sqorn-sql 提供的核心能力艇潭,就能形成完整的 pg SQL 生成功能。

實(shí)現(xiàn)數(shù)據(jù)庫(kù)連接

sqorn 不但生成 query 語(yǔ)句戏蔑,也會(huì)參與數(shù)據(jù)庫(kù)連接與運(yùn)行蹋凝,因此方言庫(kù)的一個(gè)重要功能就是做數(shù)據(jù)庫(kù)連接。sqorn 利用 pg 這個(gè)庫(kù)實(shí)現(xiàn)了連接池总棵、斷開鳍寂、查詢、事務(wù)的功能情龄。

覆寫接口函數(shù)

內(nèi)核代碼想要具有拓展能力迄汛,暴露出一些接口讓 sqorn-xx 覆寫是很基本的捍壤。

context

內(nèi)核代碼中,最重要的就是 context 屬性鞍爱,因?yàn)槿祟惲?xí)慣一步一步寫代碼鹃觉,而最終生成的 query 語(yǔ)句是連貫的,所以這個(gè)上下文對(duì)象通過 updateContext 存儲(chǔ)了每一條信息:

{
  name: 'limit',
  updateContext: (ctx, args) => {
    ctx.lim = args
  }
}

{
  name: 'where',
  updateContext: (ctx, args) => {
    ctx.whr.push(args)
  }
}

比如 Person.where({ name: 'bob' }) 就會(huì)調(diào)用 ctx.whr.push({ name: 'bob' })睹逃,因?yàn)?where 條件是個(gè)數(shù)組盗扇,因此這里用 push,而 limit 一般僅有一個(gè)沉填,所以 context 對(duì) lim 對(duì)象的存儲(chǔ)僅有一條疗隶。

其他操作諸如 where delete insert with from 都會(huì)類似轉(zhuǎn)化為 updateContext,最終更新到 context 中拜轨。

創(chuàng)建 builder

不用太關(guān)心下面的 sqorn-xx 包名細(xì)節(jié)抽减,這一節(jié)主要目的是說明如何實(shí)現(xiàn) Demo 中的鏈?zhǔn)秸{(diào)用,至于哪個(gè)模塊放在哪并不重要(如果要自己造輪子就要仔細(xì)學(xué)習(xí)一下作者的命名方式)橄碾。

sqorn-core 代碼中創(chuàng)建了 builder 對(duì)象卵沉,將 sqorn-sql 中創(chuàng)建的 methods merge 到其中,因此我們可以使用 sq.where 這種語(yǔ)法法牲。而為什么可以 sq.where().limit() 這樣連續(xù)調(diào)用呢史汗?可以看下面的代碼:

for (const method of methods) {
  // add function call methods
  builder[name] = function(...args) {
    return this.create({ name, args, prev: this.method });
  };
}

這里將 where delete insert with frommethods merge 到 builder 對(duì)象中,且當(dāng)其執(zhí)行完后拒垃,通過 this.create() 返回一個(gè)新 builder停撞,從而完成了鏈?zhǔn)秸{(diào)用功能。

生成 query

上面三點(diǎn)講清楚了如何支持方言悼瓮、用戶代碼內(nèi)容都收集到 context 中了戈毒,而且我們還創(chuàng)建了可以鏈?zhǔn)秸{(diào)用的 builder 對(duì)象方便用戶調(diào)用,那么只剩最后一步了横堡,就是生成 query埋市。

為了利用 context 生成 query,我們需要對(duì)每個(gè) key 編寫對(duì)應(yīng)的函數(shù)做處理命贴,拿 limit 舉例:

export default ctx => {
  if (!ctx.lim) return;
  const txt = build(ctx, ctx.lim);
  return txt && `limit ${txt}`;
};

context.lim 拿取 limit 配置道宅,組合成 limit xxx 的字符串并返回就可以了。

build 函數(shù)是個(gè)工具函數(shù)胸蛛,如果 ctx.lim 是個(gè)數(shù)組污茵,就會(huì)用逗號(hào)拼接。

大部分操作比如 delete from having 都做這么簡(jiǎn)單的處理即可葬项,但像 where 會(huì)相對(duì)復(fù)雜泞当,因?yàn)閮?nèi)部包含了 condition 子語(yǔ)法,注意用 and 拼接即可民珍。

最后是順序零蓉,也需要在代碼中確定:

export default {
  sql: query(sql),
  select: query(wth, select, from, where, group, having, order, limit, offset),
  delete: query(wth, del, where, returning),
  insert: query(wth, insert, value, returning),
  update: query(wth, update, set, where, returning)
};

這個(gè)意思是笤受,一個(gè) select 語(yǔ)句會(huì)通過 wth, select, from, where, group, having, order, limit, offset 的順序調(diào)用處理函數(shù),返回的值就是最終的 query敌蜂。

4 總結(jié)

通過源碼分析箩兽,可以看到制作一個(gè)這樣的庫(kù)有三個(gè)步驟:

  1. 創(chuàng)建 context 存儲(chǔ)結(jié)構(gòu)化 query 信息。
  2. 創(chuàng)建 builder 供用戶鏈?zhǔn)綍鴮懘a同時(shí)填充 context章喉。
  3. 通過若干個(gè) SQL 子處理函數(shù)加上幾個(gè)主 statement 函數(shù)將其串聯(lián)起來生成最終 query汗贫。

最后在設(shè)計(jì)時(shí)考慮到 SQL 方言的話,可以將模塊拆成 核心秸脱、SQL落包、若干個(gè)方言庫(kù),方言庫(kù)基于核心庫(kù)做拓展即可摊唇。

5 更多討論

討論地址是:精讀《sqorn 源碼》 · Issue #103 · dt-fe/weekly

如果你想?yún)⑴c討論咐蝇,請(qǐng)點(diǎn)擊這里,每周都有新的主題巷查,周末或周一發(fā)布有序。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市岛请,隨后出現(xiàn)的幾起案子旭寿,更是在濱河造成了極大的恐慌,老刑警劉巖崇败,帶你破解...
    沈念sama閱讀 219,188評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件盅称,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡后室,警方通過查閱死者的電腦和手機(jī)缩膝,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來岸霹,“玉大人逞盆,你說我怎么就攤上這事∷缮辏” “怎么了?”我有些...
    開封第一講書人閱讀 165,562評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵俯逾,是天一觀的道長(zhǎng)贸桶。 經(jīng)常有香客問我,道長(zhǎng)桌肴,這世上最難降的妖魔是什么皇筛? 我笑而不...
    開封第一講書人閱讀 58,893評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮坠七,結(jié)果婚禮上水醋,老公的妹妹穿的比我還像新娘旗笔。我一直安慰自己,他們只是感情好拄踪,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,917評(píng)論 6 392
  • 文/花漫 我一把揭開白布蝇恶。 她就那樣靜靜地躺著,像睡著了一般惶桐。 火紅的嫁衣襯著肌膚如雪撮弧。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,708評(píng)論 1 305
  • 那天姚糊,我揣著相機(jī)與錄音贿衍,去河邊找鬼。 笑死救恨,一個(gè)胖子當(dāng)著我的面吹牛贸辈,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播肠槽,決...
    沈念sama閱讀 40,430評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼擎淤,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了署浩?” 一聲冷哼從身側(cè)響起揉燃,我...
    開封第一講書人閱讀 39,342評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎筋栋,沒想到半個(gè)月后炊汤,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,801評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡弊攘,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,976評(píng)論 3 337
  • 正文 我和宋清朗相戀三年抢腐,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片襟交。...
    茶點(diǎn)故事閱讀 40,115評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡迈倍,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出捣域,到底是詐尸還是另有隱情啼染,我是刑警寧澤,帶...
    沈念sama閱讀 35,804評(píng)論 5 346
  • 正文 年R本政府宣布焕梅,位于F島的核電站迹鹅,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏贞言。R本人自食惡果不足惜斜棚,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,458評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧弟蚀,春花似錦蚤霞、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,008評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至断医,卻和暖如春滞乙,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背鉴嗤。 一陣腳步聲響...
    開封第一講書人閱讀 33,135評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工斩启, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人醉锅。 一個(gè)月前我還...
    沈念sama閱讀 48,365評(píng)論 3 373
  • 正文 我出身青樓兔簇,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親硬耍。 傳聞我的和親對(duì)象是個(gè)殘疾皇子垄琐,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,055評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容