天天看點

精讀《sqorn 源碼》

1 引言

前端精讀

《手寫 SQL 編譯器系列》

介紹了如何利用 SQL 生成文法樹,而還有一些庫的作用是根據文法樹生成 SQL 語句。

除此之外,還有一種庫,是根據程式設計語言生成 SQL。

sqorn

就是一個這樣的庫。

可能有人會問,利用程式設計語言生成 SQL 有什麼意義?既沒有文法樹規範,也不如直接寫 SQL 通用。對,有利就有弊,這些庫不遵循文法樹,但利用簡化的對象模型快速生成 SQL,使得代碼抽象程度得到了提高。而代碼抽象程度得到提高,第一個好處就是易讀,第二個好處就是易操作。

資料庫特别容易抽象為面向對象模型,而對資料庫的操作語句 - SQL 是一種結構化查詢語句,隻能描述一段一段的查詢,而面向對象模型卻适合描述一個整體,将資料庫多張表串聯起來。

舉個例子,利用

typeorm

,我們可以用

a

b

兩個 Class 描述兩張表,同時利用

ManyToMany

裝飾器分别修飾

a

b

的兩個字段,将其建立起 多對多的關聯,而這個映射到 SQL 結構是三張表,還有一張是中間表

ab

,以及查詢時涉及到的 left join 操作,而在 typeorm 中,一條

find

語句就能連帶查詢處多對多關聯關系。

這就是這種利用程式設計語言生成 SQL 庫的價值,是以本周我們分析一下

這個庫的源碼,看看利用對象模型生成 SQL 需要哪些步驟。

2 概述

我們先看一下 sqorn 的文法。

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 類型拆成不同分類的小包,這是因為不同資料庫支援的方言不同,sqorn 希望在文法上抹平資料庫間差異。

其次 sqorn 也是利用面向對象思維的,上面的例子通過

sq`person`

生成了 Person 執行個體,實際上也對應了 person 表,然後

Person`age < ${13}`

表示查詢:

select * from person where age < 13

上面是利用 ES6 模闆字元串的功能實作的簡化 where 查詢功能,sqorn 主要還是利用一些函數完成 SQL 語句生成,比如

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 源碼,探讨如何利用實作上面的功能。

3 精讀

我們從四個方面入手,講明白 sqorn 的源碼是如何組織的,以及如何滿足上面功能的。

方言

為了實作各種 SQL 方言,需要在實作功能之前,将代碼拆分為核心代碼與拓展代碼。

核心代碼就是

sqorn-sql

而拓展代碼就是

sqorn-pg

,拓展代碼自身隻要實作 pg 資料庫自身的特殊邏輯, 加上

sqorn-sql

提供的核心能力,就能形成完整的 pg SQL 生成功能。

實作資料庫連接配接

sqorn 不但生成 query 語句,也會參與資料庫連接配接與運作,是以方言庫的一個重要功能就是做資料庫連接配接。sqorn 利用

pg

這個庫實作了連接配接池、斷開、查詢、事務的功能。

覆寫接口函數

核心代碼想要具有拓展能力,暴露出一些接口讓

sqorn-xx

覆寫是很基本的。

context

核心代碼中,最重要的就是 context 屬性,因為人類習慣一步一步寫代碼,而最終生成的 query 語句是連貫的,是以這個上下文對象通過

updateContext

存儲了每一條資訊:

{
  name: 'limit',
  updateContext: (ctx, args) =&gt; {
    ctx.lim = args
  }
}

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

比如

Person.where({ name: 'bob' })

就會調用

ctx.whr.push({ name: 'bob' })

,因為 where 條件是個數組,是以這裡用

push

,而

limit

一般僅有一個,是以 context 對

lim

對象的存儲僅有一條。

其他操作諸如

where

delete

insert

with

from

都會類似轉化為

updateContext

,最終更新到 context 中。

建立 builder

不用太關心下面的

sqorn-xx

包名細節,這一節主要目的是說明如何實作 Demo 中的鍊式調用,至于哪個子產品放在哪并不重要(如果要自己造輪子就要仔細學習一下作者的命名方式)。

sqorn-core

代碼中建立了

builder

對象,将

sqorn-sql

中建立的

methods

merge 到其中,是以我們可以使用

sq.where

這種文法。而為什麼可以

sq.where().limit()

這樣連續調用呢?可以看下面的代碼:

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

對象中,且當其執行完後,通過

this.create()

傳回一個新

builder

,進而完成了鍊式調用功能。

生成 query

上面三點講清楚了如何支援方言、使用者代碼内容都收集到 context 中了,而且我們還建立了可以鍊式調用的

builder

對象友善使用者調用,那麼隻剩最後一步了,就是生成 query。

為了利用 context 生成 query,我們需要對每個 key 編寫對應的函數做處理,拿

limit

舉例:

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

context.lim

拿取

limit

配置,組合成

limit xxx

的字元串并傳回就可以了。

build

函數是個工具函數,如果 ctx.lim 是個數組,就會用逗号拼接。

大部分操作比如

delete

from

having

都做這麼簡單的處理即可,但像

where

會相對複雜,因為内部包含了

condition

子文法,注意用

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)
};
           

這個意思是,一個

select

語句會通過

wth, select, from, where, group, having, order, limit, offset

的順序調用處理函數,傳回的值就是最終的 query。

4 總結

通過源碼分析,可以看到制作一個這樣的庫有三個步驟:

  1. 建立 context 存儲結構化 query 資訊。
  2. 建立 builder 供使用者鍊式書寫代碼同時填充 context。
  3. 通過若幹個 SQL 子處理函數加上幾個主 statement 函數将其串聯起來生成最終 query。

最後在設計時考慮到 SQL 方言的話,可以将子產品拆成 核心、SQL、若幹個方言庫,方言庫基于核心庫做拓展即可。

5 更多讨論

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

如果你想參與讨論,請

點選這裡

,每周都有新的主題,周末或周一釋出。

原文位址:https://segmentfault.com/a/1190000016419523