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

时间:2016-02-19 00:51:25

标签: rest

我们假设您有一个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更简单。

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

4 个答案:

答案 0 :(得分:3)

根据@ jaco0646的要求

TL; DR

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

当前答案的问题

在我发布解决某些属性的过滤方法之前,我想给出一个quck洞察力,为什么我不同意@ jaco0646,@ yoram和@JoseMartinez目前给出的答案(这些都是相同的IMO) )

缓存响应内容

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

通过某些缓存控制标头和If-Modified-Since请求标头,客户端可以通过加载当前内容并缓存响应来影响使用缓存内容或刷新缓存。但是,带有查询参数的GET请求通常会从缓存中排除,因此会增加整体网络负载。用matrix参数替换查询参数可以快速解决这个问题,但有一种相当难闻的气味。

部分GET请求和用例

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

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

  

HTTP / 1.1定义的唯一范围单位是" bytes"。

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设计,可能不清楚某些数据是否应该是此资源的一部分或重构为其自己的资源。这里有一条经验法则,如果您需要至少两个不同资源中的某些数据重构它并将其嵌入这些资源中。

将larg(er)资源划分为hirarchy允许在更改的情况下轻松更新(在纯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"
        }
    }
}

包含/api/users/1234-5678-9123-4567/views/view-aaddresses的视图(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"
                    }
                }
            }
        ]
    }
}

其他观点(即/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表单(或某些自定义用户界面),其中包含要包含或排除的每个可用字段的复选框。在将表单数据发送到服务器时,它将检查给定属性是否已存在视图(如果存在409 Conflict)并创建可能稍后重用的新视图。您也可以命名视图,并在views部分的_links细分中包含某些选定的属性。

您可以为所有用户创建一次常规视图,并根据您的意愿重复使用,而不是为每个用户指定一个视图。

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

关于部分GET请求:

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

答案 1 :(得分:0)

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

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

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

答案 2 :(得分:0)

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

  

使用字段查询参数,该参数采用逗号分隔的字段列表来包含。例如,以下请求将检索足够的信息以显示打开的票证的已排序列表:

答案 3 :(得分:0)

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

通过添加选择查询

来完成对部分资源的请求
api/Person?$select=first_name,last_name,age