V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
chaleaochexist
V2EX  ›  程序员

请教,rest api 的设计问题,关于粒度.

  •  1
     
  •   chaleaochexist · 2019-01-31 17:12:47 +08:00 · 2481 次点击
    这是一个创建于 2126 天前的主题,其中的信息可能已经有所发展或是发生改变。
    举个例子.
    api/users?exclude=true
    返回 某种情况下的 username list
    api/users?exclude=false
    返回 某种情况下的 username,userage,以及其他一些东西的 list.

    这个例子比较简单.在我所在项目,还有需要根据 if else 返回完全不同的东西.
    这些东西真的可以放到一个 api 中吗
    第 1 条附言  ·  2019-02-01 10:01:12 +08:00
    感谢各位, 所以这就引申出另一个问题. resource 和 model 是不是一个概念.
    field 指的是 resource 的 field 是吗?

    resource/serializer/model/数据库中的 table
    这几个概念要如何理解.

    我的理解他们都是不同的东西.
    但是 resource 和 serializer 差不多.
    22 条回复    2019-02-15 17:07:56 +08:00
    chaleaochexist
        1
    chaleaochexist  
    OP
       2019-01-31 17:13:03 +08:00
    关于 api 设计有没有好的博客文章或者书籍分享.
    xuanbg
        2
    xuanbg  
       2019-01-31 17:16:52 +08:00   ❤️ 1
    最好还是分两个接口,好控制权限
    tionsin
        3
    tionsin  
       2019-01-31 17:19:34 +08:00   ❤️ 1
    刚刚解决的问题...最好设计成两个接口,不然 vue 不能刷新数据...
    rayingecho
        4
    rayingecho  
       2019-01-31 17:27:13 +08:00   ❤️ 1
    我这有一种设计风格:
    GET /api/users 或 GET /api/users?fields=* 获取列表,model 包含所有字段
    GET /users/42?fields=username,userage 获取列表,model 只包含选定字段

    也可以上 GraphQL
    nuance2ex
        5
    nuance2ex  
       2019-01-31 17:33:41 +08:00 via iPhone   ❤️ 1
    lz 可以了解下 jsonapi 1.0 规范。推荐 4 楼的方法。4 楼的方法就是在规范当中的。
    pelloz
        6
    pelloz  
       2019-01-31 17:45:04 +08:00   ❤️ 1
    我们用的和 4 楼比较像,不过获取哪些 field 我们是在 head 里面定义的
    libook
        7
    libook  
       2019-01-31 17:50:06 +08:00   ❤️ 1
    REST 是围绕资源( Resource )的,那么首先要确定有几个资源。

    假设只有 1 个资源,就是 User,那么 id、name、age 都是 User 的属性,接口可以设计如下:

    直接通过 id 定位确定的一个用户
    /user/:id
    相应的 CRUD 对应 POST GET PUT DELETE 方法。

    针对用户集合
    /user/collection
    可以通过 query 来指定 name 或 age 查询条件,
    相应的 CRUD 对应 POST GET PUT DELETE 方法。

    如果有特殊需求就是希望的到一个 Username 列表,可以让 Username 是 User 下的一个子资源(属性):
    /user/collection/username?age=99

    REST 的接口要尽可能简单、明确,每个接口值提供一个功能,如果你在内部使用 if else 来判断选取不同逻辑的话,我觉得这个接口本身承载了两个功能,建议拆分成两个不同的接口。
    UIXX
        8
    UIXX  
       2019-01-31 18:02:25 +08:00   ❤️ 1
    好像没人说到点子上。

    你的例子最大的问题是 exclude 这个词语义不明确。通过判断一个语义不明确的变量来返回不同的结果本身就是一个 API 设计的大忌。
    其次是 exclude 这个词所界定的粒度,如果是按业务返回一些相应的 list,现在这样做也许可以,但是延展性不强,对需求的抗性不够。REST API 的一些约定俗成的规矩本身就是为了简洁、可读、可组合定制(灵活)。
    ibegyourpardon
        9
    ibegyourpardon  
       2019-01-31 18:10:21 +08:00   ❤️ 1
    实际操作中我们是这样。

    一,有一组粒度极细,原子化的基础接口。

    二,一部分常用的业务接口,对基础接口做二次封装。能满足 70% 的调用需求。

    三,有 10%左右的需求因为目标简单明确,直接去了第一条里提的那些接口。

    四,有一些临时项目需求,临时拼装一些小接口。

    所以像你说的问题,我是绝对会拆出一堆各种 API 来的。

    业务千变万化,所以我是要什么给什么,能拆分的 API 绝对拆开,不怕接口多,就怕接口改。改起来才是真要命,千丝万缕在一起。

    业务 API 因为太多,所以时间久了会有重复功能的接口,我也毫不在意,因为这类接口只直接为一个业务服务,业务哪天变了或者撤了,老的接口也就没用了。
    chaleaochexist
        10
    chaleaochexist  
    OP
       2019-02-01 10:00:56 +08:00
    @ibegyourpardon
    @UIXX
    @libook
    @nuance2ex
    @pelloz
    @rayingecho
    @xuanbg
    @tionsin

    感谢各位, 所以这就引申出另一个问题. resource 和 model 是不是一个概念.
    field 指的是 resource 的 field 是吗?

    resource/serializer/model/数据库中的 table
    这几个概念要如何理解.

    我的理解他们都是不同的东西.
    但是 resource 和 serializer 差不多.

    再次感谢.
    chaleaochexist
        11
    chaleaochexist  
    OP
       2019-02-01 10:04:51 +08:00
    @ibegyourpardon
    感谢, 想问一下 二次封装 如何封装 是 从代码里面 import 还是直接调用 url?
    libook
        12
    libook  
       2019-02-01 10:53:04 +08:00   ❤️ 1
    resource 和 model 的定义是取决于你的系统架构规划的,不同人根据不同需求做不同项目可以对这两个的定义不同。
    一般来说,如果服务端套用 MVC 思想的话,resource 是 V 层的,model 是 M 层,他们两个可以是不同的,也可以是一致的,比如我上面做的方案中,User resource 和 User model 可以是同一个东西,而 Username resource 可以是 V 层单独抽象出来的,实际上是 User model 的一个属性。

    MVC 是利用了计算机科学中的分层解耦思想来降低系统复杂度的,既然分层解耦了,那么 model 和 resource 就是没有直接关系的,他们两个之间可以是任意对应甚至多对多的关系(多个 model 的信息聚合成一个 resource 的信息)。
    no1xsyzy
        13
    no1xsyzy  
       2019-02-01 10:56:32 +08:00   ❤️ 1
    @chaleaochexist #10
    serializer 可以是任何东西,而不仅仅是数据——也可以是程序甚至一个类(所以 Java serialization 经常爆漏洞,就是因为有人把一个包含恶意代码的对象传进去结果被反序列化后就被执行了)。
    table 是一种基于记录、面向关系的数据组织方式。类似报表
    resource 是一种基于中心轴、面向聚合的数据组织方式。类似 Wikipedia
    model 是描述数据的,并不是数据本身,就像 table 的表头,或者 Wikipedia 的目录。
    chaleaochexist
        14
    chaleaochexist  
    OP
       2019-02-01 11:43:50 +08:00
    @libook
    所以对于一个 resource 对应多个 model 的情况.
    就需要在 view 层做组装.

    组装的过程应该如何抽象呢?都堆在 view 层感觉越堆越多.


    @no1xsyzy
    所以有没有具体的开源项目推荐呀...代码阅读量不够...最好是基于 python 的哈.java 看不太懂.谢谢
    nuance2ex
        15
    nuance2ex  
       2019-02-01 11:57:10 +08:00 via iPhone   ❤️ 1
    @chaleaochexist lz 用的是什么框架?如果是 flask 的话,可以去了解一下 flask-rest-jsonapi,这个库是对 jsonapi1.0 规范进行了封装。

    了解这个库,可以帮助你很好理解这些概念。

    以请求 api/users 为例,数据流如下:
    GET 方法:路由层->resource 层(找到对应 model )->model 层->resource 层(过滤输出)->返回结果

    POST/PATCH 方法:路由层->resource 层(过滤输入)->model 层->resource 层(过滤输出)->返回结果

    一,路由层:根据路由,找到对应的 resource。

    二,Resource 层:即逻辑数据抽象层。关键词是“逻辑”,并非暴露实际数据库的数据,可根据业务逻辑进行自定义。因为 resource 并不一定和数据库的 model 一一对应。这句话有几层含义
    1.一个 resource 可以对应一个 model,但并不意味着 model 的数据会全部返回,比如 password 字段需要经过过滤。(最正常的情况)
    2.多个 resource 可以对应一个 model。比如 api/male_users, api/female_users 都对应 users,婚恋网站可能有这样的需求。
    3.一个 resource 对应多个 model。比如数据库中有 2 张表(table) user 和 article。每次请求 users 返回用户最新五篇 article 的标题。
    4.一个 resource 甚至可以不是数据库的 model。比如 api/sessions,当 post 的就是用户登录,如果验证成功,就返回一个 token。但数据库中并没有 sessions 的 table,验证过程是比较数据库中 user 的密码和传来的密码。

    以上就是 resource 层,在 flask-rest-jsonapi 中是通过预定义 schema 来实现的,schema 可以理解成 resource 的 table,输入输出都会经过 schema 过滤。

    [关于 serialize 和 deserialize]
    Resource 中 Schema 过滤的过程就是 serialize (输出过滤)和 deserialize (输入过滤)的过程。
    1 输入过滤:通过 deserialize 就是把远程传过来(可能脏的)数据变成 python 的字典对象。
    2 输出过滤:通过 serialize 就是把 python 对象变成格式化的字符串返回给客户端。
    libook
        16
    libook  
       2019-02-01 11:59:56 +08:00   ❤️ 1
    @chaleaochexist
    首先得确定业务上的 resource 越来越多的原因是什么:
    1. 程序规划失误,该删删,该合并合并,特别是已经废弃的业务,保持代码精简。
    2. 产品无理需求,该拒拒,改怼怼,或尝试帮产品经理梳理需求得出更简单有效的方案。
    3. 实际业务规模扩大,这时候就无法避免 resource 的数量上升了,横竖业务复杂度就那么多,View 层再怎么精简只是把复杂度甩给了其他层;如果 View 层 resource 太多造成复杂度过高,那么可以依然使用分层解耦的思想来解决,继续拆分,比如微服务和 BFF(Backend For Frontend)。
    nuance2ex
        17
    nuance2ex  
       2019-02-01 12:16:14 +08:00 via iPhone   ❤️ 1
    @chaleaochexist 再补充,你提到的 serializer 应该就是我提到的 Schema (过滤器)。

    Resource 层中不仅只有 Schema 过滤器,还可能包括 Hook 钩子(比如 before request 钩子来验证登录状态,after request 钩子记录日志),数据层(比如可以优化 model 查询方式;可以在存入数据库前,把字符串“ 2019-02-01 ”变成 model 可以理解的 Date 类型,来避免报错;针对两个分别存放在 redis 和 mysql 的数据同时提取,合并返回等情况)。
    nuance2ex
        18
    nuance2ex  
       2019-02-01 12:36:16 +08:00 via iPhone   ❤️ 1
    @chaleaochexist

    你提到的 view 层反复组装的问题,至少在 jsonapi1.0 规范中已经有解决办法,除非是特别常用的,比如婚恋网站的 male_user 和 female_user 可以定义多个 resource。其他情况,让客户端自己按需提取。

    在 Flask-rest-jsonapi 中
    include=xxx (同时查询外键数据)
    fields=xxx,yyy (按需提取字段)
    page[size]=10 (单次展示数量)
    page[number]=2 (页码)
    sort=name,birth_date (排序方式)
    filter=[{"name":"gender","op":"eq","val":"male"}](筛选,还可以用 any 和 has 外键筛选)

    以上可以理解成 restful 版本的 sql 语句
    yiyi11
        19
    yiyi11  
       2019-02-02 17:03:41 +08:00 via Android
    楼上各位大佬,请求中的 fields=xxx,yyy (按需提取字段),后台是怎么实现的?如果后台用的是 springmvc+jackjson 的话。
    ibegyourpardon
        20
    ibegyourpardon  
       2019-02-12 19:25:26 +08:00   ❤️ 1
    @chaleaochexist 关于二次封装接口这事,其实对我这样的小团队来说怎么看都是 import 更划得来,大家都用 PHP,为啥想不开要给自己对自己用接口调用,但这事我还是坚持接口化。就我这边来说,接口化不会带来多少性能损失,却给我这样的小团队和外部合作留下了足够好的基础(我们经常有交叉形式的外包),甚至某些接口我们作为收费卖给隔壁同行们……

    当然这里面我还有个出发点,是为了三年战略中,逐渐引入 Java 或者 Go 开发人员和目前的 PHP 栈混用,这时候通过接口通信的模块化意义就更大了,这是和我司实际情况有关。
    guijianshi01
        21
    guijianshi01  
       2019-02-13 19:36:49 +08:00
    @ibegyourpardon 你这种做法功能类似的接口遇到改的时候怎么做?目前快被改给逼疯了....全部写在一起发现会不敢改,拆开会发现改不全....
    ibegyourpardon
        22
    ibegyourpardon  
       2019-02-15 17:07:56 +08:00
    @guijianshi01 哈哈哈,理解理解,我就这么过来的。我的做法不敢说是完美,也是一个一个坑才过来的血泪史。

    最核心的,我称为核心功能接口,我是尽最大可能划分到最细的粒度,比如一个 auth,这类接口我设计的时候原则上是不直接暴露给前端调用的,因为直接暴露意味着给前端需要请求太多的内容,无论性能还是开发流程上都不可接受,前后端会打架的。

    但这类接口也不绝对,某些高频,无法也无需改动的,比如授权,这种模式下我会适当直接传给前端。

    然后核心功能接口写出来其实是为了给后端调用的,用来做各种业务接口组合。这里面也是血泪史,差点和后端打疯,后端说我几个 import 的事你非要我调网络请求的形式来(我们目前的能力只能以 RESTful API 的形式互相调用),包括业务接口不一次写完,而是写完再组合… 但最后后端发现,在不同的项目中,熬过前期后,往往重新组合包装一个接口提供给前端,会变的异常轻松。

    当然这里也有 API 网关这个我 18 年年初才学到的概念的支援。包括我上面讲的模式里,其实在包装组合接口提供给业务用这件事上,前端突然发现他们也有能力直接调用以及做组合了,所以业务上其实突然变的相对好一些了。

    但一个系统的总体复杂度是不变的,对应的,压力放到了我这边,我自己做的这个设计和决策,现在我要为统筹管理和理清这么多接口、模式进行负责,这个压力也大。可总比和大家一起开会分析那绕来绕去的业务逻辑还是要轻松一些的。

    所以我写了这么多,其实也没有直接回答你的问题。全部写一起肯定是弊大于利,畏手畏脚,甚至逼不得已要重新写一个新的大的单体出来。拆开的不合理的话,又会东改西改,并且接口功能重复,模糊,界限不清。

    但就我自己的经验而言,还是要拆,拆的时候优先提炼出核心的不变的东西,尽可能在少增加额外开发的情况下将接口复用组合提供出业务接口来。而且业务接口尽量不要由核心系统直接提供,一定要有个 API gateway 层,让这个层来承担接口的分发工作。 我们用的也不好,业务中有大量冗余接口,也有很多沉没的存在安全隐患的东西,这个以后早晚要慢慢解决。但小公司,四五个人的团队,只能先到这样了。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3143 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 13:32 · PVG 21:32 · LAX 05:32 · JFK 08:32
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.