【问题标题】:REST services and multiple representations of same object with different fieldsREST 服务和具有不同字段的同一对象的多个表示
【发布时间】:2016-05-31 10:34:31
【问题描述】:

假设您有一个Person 对象,其中有几个相对较小的字段,例如first_namelast_nameage,以及几个较大的字段,例如life_story

大多数检索Person 对象的调用不需要返回life_story,因此我们宁愿在对Person 端点的所有调用中都不返回它。另一方面,在发布新的Person 时,我们希望允许客户端包含life_story 字段。

一种选择是拥有一个 Person 端点和一个 PersonDetailed 端点,其中对 Person 的所有调用 (GET/POST/PUT) 不处理 life_story 字段,并且对PersonDetailed 需要所有字段。

最后我们可以捏造它并在 Person 上创建 POST 和 PUT 方法,以允许客户端选择性地包含 life_story,但在对端点进行 GET 调用时不返回它

API/Person/?last_name_like=La

我不喜欢在同一端点上使用 GET、POST 和 PUT 方法返回具有不同字段的对象,但它确实使 API 更简单。

我一直在寻找人们如何处理此类问题的示例,但没有找到任何示例。谁能指出讨论此类问题的文章或书籍?

【问题讨论】:

  • 您应该使用内容协商。见stackoverflow.com/questions/7846900/…
  • @WillHartung,如果我们的内容格式数量有限,例如精简版 vs. 完整版;但是如果内容有数百个字段并且我们希望允许客户端选择这些字段的任意组合呢?
  • 我的印象是内容协商是针对格式的,即 json 与 xml,而不是针对给定特定编码的表示。

标签: rest


【解决方案1】:

使用查询参数。
api/people?fields=first_name,last_name,age

使用?fields= 语法易于阅读;客户可以只选择给定时间所需的信息。

在一些相关的说明中,HTTP 还包括对部分内容请求的支持,由 206 响应代码表示。您可以提供life_story 的一部分而不返回全部。

【讨论】:

  • 此方法需要对可用数据的先验知识。在真正的 RESTful 环境中,客户端应该从入口 URL(如页面的域)开始,并使用响应中的给定链接浏览内容。客户端和服务器也应该依靠内容协商来检索所需格式的内容。虽然 application/xmlapplication/json 几乎没有任何语义价值,但像 application/vnd+compX.userDataShort+xmlapplication/vnd+compX.userDataComplete+json 这样的东西可能会传达更多的语义。
  • 另外,206 的含义也略有不同。应该为 a partial GET request 返回它,它必须包含一个 Range 标头字段(以字节为单位)。可以找到206 的一个很好的用例in this article
  • @RomanVottner,您应该为这个问题添加一个答案,以 ReSTful 方式演示部分响应查询。内容协商对格式化很有意义,它通常提供非常有限的选项;但是您是否建议每个可能的字段组合都应该用自己的格式表示?
  • 我们对字段参数之类的主要问题是,这意味着您不能往返您的对象。 GET 返回的带有参与字段的对象无法在 PUT 中使用,因为缺少某些字段。
  • @bpeikes,据我所知,这不是 ReST 的要求。在部分回复中,我没有看到对hateoas 的任何障碍。
【解决方案2】:

我喜欢this article 中给出的建议。

使用字段查询参数,该参数采用逗号分隔的要包含的字段列表。例如,以下请求将检索到足够的信息来显示未处理工单的排序列表:

【讨论】:

  • 这与我给出的答案有什么不同吗?它是否解决了@RomanVottner 的评论?
  • 是的。 “谁能指出讨论此类问题的文章或书籍?”这就是我要回答的问题。
  • 我希望避免直接回答这个问题,因为这将导致该线程作为题外话被关闭。 "要求我们推荐或查找书籍、工具、软件库、教程或其他场外资源的问题不属于 Stack Overflow 的主题..."
【解决方案3】:

OData 协议提供了非常全面的 RESTful API。执行CRUD(创建、检索、更新、删除)的常用方法分别由POSTGETPUTDELETE 完成。

通过添加选择查询来完成对部分资源的请求:

api/Person?$select=first_name,last_name,age

【讨论】:

    【解决方案4】:

    根据@jaco0646 的要求

    TL;DR

    • 核心user 资源,带有嵌入式子资源,如地址、组、帖子或下午。 (/api/v1/users/{user_uuid})
    • users 还将包含一个名为 views 的嵌入式资源,用于处理当前注册的视图 (/api/v1/users/{user_uuid}/views/{some_view})
    • view 是使用 POST 请求(即来自 HTML 表单)创建的,其中包括选定的子资源
    • 每个视图都包含核心 user 数据和所选字段的数据
    • 如果所有视图都以核心user 数据开头,则使用部分GET 请求可以只下载所需的数据;虽然可能有其局限性

    当前答案的问题

    在我发布解决某些属性过滤问题的方法之前,我想快速了解一下为什么我不同意 @jaco0646、@Yoram 和 @JoseMartinez 目前给出的答案(它们都是相同的 IMO )

    响应内容的缓存

    HTTP 尝试通过缓存响应来减少网络开销。对同一资源的第二次查找最好从本地缓存中查找,而不是直接从服务器实际查询和下载结果。如果资源数据不经常更改,这将特别有用。

    使用某些缓存控制标头和If-Modified-Since 请求标头,客户端可以通过加载当前内容并缓存响应来影响是使用缓存内容还是刷新缓存。然而,GET 带有查询参数的请求通常被认为是被排除在缓存之外的,这更像是一个城市传说而不是实际情况。但是,某些实现可能会避免缓存此类资源。根据 RFC 7234,缓存应该使用 effective request URIreconstruct stored responses,默认情况下是目标 URI,包括任何查询、矩阵和路径参数。因此,整个 URI 被认为是用于存储和访问响应的密钥。

    部分 GET 请求和用例

    正如 jaco 在他的帖子中提到的,除了标准 GET 和条件 GET 请求之外,HTTP 协议还定义了一个 partial GET request,它允许客户端仅请求资源的一部分而不是全部资源。

    虽然这听起来不错,但部分 GET 请求至少在 HTTP/1.1 中具有 only works on bytes 的限制。

    HTTP/1.1 定义的唯一范围单位是“字节”。

    Range 标头允许在请求中添加多个字节段,以在响应中包含多个段:

    GET /someResource HTTP/1.1
    Host: http://some-host.com/
    Range: 500-700,1200-
    

    部分请求只要求下载(包括)500-700 之间的字节以及从字节 1200 到结尾的所有内容。

    通常使用部分 GET 请求来恢复中断的下载或缓冲正在运行的流,因为确切下载的字节是已知的。但是,如何提前指定每个过滤字段的字节范围?如果没有先验知识,我认为这是行不通的。

    网址大小限制

    如果有许多字段可用于过滤,使用带有查询或矩阵参数的GET 请求可能会导致某些浏览器问题,因为某些浏览器有2000 characters 的限制。

    虽然这可能不会对 OP 问题产生影响,但需要详尽过滤属性的其他用户可能会遇到此问题。

    资源和子资源

    ReST 的重点是资源以及 HTTP 协议提供的与资源交互的方法。

    用户资源,即具有某些“核心”数据,例如用户名、ID 以及其他特定于域的内容。但它也有额外的数据,比如地址,......这也可能是用户资源的一部分。

    ReSTfull 应用程序不是将每个属性混合到一个实体中,而是尝试拥有大量资源。就像上面的示例一样,useraddress 只是两个名称,但肯定还有更多。如果您开始使用 ReSTfull 设计,可能不清楚某些数据应该是该资源的一部分还是重构为自己的资源。这里的经验法则是,如果您需要至少两个不同资源中的某些数据,请重构它并将其嵌入到这些资源中。

    将大(er)资源划分为层次结构允许在发生更改(如地址更改)时轻松更新(在纯 HTTP 意义上,用新内容替换资源 X 上当前可用的内容)子资源用户)同时拥有一个大资源来处理所有数据需要将整个实体主体(如果使用得当)发送到服务器,而不仅仅是更改。

    实体格式

    大量“ReSTfull”服务以application/xmlapplication/json 格式交换数据。但是,两者都没有传达太多语义。他们只是列出了可能在客户端验证的使用的语法规则。但他们没有对实际内容给出任何暗示。因此,客户还必须具备有关如何处理以其中一种格式接收的数据的先验知识。

    如果 JSON 是您选择的表示格式,我会改用 JSON HAL (application/hal+json),因为它定义了核心数据、链接和嵌入内容,这对于呈现的 IMO 场景尤其有用。

    建议的解决方案

    建议的方法有一个核心 user 资源,它嵌入了某些子资源,如地址、组、帖子或 pm。它还将包含一个名为views 的嵌入式资源,它为用户或一般用户处理当前注册的视图。 view 是通过发送 POST 请求(即来自 HTML 表单)创建的,其中包括要包含在响应中的选定子资源。

    核心资源是user 资源,可能在/api/v1/users/{user_uuid} 上可用,默认情况下仅包含用户核心数据和其他资源的链接

    {
        "firstName": "Maria",
        "lastName": "Sample",
        ...
        "_links": {
            "self": {
                "href": "/api/users/1234-5678-9123-4567"
            },
            "addresses": [
                { "href": "/api/users/1234-5678-9123-4567/addresses/abc1" }
            ],
            "groups": [
                { "href": "/api/users/1234-5678-9123-4567/groups" }
            ],
            "posts": [
                { "href": "/api/users/1234-5678-9123-4567/posts" }
            ],
            ...
            "views: [
                { "href": "/api/users/1234-5678-9123-4567/views/view-a" },
                { "href": "/api/users/1234-5678-9123-4567/views/view-b" }
            ]
        }
    }
    

    任何子资源都可通过用户资源 URI:/api/v1/users/1234-5678-9123-4567/{sub_resource},其中 sub_resource 可能是以下之一:addressesgroupsposts、...

    地址的实际子资源可能如下所示

    {
        "street": "Sample Street"
        "city": "Some City"
        "zipCode": "12345"
        "country": "Neverland"
        ...
        "_links": {
            "self": {
                "href": "/api/v1/users/1234-5678-9123-4567/addresses/abc1"
            },
            "googleMaps": {
                "href": "http://maps.google.com/?ll=39.774769,-74.86084"
            }
        }
    }
    

    虽然用户有两个这样的帖子

    {
        "id": 1;
        "date": "2016-02-21'T'14:06:20.345Z",
        "text": "Lorem ipsum ...",
        "_links": {
            "self: {
                "href": "/api/users/1234-5678-9123-4567/posts/1"
            }
        }
    }
    
    {
        "id": 2;
        "date": "2016-02-21'T'14:34:50.891Z",
        "text": "Lorem ipsum ...",
        "_links": {
            "self: {
                "href": "/api/users/1234-5678-9123-4567/posts/2"
            }
        }
    }
    

    包含addressesposts 的视图(/api/users/1234-5678-9123-4567/views/view-a)可能如下所示:

    {
        "firstName": "Maria",
        "lastName": "Sample",
        ...
        "_links": {
            "self": {
                "href": "/api/users/1234-5678-9123-4567"
            },
            "addresses": [
                { "href": "/api/users/1234-5678-9123-4567/addresses/abc1" }
            ],
            "groups": [
                { "href": "/api/users/1234-5678-9123-4567/groups" }
            ],
            "posts": [
                { "href": "/api/users/1234-5678-9123-4567/posts" }
            ],
            ...
            "views: [
                { "href": "/api/users/1234-5678-9123-4567/views/view-a" },
                { "href": "/api/users/1234-5678-9123-4567/views/view-b" }
            ]
        },
        "_embedded": {
            "addresses:" : [
                {
                    "street": "Sample Street"
                    "city": "Some City"
                    "zipCode": "12345"
                    "country": "Neverland"
                    ...
                    "_links": {
                        "self": {
                            "href": "/api/v1/users/1234-5678-9123-4567/addresses/abc1"
                        },
                        "googleMaps": {
                            "href": "http://maps.google.com/?ll=39.774769,-74.86084"
                        }
                    }
                }
            ],
            "posts": [
                {
                    "id": 1;
                    "date": "2016-02-21'T'14:06:20.345Z",
                    "text": "Lorem ipsum ...",
                    "_links": {
                        "self: {
                            "href": "/api/users/1234-5678-9123-4567/posts/1"
                        }
                    }
                },
                {
                    "id": 2;
                    "date": "2016-02-21'T'14:34:50.891Z",
                    "text": "Lorem ipsum ...",
                    "_links": {
                        "self: {
                            "href": "/api/users/1234-5678-9123-4567/posts/2"
                        }
                    }
                }
            ]
        }
    }
    

    另一个视图(即/api/users/1234-5678-9123-4567/views/view-b)可能只包括所选用户完成的posts

    {
        "firstName": "Maria",
        "lastName": "Sample",
        ...
        "_links": {
            "self": {
                "href": "/api/users/1234-5678-9123-4567"
            },
            "addresses": [
                { "href": "/api/users/1234-5678-9123-4567/addresses/abc1" }
            ],
            "groups": [
                { "href": "/api/users/1234-5678-9123-4567/groups" }
            ],
            "posts": [
                { "href": "/api/users/1234-5678-9123-4567/posts" }
            ],
            ...
            "views: [
                { "href": "/api/users/1234-5678-9123-4567/views/view-a" },
                { "href": "/api/users/1234-5678-9123-4567/views/view-b" }
            ]
        },
        "_embedded": {
            "posts": [
                {
                    "id": 1;
                    "date": "2016-02-21'T'14:06:20.345Z",
                    "text": "Lorem ipsum ...",
                    "_links": {
                        "self: {
                            "href": "/api/users/1234-5678-9123-4567/posts/1"
                        }
                    }
                },
                {
                    "id": 2;
                    "date": "2016-02-21'T'14:34:50.891Z",
                    "text": "Lorem ipsum ...",
                    "_links": {
                        "self: {
                            "href": "/api/users/1234-5678-9123-4567/posts/1"
                        }
                    }
                }
            ]
        }
    }
    

    在调用/api/users/1234-5678-9123-4567/views 时,您可能会显示当前可用视图的列表以及 HTML 表单(或某些自定义 UI),您可以在其中为要包含或排除的每个可用字段设置复选框。在将表单数据发送到服务器时,它将检查给定属性的视图是否已经存在(如果存在409 Conflict)并创建一个新视图,以后可能会重用。您还可以在_links 部分的views 段中命名视图并包含某些选定的属性。

    除了为每个用户指定一个视图外,您还可以为所有用户创建一次通用视图,然后按照您的意愿重复使用它们。

    由于视图没有查询参数,因此整个响应是可缓存的。当您使用POST 请求创建视图时(如果幂等性是一个问题,请使用空的POST 请求,然后是PUT 请求),您可以使用几乎无限的参数。这种HAL 类似的方言对views 使用了自己的逻辑。因此,创建自己的内容类型可能也是一个好主意:application/vnd+users.views+hal+json

    关于部分GET 请求:

    由于每个视图的核心user 数据都是相同的,因此可以使用核心数据的长度(减去右括号和倒数第二个括号后的任何空白字符)并发出部分@987654380 @ 请求服务器。它应该只响应嵌入的数据(和最后的右括号),尽管我不确定当前的浏览器是否真的能够相应地更新当前的数据,特别是如果已知内容的某些字节需要像最后一样被删除核心user数据的括号。

    【讨论】:

    • 使用 /user/{id}/views 方法,您将如何为所有 last name = Last 的用户请求 viewA?
    • @bpeikes 抱歉这么晚才回复,但我可能错过了有关问题的通知。建议的解决方案仅展示了一个单一用户案例。由于搜索词可能会击中多个用户,因此可能会返回一组数据。 IMO 在这种情况下最好返回即 JSON 数组,它只包含基本信息,如名称和直接指向该用户视图的链接,即 viewA。如果客户想要检索更详细的信息,它可以直接调用该视图
    猜你喜欢
    • 1970-01-01
    • 2010-11-05
    • 2021-02-09
    • 2021-08-11
    • 2021-12-08
    • 1970-01-01
    • 1970-01-01
    • 2014-05-28
    • 1970-01-01
    相关资源
    最近更新 更多