前端精读《手写 SQL 编译器系列》 介绍了如何利用 SQL 生成语法树,而还有一些库的做用是根据语法树生成 SQL 语句。前端
除此以外,还有一种库,是根据编程语言生成 SQL。sqorn 就是一个这样的库。git
可能有人会问,利用编程语言生成 SQL 有什么意义?既没有语法树规范,也不如直接写 SQL 通用。对,有利就有弊,这些库不遵循语法树,但利用简化的对象模型快速生成 SQL,使得代码抽象程度获得了提升。而代码抽象程度获得提升,第一个好处就是易读,第二个好处就是易操做。github
数据库特别容易抽象为面向对象模型,而对数据库的操做语句 - SQL 是一种结构化查询语句,只能描述一段一段的查询,而面向对象模型却适合描述一个总体,将数据库多张表串联起来。sql
举个例子,利用 typeorm,咱们能够用 a
与 b
两个 Class 描述两张表,同时利用 ManyToMany
装饰器分别修饰 a
与 b
的两个字段,将其创建起 多对多的关联,而这个映射到 SQL 结构是三张表,还有一张是中间表 ab
,以及查询时涉及到的 left join 操做,而在 typeorm 中,一条 find
语句就能连带查询处多对多关联关系。数据库
这就是这种利用编程语言生成 SQL 库的价值,因此本周咱们分析一下 sqorn 这个库的源码,看看利用对象模型生成 SQL 须要哪些步骤。编程
咱们先看一下 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 源码,探讨如何利用实现上面的功能。
咱们从四个方面入手,讲明白 sqorn 的源码是如何组织的,以及如何知足上面功能的。
为了实现各类 SQL 方言,须要在实现功能以前,将代码拆分为内核代码与拓展代码。
内核代码就是 sqorn-sql
而拓展代码就是 sqorn-pg
,拓展代码自身只要实现 pg 数据库自身的特殊逻辑, 加上 sqorn-sql
提供的核心能力,就能造成完整的 pg SQL 生成功能。
实现数据库链接
sqorn 不但生成 query 语句,也会参与数据库链接与运行,所以方言库的一个重要功能就是作数据库链接。sqorn 利用 pg
这个库实现了链接池、断开、查询、事务的功能。
覆写接口函数
内核代码想要具备拓展能力,暴露出一些接口让 sqorn-xx
覆写是很基本的。
内核代码中,最重要的就是 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 中。
不用太关心下面的 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
,从而完成了链式调用功能。
上面三点讲清楚了如何支持方言、用户代码内容都收集到 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。
经过源码分析,能够看到制做一个这样的库有三个步骤:
最后在设计时考虑到 SQL 方言的话,能够将模块拆成 核心、SQL、若干个方言库,方言库基于核心库作拓展便可。
若是你想参与讨论,请点击这里,每周都有新的主题,周末或周一发布。