RestQL:高效的 API 开发方式
发布于 4 个月前 作者 crzidea 1316 次浏览 来自 分享

本文已在美团点评技术博客发表,谢绝转载! 作者原文链接:https://crzidea.com/2016/08/12/koa-restql/ koa-restql 已经在 github 开源并在 npm 发布。感兴趣的同学可以前往围观一下。欢迎 Pull Request,同时热烈欢迎 Star。

在现代的业务系统中,后端开发工作基本上可以被拆分为三项:

  • 接口鉴权。例如拍段是不是当前系统的用户,以及该用户是否有权限访问接口。
  • 与其他系统的交互。例如调用第三方的服务,或内部搭建的其他服务。
  • 数据操作。基本上所有需要持久化存储的系统都会在这项工作上耗费大量时间。

本文将介绍如何利用 RestQL 来非常有效地减少「数据操作」相关的工作量。

现状与挑战

我们先来做个假设。

  • 假设系统中有 60 张表,每张表对应的接口都要有四种 CRUD 的 API。那么就需要后端工程师写60 * 4 = 240个API。
  • 假设上述 60 张表中,40 张表存的是资源类的数据,其余 20 张表为关系类的数据,也就是说每张表和 20 张表都要进行关联,每个关联也需要四种 CRUD 操作,那么又要增加40 * 20 * 4 = 3200个API。

所以在上述假设场景中,后端工程师要编写 3200 + 240 = 3440 个 API。而且这还不是全部。假如后端代码需要 100% 的测试覆盖,那么工程师们就要写至少 3440 个测试!

60 张表 = 3440 个 API + 3440 个单元测试

众所周知,数据操作 API 的实现过程基本上是重复的,有的同学甚至认为这是低端的,体现不出工程师价值的工作,纯粹的「体力活」。但是却没有一个能真正解放生产力的方案。

解决思路

尽管我们把数据库抽象成了「关系型」数据库,把操作数据的命令抽象成了 SQL ,同时我们也有了 MySQL 客户端,甚至是 sequelize 这种非常方便的库,也有「RESTful」API 命名规则,但是接口的实现从来都是需要工程师们自己用手敲出来的。

如果说我看得比别人远,那是因为我站在巨人的肩膀上。

所以我们在现有的技术基础上再抽象,把已有的东西重新组合起来,拼装成一个新的工具,帮助工程师从「体力活」中解脱出来,解放生产力。

什么样的工具

最开始的时候,我们最先需要明确的问题就是:「我们需要什么样的工具?」或者说「这种工具要帮我们解决什么问题?」。

实际上我们从刚才的假设中,已经可以得出结论:我们希望有一个工具可以让工程师免于编写数据操作 API,把数据库操作直接映射到 HTTP RESTful API 上

调用方式

如何请求

为了解释「如何请求」,我们先从一些公认的规则出发,举一个例子,然后再从例子中抽象出一些规则。

注意:为了更便于理解,我们把所有的命名从客户端一直穿透到数据库,所以请不要纠结于我们在定义一个 API 时名词单复数的问题。

基本用例

几乎所有的系统都会有一个用户表(user)。根据 RESTful 规则的约定,我们应该把访问 user 表的 API 路径定义为 /user,并把 CRUD 的访问方法映射到 HTTP 协议中的四种方法:GETPOSTPUTDELETE

比如:

  • GET /user:获取用户列表,应该返回一个数组。
  • GET /user/:id:获取指定的用户,应该返回一个对象。
  • POST /user:创建一个用户,应该返回被存储的对象,状态码应该为 201(Created)。
  • PUT /user:修改一个用户的信息,应该返回修改后的对象。
  • DELETE /user/:id:删除一个用户,状态码应该为 204(No Content)。

如果 user 表有一个关系表 feed,那么我们的路径就会再复杂一点:

  • GET /user/:id/feedGET /feed?user_id=:id:获取某个用户的帖子,应该返回一个数组。
  • GET /user/:id/feed/:feed_idGET /feed/:id:获取指定的帖子,应该返回一个对象。

上述的例子中还会衍生出其他的数据操作,不仅仅只有 GET,这里不一一列举了。

抽象出规则

上一节中,列举了要提供一个表的数据访问 API,大概要实现哪些路由。从这些枚举中,可以找出其中的规律,总结出一套规则。最终我们在「把能实现的路由,全部实现」的原则基础上,开发了 RestQL 的 koa 版本。

支持的 HTTP 方法:

HTTP verb CRUD
GET Read
POST Create
PUT Create/Update
DELETE Delete

支持的带有 body 的 HTTP 方法:

HTTP verb List Single
POST Array/Object ×
PUT Array/Object Object

说明

  • List 路径为返回值为数组的路径,包括:
    • /resource
    • /resource/:id/association, association 为 1:n 关系
    • /resource/:id/association, association 为 n:m 关系
  • Single 路径为返回值为单个对象的路径,包括:
    • /resource/:id
    • /resource/:id/association, association 为 1:1 关系
    • /resource/:id/association/:id, association 为 1:n 关系
    • /resource/:id/association/:id, association 为 n:m 关系

如何使用

我们已经开源了 koa-restql,koa 应用开发者可以通过 npm 安装它:

npm install koa-restql

然后在 koa 应用的代码中引用 RestQL:

const koa       = require('koa')
const RestQL    = require('koa-restql')

let app = koa()
// Build APIs from `sequelize.models`
let restql = new RestQL(sequelize.models)
app.use(restql.routes())

常见问题

修改参数

用户可以通过querystring来修改参数。强烈建议使用qs对 querystring 进行解析,例如:

qs.stringify({a: 1, b:2}) // => a=1&b=2

RestQL 中的querystring仅有 3 条规则:

  • 所有不以_开头的建,都会被放进sequelize#query()where参数中。例如:

    // query
    {
      name: "Li Xin"
    }
    // option for sequelize
    {
      where: {
        name: "Li Xin"
      }
    }
    
  • 所有以_开头的建,都会被放进sequelize#query()的参数中,和where保持平级。例如:

    // query
    {
      _limit: 10
    }
    // option for sequelize
    {
      limit: 10
    }
    
  • 当需要使用关系时,可以用关系名称的字符串代替关系对象传入。例如需要使用include时:

    // query
    {
      _include: ['friends']
    }
    // option for sequelize
    {
      include: [
        models.user.association.friends
      ]
    }
    

访问控制

通常来说,我们有两种方法实现访问控制:

通过中间件

在 koa 应用挂载 RestQL 的 router 之前,可以实现一个鉴权中间件,控制用户的访问权限:

app.use(authorizeMiddleware)
app.use(restql.routes())

Authorize Middleware

通过 restql 参数

在使用sequelize定义关联时,我们可以设定restql参数,实现访问控制。例如:

  • 禁止通过restql访问关联:

    models.user.hasOne(
      models.privacy,
      {
        restql: {
          ignore: true
        }
      }
    )
    
  • 禁止通过restql使用指定的 HTTP 方法访问关联

    models.user.hasOne(
      models.privacy,
      {
        restql: {
          ignore: ['get']
        }
      }
    )
    

其他语言/框架

目前我们仅实现了基于nodekoa的版本,还没有其他语言/框架的实现版本。欢迎开发者提交其他语言/框架的实现到 RestQL 组

参考链接

18 回复

这个我试了下的,貌似npm版本低于3的话还有点小问题

@luoyjx 我们目前还没有支持低版本 node/npm 的计划。 项目里大量用了 ES6 的特性,所以有可能是 node 的问题。

不知道跟couchdb/couchbase、parse-server的思路有什么不同?

看着不错,确实简化了一些开发,但大部分从db取出来后都要编辑,route也要加权限管理的,所以实际意义不知道有多大

@winglight 有很多不同点:

  • 可以和其他中间件更灵活的配合使用,比如可以自己实现鉴权,或者和其他服务交互的中间件。因为实现的是一个中间件,而不是一个 HTTP 服务。当然也可以单独启动一个服务器跑起来。
  • 充分利用关系型数据库的优势,嵌套的 path/body 可以自动的转换成对关系表的操作。如果直接使用文档型数据库,数据结构会比较混乱。
  • 数据库使用的是 mysql,可以很方便的在各种云平台上使用。666

@simdm 我们在内部的项目中尝试了一些思路,FYI:

  • 原则上任何数据都尽量穿透到最终的服务上,中间不做任何修改。
  • 权限可以通过中间件实现,或者通过参数让koa-restql不挂载相关的 model。

@crzidea 嵌套的 path/body 可以自动的转换成对关系表的操作——这里是不是可以理解成ORM的实现?如果是这样,nosql不是更适合吗?

@winglight 他这个是基于sequlizejs的,sequlizejs是node上目前最好的ORM实现了,他这个框架就是把路由和sequlizejs进行了整合

@crzidea 这样的话,还在4.x的童鞋不是还得手动升级npm?

blueprint api + waterline + sails-permission?

@wi nglight 我们定义 RestQL 的时候,并不限制后端使用的数据库是什么。只是目前的 koa-restql 我们用 sequelize 实现了这套规则。从长远规划来看,是很想实现一个 rethinkdb 版本的。

至于文档型和关系型数据库哪个更符合业务场景,可以参考这一篇博客:从数据结构出发对比关系型数据库和文档型数据库

@crzidea 说实话,用了这么多年RMDB,对比nosql,感觉没什么优势了——毕竟coding是面向对象,存储也用对象才更容易设计实现。 我看了你引用的文章,里面对于选用数据库谈的很浅,以前实现系统必须从数据库schema开始,后来进化到使用ORM自动生产schema,再到现在nosql和实体直接映射,这很明显是一个越来越趋近OO思想的进化之路。另外,如果要对比两种数据库,那么不应该直接使用传统table的概念来比较,而应该是使用几种不同的系统应用场景来看,这一点在《mongodb权威指南》里面做的很好,我就不啰嗦了。

这个项目我前两天才看到,当时考虑的是用 epilogue 还是你们公司这个 但是我大概看了下,文档写的真不好,一眼看完不会用, 所以选择了用 epilogue,不过为了koa用还修改了源代码, 目前来看还算满意

看看

来自酷炫的 CNodeMD

楼主估计没有用过loopback,并不觉得有啥稀奇的,解决一个问题的同时往往会产生新的问题

回到顶部