1 引言

前端精读《手写 SQL 编译器系列》 介绍了如何利用 SQL 生成语法树,而还有一些库的作用是根据语法树生成 SQL 语句。

除此之外,还有一种库,是根据编程语言生成 SQL。sqorn 就是一个这样的库。

可能有人会问,利用编程语言生成 SQL 有什么意义?既没有语法树规范,也不如直接写 SQL 通用。对,有利就有弊,这些库不遵循语法树,但利用简化的对象模型快速生成 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) =&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 frommethods 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

精读《sqorn 源码》的更多相关文章

  1. 精读《V8 引擎 Lazy Parsing》

    1. 引言 本周精读的文章是 V8 引擎 Lazy Parsing,看看 V8 引擎为了优化性能,做了怎样的尝试吧! 这篇文章介绍的优化技术叫 preparser,是通过跳过不必要函数编译的方式优化性 ...

  2. 深入浏览器工作原理和JS引擎(V8引擎为例)

    浏览器工作原理和JS引擎 1.浏览器工作原理 在浏览器中输入查找内容,浏览器是怎样将页面加载出来的?以及JavaScript代码在浏览器中是如何被执行的? 大概流程可观察以下图: 首先,用户在浏览器搜 ...

  3. [翻译] V8引擎的解析

    原文:Parsing in V8 explained 本文档介绍了 V8 引擎是如何解析 JavaScript 源代码的,以及我们将改进它的计划. 动机 我们有个解析器和一个更快的预解析器(~2x), ...

  4. 一文搞懂V8引擎的垃圾回收

    引言 作为目前最流行的JavaScript引擎,V8引擎从出现的那一刻起便广泛受到人们的关注,我们知道,JavaScript可以高效地运行在浏览器和Nodejs这两大宿主环境中,也是因为背后有强大的V ...

  5. Chrome V8引擎系列随笔 (1):Math.Random()函数概览

    先让大家来看一幅图,这幅图是V8引擎4.7版本和4.9版本Math.Random()函数的值的分布图,我可以这么理解 .从下图中,也许你会认为这是个二维码?其实这幅图告诉我们一个道理,第二张图的点的分 ...

  6. (译)V8引擎介绍

    V8是什么? V8是谷歌在德国研发中心开发的一个JavaScript引擎.开源并且用C++实现.可以用于运行于客户端和服务端的Javascript程序. V8设计的初衷是为了提高浏览器上JavaScr ...

  7. 浅谈Chrome V8引擎中的垃圾回收机制

    垃圾回收器 JavaScript的垃圾回收器 JavaScript使用垃圾回收机制来自动管理内存.垃圾回收是一把双刃剑,其好处是可以大幅简化程序的内存管理代码,降低程序员的负担,减少因 长时间运转而带 ...

  8. V8引擎嵌入指南

    如果已读过V8编程入门那你已经熟悉了如句柄(handle).作用域(scope)和上下文(context)之类的关键概念,以及如何将V8引擎作为一个独立的虚拟机来使用.本文将进一步讨论这些概念,并介绍 ...

  9. 浅谈V8引擎中的垃圾回收机制

    最近在看<深入浅出nodejs>关于V8垃圾回收机制的章节,转自:http://blog.segmentfault.com/skyinlayer/1190000000440270 这篇文章 ...

  10. 深入出不来nodejs源码-V8引擎初探

    原本打算是把node源码看得差不多了再去深入V8的,但是这两者基本上没办法分开讲. 与express是基于node的封装不同,node是基于V8的一个应用,源码内容已经渗透到V8层面,因此这章简述一下 ...

随机推荐

  1. Codeforces Round #375 (Div. 2) Polycarp at the Radio 优先队列模拟题 + 贪心

    http://codeforces.com/contest/723/problem/C 题目是给出一个序列 a[i]表示第i个歌曲是第a[i]个人演唱,现在选出前m个人,记b[j]表示第j个人演唱歌曲 ...

  2. Java集合——集合框架Set接口

    1.Set接口 一个不包含重复元素的collecyion.更确切的讲,set不包含满足e1.equals(e2)的元素e1和e2,并且最多包含一个null元素. 2.HashSet 类实现Set接口, ...

  3. docker的卸载方法

    卸载: 1.查询docker安装过的包: yum list installed | grep docker 2.删除安装包: yum remove docker-ce.x86_64 ddocker-c ...

  4. 傻瓜式的go modules的讲解和代码,及gomod能不能引入另一个gomod和gomod的use of internal package xxxx not allowed

    一 国内关于gomod的文章,哪怕是使用了百度 -csdn,依然全是理论,虽然golang的使用者大多是大神但是也有像我这样的的弱鸡是不是? 所以,我就写个傻瓜式教程了. github地址:https ...

  5. Java 编码规范有感

    应小组要求,开发测试都需要考阿里编码规范,因此,相当于是突击了一下关于编码规范方面的知识,目前做的项目后期需要进行项目迁移,数据迁移,功能迁移... 各种迁移... 阿里巴巴编码规范(Java)考试地 ...

  6. 从零开始的全栈工程师——js篇2.9(this详解)

    this 一.this是js的一个关键字 指定一个对象然后去替代他    只研究函数内的this 分两种 函数内的this和函数外的this1)函数内的this指向行为发生的主体2)函数外的this都 ...

  7. while循环,break和continue,运算符,格式化输出

    一丶while循环 while条件: 代码块(循环体) #数数 打印1-100 count = 1 while count <= 100: print(count) count += 1 执行顺 ...

  8. Java框架:spring框架整合hibernate框架的xml配置(使用注解的方式)

    <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.sp ...

  9. Android商城开发系列(十三)—— 首页热卖商品布局实现

    热卖商品布局效果如下图: 这个布局跟我们上节做的推荐是一样的,也是用LinearLayout和GridView去实现的,新建一个hot_item.xml,代码如下所示: <?xml versio ...

  10. 利用expect实现自动化操作

    管理机上需要安装expect包 yum -y install expect 1.定义主机ip [root@localhost ~]# cat ip.txt 192.168.1.12 192.168.1 ...