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,我們可以用 a
與 b
兩個(gè) Class 描述兩張表再层,同時(shí)利用 ManyToMany
裝飾器分別修飾 a
與 b
的兩個(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
from
等 methods
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è)步驟:
- 創(chuàng)建 context 存儲(chǔ)結(jié)構(gòu)化 query 信息。
- 創(chuàng)建 builder 供用戶鏈?zhǔn)綍鴮懘a同時(shí)填充 context章喉。
- 通過若干個(gè) SQL 子處理函數(shù)加上幾個(gè)主 statement 函數(shù)將其串聯(lián)起來生成最終 query汗贫。
最后在設(shè)計(jì)時(shí)考慮到 SQL 方言的話,可以將模塊拆成 核心秸脱、SQL落包、若干個(gè)方言庫(kù),方言庫(kù)基于核心庫(kù)做拓展即可摊唇。
5 更多討論
如果你想?yún)⑴c討論咐蝇,請(qǐng)點(diǎn)擊這里,每周都有新的主題巷查,周末或周一發(fā)布有序。