摘要:引言前端精读手写编译器系列介绍了如何利用生成语法树,而还有一些库的作用是根据语法树生成语句。对,有利就有弊,这些库不遵循语法树,但利用简化的对象模型快速生成,使得代码抽象程度得到了提高。
1 引言
前端精读《手写 SQL 编译器系列》 介绍了如何利用 SQL 生成语法树,而还有一些库的作用是根据语法树生成 SQL 语句。
除此之外,还有一种库,是根据编程语言生成 SQL。sqorn 就是一个这样的库。
可能有人会问,利用编程语言生成 SQL 有什么意义?既没有语法树规范,也不如直接写 SQL 通用。对,有利就有弊,这些库不遵循语法树,但利用简化的对象模型快速生成 SQL,使得代码抽象程度得到了提高。而代码抽象程度得到提高,第一个好处就是易读,第二个好处就是易操作。
数据库特别容易抽象为面向对象模型,而对数据库的操作语句 - SQL 是一种结构化查询语句,只能描述一段一段的查询,而面向对象模型却适合描述一个整体,将数据库多张表串联起来。
举个例子,利用 typeorm,我们可以用 a 与 b 两个 Class 描述两张表,同时利用 ManyToMany 装饰器分别修饰 a 与 b 的两个字段,将其建立起 多对多的关联,而这个映射到 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 from 等 methods 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 总结通过源码分析,可以看到制作一个这样的库有三个步骤:
创建 context 存储结构化 query 信息。
创建 builder 供用户链式书写代码同时填充 context。
通过若干个 SQL 子处理函数加上几个主 statement 函数将其串联起来生成最终 query。
最后在设计时考虑到 SQL 方言的话,可以将模块拆成 核心、SQL、若干个方言库,方言库基于核心库做拓展即可。
5 更多讨论讨论地址是:精读《sqorn 源码》 · Issue #103 · dt-fe/weekly
如果你想参与讨论,请点击这里,每周都有新的主题,周末或周一发布。
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/97831.html
摘要:会自动触发函数内回调函数的执行。因此利用并将依赖置为使代码在所有渲染周期内,只在初始化执行一次。同时代码里还对等公共方法进行了包装,让这些回调函数中自带效果。前端精读帮你筛选靠谱的内容。 1. 引言 react-easy-state 是个比较有趣的库,利用 Proxy 创建了一个非常易用的全局数据流管理方式。 import React from react; import { stor...
摘要:引言本周精读的源码是这个库。这个库的目的是为了实现的依赖注入。精读那么开始源码的解析,首先是整体思路的分析。讨论地址是精读源码如果你想参与讨论,请点击这里,每周都有新的主题,周末或周一发布。前端精读帮你筛选靠谱的内容。 1. 引言 本周精读的源码是 inject-instance 这个库。 这个库的目的是为了实现 Class 的依赖注入。 比如我们通过 inject 描述一个成员变量,...
摘要:引言是一个版语法解析器生成器,具有分词语法树解析的能力。实现函数用链表设计函数是最佳的选择,我们要模拟调用栈了。但光标所在的位置是期望输入点,这个输入点也应该参与语法树的生成,而错误提示不包含光标,所以我们要执行两次。 1. 引言 syntax-parser 是一个 JS 版语法解析器生成器,具有分词、语法树解析的能力。 通过两个例子介绍它的功能。 第一个例子是创建一个词法解析器 my...
摘要:精读源码一共行,我们分析一下其精妙的方式。更多讨论讨论地址是精读新用法如果你想参与讨论,请点击这里,每周都有新的主题,周末或周一发布。前端精读帮你筛选靠谱的内容。 1 引言 很高兴这一期的话题是由 epitath 的作者 grsabreu 提供的。 前端发展了 20 多年,随着发展中国家越来越多的互联网从业者涌入,现在前端知识玲琅满足,概念、库也越来越多。虽然内容越来越多,但作为个体的...
阅读 2518·2021-09-09 09:33
阅读 2877·2019-08-30 15:56
阅读 3163·2019-08-30 14:21
阅读 914·2019-08-30 13:01
阅读 880·2019-08-26 18:27
阅读 3597·2019-08-26 13:47
阅读 3468·2019-08-26 10:26
阅读 1600·2019-08-23 18:38