REST 服务和具有不同字段的同一对象的多种表示

bpe*_*kes 5 rest

假设您有一个Person对象,其中有几个字段,例如first_name, last_name,age它们相对较小,还有几个大字段,例如life_story。

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

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

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

API/Person/?last_name_like=La
Run Code Online (Sandbox Code Playgroud)

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

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

Rom*_*ner 6

根据@jaco0646 的要求

TL; 博士

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

当前答案的问题

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

响应内容的缓存

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

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

部分 GET 请求和用例

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

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

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

的Range报头允许到多个字节段添加到请求中,包括在响应中的多个段:

GET /someResource HTTP/1.1
Host: http://some-host.com/
Range: 500-700,1200-
Run Code Online (Sandbox Code Playgroud)

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

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

URL 大小限制

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

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

资源和子资源

ReST 的重点是资源和 HTTP 协议提供的与它们交互的方法。

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

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

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

实体格式

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

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

建议的解决方案

提议的方法有一个核心user资源,它嵌入了某些子资源,如地址、群组、帖子或下午。它还将包含一个名为的嵌入式资源views,该资源为用户或一般用户处理当前注册的视图。Aview是通过发送一个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" }
        ]
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

{
    "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"
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

而用户有两个这样的帖子

{
    "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"
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

视图 ( /api/users/1234-5678-9123-4567/views/view-a) 包含addresses并且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": {
        "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"
                    }
                }
            }
        ]
    }
}
Run Code Online (Sandbox Code Playgroud)

其他视图(即/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"
                    }
                }
            }
        ]
    }
}
Run Code Online (Sandbox Code Playgroud)

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

除了为每个用户指定一个视图之外,您还可以为所有用户创建一个通用视图,并根据您的意愿重复使用它们。

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

关于部分GET请求:

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