分享 一次无后端的供应链系统开发实践 (上篇): 前后端分离的 Restful 接口设计

ylt · 发布于 2016年12月07日 · 660 次阅读
5700

本人在去年底加入了一家医疗保险行业的创业公司‘亿保健康’,公司的技术是基于Java体系的。而这次的分享是今年做的一个新的供应链项目。当时项目组缺后端java程序员,所以我用ruby写了一个自动化的后端。本文分享供应链项目采用的后端自动化技术,该技术极大提升了项目的开发效率。

1. 缘起

我们在2016年初上线了一个网上药房:老白网laobai.com。 半年多的时间,老白网的官网销售额在全国自营的网上药房里已经排名前10了。电商的后端需要有一套供应链管理系统,但是由于药品的特殊性,药品采购/仓储/物流等需要符合GSP规范,导致我们目前外购并同时使用两套供应链系统,一套通用版满足基本的功能需求,一套主要是药品的GSP审核的需要,两个系统之间还需要数据交换。此外,这些外购的系统也无法满足我们自己的一些定制化开发的需求。所以就迫切需要自己开发一个满足GSP规范的药品行业供应链系统。

外购的第三方的供应链系统有1000多张表,功能上的复杂度主要在数据表多且有关联,对性能/并发上到是没有太多的要求。因为可以参考第三方的数据库结构设计,也可以参考第三方的界面设计,所以数据库/前端都没有太多的不确定性。 当时这个项目的团队情况是有一个靠谱的DBA,有几个前端开发,但是缺少后端程序员。功能很多,需要开发的人力缺口很大,正是在这种情况下催生了本文提到的技术方案。方案的主要考虑是:

  1. 数据库设计完成后,前端要求可以直接开始开发,不能等后端接口。
  2. 后端的开发工作量要尽可能少,多用代码生成技术,因为没有专职的后端程序员。

2. REST的优点与不完备性

关于前后端之间的接口规范,主要有三种风格:RPC vs REST vs GraphQL。 RPC风格的接口规范是最传统的,但是不太合适基于Web的系统,所以本方案中不予考虑。 GraphQL调研了一下,它能够满足我们的第一个需求,前端可以定义接口,对后端接口依赖少。但是无法满足第二点,采用GraphQL的后端开发工作量很大,测试也麻烦,而且GraphQL太新,整个团队都没有使用的经验。 REST有很多可用的代码生成系统可参考,比如 rails scaffold / django admin,不需要从零开始。所以综合考虑还是采用REST风格。

按照Fielding博士的说法,REST只适用于应用软件的架构, 不包括操作系统、网络软件和一些仅仅为得到系统支持而使用网络的架构风格 (例如,进程 控制风格)。应用软件代表的是一个系统的“理解业务”(business-aware)的那部分功能。

按照REST架构设计的Web API一般被成为Restful API。 REST在数据库类应用中使用广泛。针对数据库中的一张表,Restful的api通过约定来定义好CRUD的全部接口api,不需要前后端之间沟通接口的设计,而且各种语言都有针对单表的CRUD后端自动代码生成,能够减少后端的开发工作量。约定大于配置是一个很好的软件工程实践,能够大大减少软件开发的复杂性。下面就是一个restful的api约定:

操作 HTTP Method URI
获取列表数据 GET /表名(.:format)
添加新数据 POST /表名(.:format)
修改已有数据 PUT /表名/:id(.:format)
查看已有数据 GET /表名/:id(.:format)
删除已有数据 DELETE /表名/:id(.:format)

很多知名的软件和框架都采用了类似这套的约定,比如rails开发框架/ElasticSearch搜索等。

当然针对实际的软件开发需求,REST的规范还是太简单了。标准的Restful接口只有四个动作,CRUD。一个最常见的扩展(或者说是误用),就是使用更多的动作。因为常见的后端开发框架是controller+action的模式,一个controller对应一个资源,里面配置多个action对应前端用户的多个动作。典型的,比如一个帖子,点赞/取消赞是两个动作。然后随着业务的发展,还有锁帖/解锁操作,回复帖子/查看所有0回复帖子等等需要。于是就有了下面的这种url设计:

POST /topics/follow
POST /topics/unfollow
POST /topics/lock
POST /topics/unlock
POST /topics/reply
GET /topics/no_reply

慢慢的,接口越来越复杂,离原来的Restful风格越来越远。当然,有人觉得这种风格也不错。而DHH的观点是这种风格url需要改造为Restful的风格,比如对于点赞的场景,可以认为有一个资源是topics/follows,然后这个资源有添加和删除两个操作。 关于DHH对这种风格的讨论,可参考这个链接, 这里是中文翻译版

今年的rubychina大会上也有一场针对类似问题的分享, 可以参考[这个链接](https://speakerdeck.com/mechiland/reconsider-rest-chong-gou-jian-da-xing-railsying-yong-de-fang-shi

这还只是对单表资源的CRUD操作,我们碰到的Restful规范主要是缺失下面的一些部分:

  1. 查询/分页/排序的支持。 Restful接口只有一个列表的接口,对查询相关的功能没有约定。
  2. 批量操作的支持。 Restful接口默认只支持单个资源的操作,而实际的业务场景中经常需要有批量操作的需求,比如商品的批量上架、数据的批量删除等。
  3. 有关联的数据表的支持。Restful只支持单个资源的CRUD,而实际业务中经常有主子表的级联保存,关联表的查询等。

所以本方案主要考虑两点:

  1. 扩展REST,针对上述三种场景约定好接口规范。 目的是让前端程序员只需要知道接口约定,就可以针对所有的数据库表进行接口开发。无需提供详细的接口说明文档。
  2. 如何通过自动代码生成的方式实现这些规范,以减少后端开发的工作量。

3. Restful扩展:单表批量操作(添加/更新/删除)

3.1 批量新增接口

REST规范没有约定如何实现批量操作,也没有说明提交参数和返回值的格式。实践中,elasticsearch提供了批量操作的入口/_bulk,统一处理所有的批量操作,可以在一次请求里完成索引/更新/删除等多个操作。本方案对批量操作的要求更严格,只支持单表数据的一种操作。对于批量新增接口,我们重用Restful的新增接口,只是提交的数据格式不一样。

单个新增的话,提交的是一个单个的hash对象

{
    "表名单数": {id:id, field:value,...}
}

批量新增的话,提交的里层数据是一个数组

{
    "表名复数": [{id:id1, field:value,...},{id:id2}...]
}

而且约定在所有的输入输出中,如果是集合对象,名字采用复数形式;如果是单个对象,名字采用单数形式。当然,还有一个约定是所有的主键的字段名都是“id”。请求中的field直接对应数据库的列名。如果id不传,则由数据库提供自动生成的主键。

3.2 批量修改接口

对于如何定义批量修改接口,有点左右为难。首先,无法按照批量新增的模式重用修改接口,因为Restful的单条数据修改接口“PUT /表名/:id”和单个id绑定了。如果严格参考DHH的做法,批量修改也需要抽象成一个资源,类似于点赞,而不应该增加一个动作。

但是我对这个做法不习惯,批量修改我认为还是和CRUD一个性质的。目前还是决定增加了一个动作batch_update,也是整个约定里唯一增加的动作。批量修改接口的url地址是“/表名/batch_update.json”,提交的json数据格式和批量新增接口一致。以后也有可能统一提供一个类似elasticsearch的bulk接口。

3.3 批量删除接口

批量删除接口也没有按照DHH的说法抽象成资源,而是重用了Restful的删除接口,只是id的格式不一样。批量删除接口,一次传入多个id,id之间以英文逗号“,”分割。

所以针对批量操作,本方案增加了两个约定如下:

操作 HTTP Method URI
批量修改数据 POST /表名/batch_update(.:format)
批量删除数据 DELETE /表名/:id,:id

4. Restful扩展:单表查询

4.1 查询格式

单表的查询,uri直接重用rest规范,但是要约定好查询的参数的传递规范。我们定义了下面这些查询的请求格式

s[field]=value
s[like[field]]=value
s[date[field]]=value
s[range[field]]=value
s[in[field]]=value
s[cmp[field,field]]=

分别代表精确查询/like字符串模糊查询/date日期范围查询/range范围查询/in枚举查询/cmp比较查询。这些查询基本满足了OLTP业务的常见需求,报表统计类需求有专门的报表系统。

如果有多个查询条件,条件之间是逻辑与的关系。

s[field1]=value1&s[like[field2]]=value2

查询的field直接对应到数据库的字段。如果field有逗号“,”,则表示同时查询多个字段,其中一个满足条件即可,也就是OR查询。

"/warehouses.json?s[like[company,address]]=测试"

上面这个查询的意思是查找所有company包含‘测试’或者address包含‘测试’的所有仓库。

针对date/range/in查询,支持value中包含逗号“,”。以range查询为例,“1,5”代表范围是1到5,",5"代表小于等于5,"3,"代表大于等于3。例如:

"/warehouses.json?s[range[id]]=1,5"
"/warehouses.json?s[range[id]]=,5"
"/warehouses.json?s[range[id]]=3,"

4.2 分页/排序/Count计数

分页参数page/per,排序参数order,计数参数count之间都是可以自由组合的。例如:

"/warehouses.json?page=1"
"/warehouses.json?page=1&per=100"
"/warehouses.json?page=1&order=id+desc"
"/warehouses.json?page=1&per=100&count=1"

5. Restful扩展:关联表支持

5.1 外键查询的支持

上一节提到的单表查询规范,其中的field字段,增加对特殊符号"."的支持,用于外键查询。这样Field可以包含三种类型:

单个key;
多字段的key,格式:"key1,key2,..."
外键的key,格式:“key1.key2”。

其中,多字段的key的格式表示多个字段的or查询,上文已经提及。外键查询要求被查询的表有对应的外键字段。比如仓库属于公司,那么warehouses表有一个外键company_id,company有id、name字段,那么可以有下面的查询:

"warehouses.json?s[company.name]=测试公司"
"warehouses.json?s[range[company.id]]=1,5"

这两个查询的含义是显而易见的。

5.2 关联表查看

本系统支持在查看一条数据时,自动带出关联的父表的数据。同时也支持带出给定的几个关联子表数据:传递参数many=表1[,表2],比如:

GET warehouses/1.json?many=stores

查看编号为1的仓库的基本信息,同时给出这个仓库下面的所有库位的信息。这个查询要求stores表有一个外键指向warehouses表。

5.3 子表的级联新增和删除

主子表的级联保存

有很多业务场景需要支持在一个事务里保存主表和关联的多个子表的数据。级联保存的接口和批量保存的接口类似,只是提交的参数有区别。主子表新增的话,提交的数据格式如下:

{
    "主表单数": {id:id, field:value,...} ,
    "子表复数": [{id:id1, field:value,...},{id:id2,}...]
}

6. 小结

这篇文章主要介绍了项目采用的一种扩展的restful接口协议,基于该协议,前端程序员在知道数据库结构的情况下就可以开始项目开发了。由于供应链项目是一类成熟的项目,可以且必须参考已有系统的数据库设计(因为要保证旧系统的数据可以方便的迁移到新系统),所以数据库设计实际上是提前完成的。这和通常的rails项目不太相同,常见的rails项目数据库结构是在代码中定义的,随着开发的进行不断完善的,而不是提前给定的。

本文的下篇则会介绍到代码生成技术。

暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册