首页 > 解决方案 > 如何在 REST api 中为进度“资源”建模?

问题描述

我有以下包含sectionIds 数组的数据结构。它们按完成的顺序存储:

applicationProgress: ["sectionG", "sectionZ", "sectionA"]

我希望能够做类似的事情:

GET /application-progress- 预期:sectionG、sectionZ、sectionA

GET /application-progress?filter[first]=true- 预期:G 部分

GET /application-progress?filter[current]=true- 预期:A 部分

GET /application-progress?filter[previous]=sectionZ- 预期:G 部分

我赞赏上述 URL 不正确,但我不确定如何命名/构造它们以获取预期数据,例如这里的资源是“sectionids”吗?

我想遵守 JSON:API 规范。

更新

我希望遵守JSON:API v1.0

在资源方面,我相信我有“Section”和“ProgressEntry”。每个 ProgressEntry 将与 Section 具有一对一的关系。

我希望能够在集合中查询,例如

获取集合中的第一项:

GET /progress-entries?filter[first]

回报:

{
    "data": {
        "type": "progress-entries",
        "id": "progressL",
        "attributes": {
            "sectionId": "sectionG"
        },
        "relationships": {
            "section": {
                "links": {
                    "related": "http://example.com/sections/sectionG"
                }
            }
        }
    },
    "included": [
        {
            "links": {
                "self": "http://example.com/sections/sectionG"
            },
            "type": "sections",
            "id": "sectionG",
            "attributes": {
                "id": "sectionG",
                "title": "Some title"
            }
        }
    ]
}

在给定相对 ProgressEntry 的情况下获取上一个 ProgressEntry。所以在下面的例子中,找到一个其 sectionId 属性等于“sectionZ”的 ProgressEntry,然后获取上一个条目(sectionG)。之前我不清楚这个过滤是基于 ProgressEntry 的属性:

GET /progress-entries?filter[attributes][sectionId]=sectionZ&filterAction=getPreviousEntry

回报:

{
    "data": {
        "type": "progress-entries",
        "id": "progressL",
        "attributes": {
            "sectionId": "sectionG"
        },
        "relationships": {
            "section": {
                "links": {
                    "related": "http://example.com/sections/sectionG"
                }
            }
        }
    },
    "included": [
        {
            "links": {
                "self": "http://example.com/sections/sectionG"
            },
            "type": "sections",
            "id": "sectionG",
            "attributes": {
                "id": "sectionG",
                "title": "Some title"
            }
        }
    ]
}

标签: restjson-api

解决方案


我开始评论 jelhan 的回复,尽管我的回答只是渴望对他的反对做出合理的评论,因此我将其包含在此处,因为它或多或少地为答案提供了一个很好的介绍。

资源由唯一标识符 (URI) 标识。URI 通常独立于任何表示格式,否则内容类型协商将毫无用处。json-api 是一种媒体类型,它定义了为特定资源交换的表示的结构和语义。媒体类型不应该对资源的 URI 结构施加任何约束,因为它独立于它。即使 URI 包含类似的内容,也无法根据给定的 URI 推断要使用的媒体类型,vnd.api+json因为这可能只是一个谈论 json:api 的网页。如果服务器支持这两种表示格式,客户端也可以请求application/hal+json而不是在相同的 URI 上接收相同的状态信息,这些信息只是打包在不同的表示语法中。application/vnd.api+json

正如 jelhan 所提到的,配置文件只是对实际媒体类型的扩展机制,它允许通用媒体类型通过添加进一步的约束、约定或扩展来专门化。此类配置文件使用类似于 XML 名称空间的 URI,并且这些 URI 不需要但应该被取消引用以允许访问进一步的文档。除了Web Linking给出的实际资源的 URI 之外,没有讨论URI 可能会提示客户端使用的媒体类型,我不建议这样做,因为这需要客户端对该提示有一定的了解。

正如我在最初的评论中提到的,URI 不应该传达语义,因为链接关系是存在的!

链接关系

这样,您概述的资源似乎是一些进一步资源的集合,按您的领域语言划分。虽然中定义的分页json:api并不能完美地直接映射到此处,除非您有太多的部分需要将它们拆分为多个页面,否则可以使用IANA定义的标准化链接关系来使用相同的概念。

在这里,有一次,服务器可能会为您提供一个指向集合资源的链接,该链接可能如下所示:

{
  "links": {
    "self": "https://api.acme.org/section-queue",
    "collection": "https://api.acme.org/app-progression",
    ...
  },
  ...
}

由于collectionIANA 标准化的链接关系,您知道此资源可能包含一组条目,在调用这些条目时可能会返回json:api如下表示:

{
  "links": {
    "self": "https://api.acme.org/app-progression",
    "first": "https://api.acme.org/app-progression/sectionG",
    "last": "https://api/acme.org/app-progression/sectionA",
    "current": "https://api.acme.org/app-progression",
    "up": "https://api.acme.org/section-queue",
    "https://api/acme.org/rel/section": "https://api.acme.org/app-progression/sectionG",
    "https://api/acme.org/rel/section": "https://api.acme.org/app-progression/sectionZ",
    "https://api/acme.org/rel/section": "https://api.acme.org/app-progression/sectionA",
    ...
  },
  ...
}

您有更多链接可以在层次结构中向上或向下移动,或者选择完成的第一个或最后一个部分。请注意最后 3 个示例 URI,它们利用了RFC 5988(Web 链接)定义的扩展关系类型机制。

在进一步深入层次结构时,您可能会发现链接,例如

{
  "links": {
    "self": "https://api.acme.org/app-progression/sectionZ",
    "first": "https://api.acme.org/app-progression/sectionG",
    "prev": "https://api.acme.org/app-progression/sectionG",
    "next": "https://api.acme.org/app-progression/sectionA",
    "last": "https://api.acme.org/app-progression/sectionA",
    "current": "https://api.acme.org/app-progression/sectionA",
    "up": "https://api.acme.org/app-progression",
    ...
  },
  ...
}

这个例子应该只是展示服务器如何为您提供客户端完成其任务可能需要的所有选项。它只会跟随它感兴趣的链接。根据提供的链接关系名称,客户端可以就所提供的链接是否感兴趣做出明智的选择。如果它知道资源是一个集合,它可能会遍历所有元素并一个一个地处理它们(或同时由多个线程处理)。

这种方法在 Internet 上很常见,并且允许服务器随着时间的推移轻松更改其 URI 方案,因为客户端只会对链接关系名称进行操作,并且只会调用 URI 而不会尝试从中推断出任何逻辑。这种技术也很容易用于其他媒体类型,例如application/hal+json或类似的,并且默认情况下允许缓存和重用每个相应的资源,这可能会减轻服务器的负载,具体取决于它必须处理的查询量和。

请注意,尚未说明该部分的实际内容。它可能是对部分典型事物的复杂总结,也可能只是一个词。我们可以对其进行分类并为其命名,因此即使是一个简单的单词也是资源的有效目标。此外,正如Jim Webber所提到的,您通过 HTTP (REST) 公开的资源和您的域模型并不相同,并且通常不会一对一地映射。

过滤

json:api允许通过定义自定义x-www-form-urlencoded解析将参数组合在一起语义。如果使用内容类型协商json:api作为表示格式达成一致,则互操作性问题的可能性相当低,尽管如果这种表示是主动发送的,则对此类查询参数的解析可能会失败。

值得一提的是,在 REST 架构中,客户端应该只使用服务器提供的链接,而不是自己生成链接。客户端通常对 URI 不感兴趣,但对 URI 的内容感兴趣,因此服务器需要知道如何对 URI 进行操作。

可以使用概述的建议,也可以使用表单的 URI

.../application-progress?filter=first
.../application-progress?filter=current
.../application-progress?filter=previous&on=sectionZ

可以代替使用。除此之外,这种方法还应该适用于几乎所有客户端,而无需更改其 url 编码的解析机制。除此之外,还可以最小化为生成的其他媒体类型返回 URI 的管理开销。请注意,上面示例中的每个 URI 都表示它们自己的资源,并且缓存将根据用于检索此类结果的 URI 存储对此类资源的响应。类似.../application-progress?filter=next&on=sectionG.../application-progress?filter=previous&on=sectionA检索基本相同表示的查询是两个不同的资源,您的 API 将处理两次,因为第一个查询的响应不能被重用,因为缓存键 (URI) 不同。根据菲尔丁缓存是 REST 必须遵守的少数约束之一,否则您将违反它。

如何设计此类 URI 完全取决于您自己。重要的是,您如何教客户端何时调用此类 URI 以及何时不应调用。在这里,同样,可以而且应该使用链接关系。

概括

总之,您喜欢哪种方法以及您选择哪种 URI 样式取决于您。客户端,尤其是在 REST 环境中,并不关心 URI 的结构。他们对链接关系进行操作,并使用 URI 来调用它以继续他们的任务。因此,如Jim Webber 所提到的,服务器 API 应该通过在 70/80 年代基于文本的计算机游戏中教授它需要知道的内容来帮助客户端。正如 Asbjørn Ulsberg 所解释的,将交互模型设计为可供性和状态机是有帮助的。

虽然您可以对此类链接提供的分组参数应用过滤,但json:api可能只能在“json:api”表示中使用。如果您将此类链接复制并粘贴到浏览器或其他渠道,则该客户端可能无法处理它。因此,这不是我的第一选择,TBH。无论您是将部分设计为自己的资源还是只是要检索的属性,这也是您的选择。我们真的不知道您的域模型中有哪些部分,IMO 这听起来像是一个有效的资源,尽管它可能有也可能没有更多的属性。


推荐阅读