精讀《sqorn 源碼》

1 引言

前端精讀《手寫 SQL 編譯器系列》 介紹瞭如何利用 SQL 生成語法樹,而還有一些庫的做用是根據語法樹生成 SQL 語句。前端

除此以外,還有一種庫,是根據編程語言生成 SQL。sqorn 就是一個這樣的庫。git

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

數據庫特別容易抽象爲面向對象模型,而對數據庫的操做語句 - SQL 是一種結構化查詢語句,只能描述一段一段的查詢,而面向對象模型卻適合描述一個總體,將數據庫多張表串聯起來。sql

舉個例子,利用 typeorm,咱們能夠用 ab 兩個 Class 描述兩張表,同時利用 ManyToMany 裝飾器分別修飾 ab 的兩個字段,將其創建起 多對多的關聯,而這個映射到 SQL 結構是三張表,還有一張是中間表 ab,以及查詢時涉及到的 left join 操做,而在 typeorm 中,一條 find 語句就能連帶查詢處多對多關聯關係。數據庫

這就是這種利用編程語言生成 SQL 庫的價值,因此本週咱們分析一下 sqorn 這個庫的源碼,看看利用對象模型生成 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) => {
    ctx.lim = args
  }
}

{
  name: 'where',
  updateContext: (ctx, args) => {
    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 frommethods merge 到 builder 對象中,且當其執行完後,經過 this.create() 返回一個新 builder,從而完成了鏈式調用功能。

生成 query

上面三點講清楚瞭如何支持方言、用戶代碼內容都收集到 context 中了,並且咱們還建立了能夠鏈式調用的 builder 對象方便用戶調用,那麼只剩最後一步了,就是生成 query。

爲了利用 context 生成 query,咱們須要對每一個 key 編寫對應的函數作處理,拿 limit 舉例:

export default ctx => {
  if (!ctx.lim) return;
  const txt = build(ctx, ctx.lim);
  return txt && `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

若是你想參與討論,請點擊這裏,每週都有新的主題,週末或週一發佈。

相關文章
相關標籤/搜索