REST 服务和具有不同字段的同一对象的多种表示 [英] REST services and multiple representations of same object with different fields

查看:46
本文介绍了REST 服务和具有不同字段的同一对象的多种表示的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

假设您有一个 Person 对象,其中包含多个字段,例如 first_namelast_nameage,它们分别是相对较小,还有几个大字段,如life_story.

Let's say you have an Person object, with several fields like first_name, last_name, age which are relatively small, and several large fields like life_story.

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

Most calls to retrieve Person objects do not require returning the life_story, so we would rather not return it on all calls to the Person endpoint. On the other hand, when POSTing a new Person, we would like to allow the client to include the life_story field.

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

One option would be to have a Person endpoint and a PersonDetailed endpoint, where all calls (GET/POST/PUT) to the Person do not handle the life_story field, and all calls to the PersonDetailed require all fields.

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

Finally we could fudge it and make POST and PUT methods on Person to allow clients to optionally include the life_story, but to not return it when making GET calls to endpoints like

API/Person/?last_name_like=La

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

I'm not a fan of having GET, POST and PUT methods on the same endpoint return objects with different fields, but it does keep the API simpler.

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

I've been looking for examples of how people deal with issues like this, but have not found any. Can anyone point to an article or book that discusses issues like this?

推荐答案

根据@jaco0646 的要求

As requested by @jaco0646

  • 核心user 资源,带有嵌入的子资源,如地址、群组、帖子或 pm.(/api/v1/users/{user_uuid})
  • users 还将包含一个名为 views 的嵌入式资源,用于处理当前注册的视图 (/api/v1/users/{user_uuid}/views/{some_view})
  • view 是使用 POST 请求(即来自 HTML 表单)创建的,包括选定的子资源
  • 每个视图都包含核心user 数据和所选字段的数据
  • 部分GET请求可以使用,如果所有视图都以核心user数据开始,只下载所需的数据;虽然可能有其局限性
  • core user resource with embedded sub-resoruces like address, groups, posts or pm. (/api/v1/users/{user_uuid})
  • users will also contain an embedded resource called views which handles the currently registered views (/api/v1/users/{user_uuid}/views/{some_view})
  • A view is created using POST request (i.e. from a HTML form) including the selected sub-resources
  • Each view contains the core user data and the data for the selected fields
  • Partial GET request may be used if all views start with the core user data to only download the required data; though may have its limits

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

Before I post my approach to tackle the filtering on certain properties, I want to give a quck insight why I do not agree with the currently given answers by @jaco0646, @Yoram and @JoseMartinez (which are all rather the same IMO)

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

HTTP tries to reduce the network overhead by cacheing responses. A second lookup for the same resource should in best case result in a lookup from the local cache instead of actually querying and downloading the result from the server directly. This is especially helpful if the resource data does not change often.

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

With certain cache-control header and If-Modified-Since request header a client can take influence on wheter to use a cached content or refresh the cache by loading the current content and cache the response instead. However, GET requests with query parameter are often said to be excluded from caching, which is more of an urban legend than the actual truth. Certain implementations, however, may avoid caching of such resources though. By RFC 7234 a cache should use the effective request URI to reconstruct stored responses, which by default is the target URI including any query, matrix and path parameters. As such the whole URI is considered to be the key used to store and access responses.

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

As mentioned by jaco in his post, the HTTP Protocol defines, besides the standard GET and conditional GET request, also a partial GET request which allows a client to request only a part of a resource instead of the full resource.

虽然这听起来不错,但是部分 GET 请求至少在 HTTP/1.1 中具有以下限制:仅适用于字节.

While this may sound great to start with, a partial GET request however has, at least in HTTP/1.1, the limitation that it only works on bytes.

HTTP/1.1 定义的唯一范围单位是字节".

The only range unit defined by HTTP/1.1 is "bytes".

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

The Range header allows to add multiple byte segments to the request to include multiple segments within the response:

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

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

The partial request asks to download only the bytes between (and including) 500-700 and everything from byte 1200 till the end.

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

Usually a partial GET request is used to resume a broken download or for buffering a running stream as the exactly downloaded bytes are already known. But, how do you specify in advance the byte ranges of each filter-field? Without a-priori knowledge I don't think this will work.

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

In case there are many fields which may be available for filtering, using a GET request with query or matrix parameter may cause certain browser issues as some browsers have a limitation of 2000 characters.

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

While this may not have an impact on the OPs issue, an other user who requires exhaustive filtering properties may run into this issue though.

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

ReST focus is on resources and the methods HTTP protocol offers to interact with them.

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

A user-resource i.e. has certain "core" data like the user name, an id and maybe other domain specific things. But it also has additional data like the address, ... which may be part of the user resource as well.

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

Instead of mingling every property into a single entity, ReSTfull applications try to have plenty of resources. Like in the sample above user and address are just two to name but there are many more for sure. If you start working on a ReSTfull design it might not be clear if certain data should be part of this resource or refactored to its own resource. Here a rule of thumb is, if you need certain data in at least two different resources refactor it and embedd it within these resources.

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

Dividing larg(er) resources into a hirarchy allows to easily update (in the pure HTTP sense of replacing what is available currently at resource X with the new content) sub-resources in case of changes (like an address change of a user) while having one big resource to handle all data requires to send the whole entity body (if used properly) to the server instead of only the change.

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

Plenty of "ReSTfull" services exchange data in application/xml or application/json format. However, both do not convey much semantic. They just lay out the used syntax rules which might be validated on client side. But they do not give any hint on the actual content. Therefore a client has to have also a-priori knowledge on how to process data received in one of these formats.

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

If JSON is the representation format of your choice, I'd use JSON HAL (application/hal+json) instead as this defines core data, links and embedded content which is quite usefull especially for the presented scenario IMO.

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

The proposed approach has a core user resource which embedds the certain sub-resoruces like address, groups, posts or pm. It will also contain an embedded resource called views which handles the currently registered views for either a user or for users in general. A view is created by sending a POST request (i.e. from a HTML form) including the selected sub-resources to include within the response.

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

The core resource is a user resource, which might be available at /api/v1/users/{user_uuid} and by default only includes the user core data and links to the other resources

{
    "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 可以是以下之一:addresses, groups, posts, ...

Any sub-resource is available via the users resource URI: /api/v1/users/1234-5678-9123-4567/{sub_resource}, where sub_resource may be one of the following: addresses, groups, posts, ...

地址的实际子资源,即可能看起来像这样

The actual sub-resource for an address i.e. may look like this

{
    "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"
        }
    }
}

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

while the user has two posts like these

{
    "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)> 可能看起来像这样:

A view (/api/users/1234-5678-9123-4567/views/view-a) which contains addresses and posts may look like this:

{
    "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)只能包含所选用户发布的 帖子:

An other view (i.e. /api/users/1234-5678-9123-4567/views/view-b) may only include posts done by the selected user:

{
    "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 段中包含某些选定的属性.

On invoiking /api/users/1234-5678-9123-4567/views you may show a list of currently available views and also a HTML form (or some custom UI) where you have checkboxes for each available field you want to include or exclude. On sending the form data to the server, it will check if for the given properties already a view exists (if so 409 Conflict) and creates a new view which might be reused later. You might also name the views and include certain selected properties within the views segment within the _links section.

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

Instead of specifying a view per user, you can also create a general view once for all users and reuse them to your will.

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

As the views have no query parameters the whole response is cacheable. As you create a view using a POST request (if idempotancy is an issue use an empty POST request followed by a PUT request) you are good to go for almost infinite parameters. This HAL similar dialect uses its own logic for views. It, therefore, might be also a good idea to create an own content type like: application/vnd+users.views+hal+json

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

As the core user data is the same for every view, it might be possible to use the length of the core data (minus the closing bracket and any whitespace characters after the second last bracket) and issue a partial GET request to the server. It should respond with only the embedded data (and the final closing bracket), though I'm not sure if current browsers are actually able to update the current data accordingly, especially if certain bytes of the known content need to be removed like the final bracket of the core user data.

这篇关于REST 服务和具有不同字段的同一对象的多种表示的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆