如何在RESTful服务设计中正确支持部分创建记录?

时间:2013-01-23 07:42:34

标签: api rest

我正在设计一个restful服务来支持一个现有的向导,该向导允许用户提交一个新的资源(让我们称之为customer),但是要分片。

此向导会在用户提交每个页面时对其进行验证,但仅针对用户提交的页面进行验证。仅当用户选择提交客户进行最终处理时,它才会对整个对象执行完整的验证传递。

为了简化向导,并允许我们在添加更多字段时在维护版本中随机播放UI,我们还没有将向导的结构编入资源。客户不会“卷起”向导呈现数据的方式。

以某种方式设计RESTful服务是否奇怪,因为资源的命名子文档不一定在该资源的完整文档中分层显示(或者至少不以相同的方式)?

说我的向导页面是:

  • 联系信息
  • 食物偏好
  • 恐惧名单

然后这是一个示例客户对象:

// Note that the wizard page groupings don't show up explicitly
{
    customer: {
        firstName: "Pilsner",
        lastName: "Dopplebock",
        emailAddress: "nextguest@hotelcalifornia.com",
        addressLine1: "123 Fleece Place",
        addressLine2: ""
        town: "Ibinjad",
        region: "North Dakota",
        postalCode: "12123",
        homePhoneNumber: "2123234124",
        faxPhoneNumber: null,
        meatPreference: "well-done",
        allergies: "shellfish",
        fears: [
            "banshees",
            "baths",
            "sleeveless shirts"
        ]
    }
}

说出资源的基本URL是:

http://www.somewhere.com/customers
http://www.somewhere.com/customers/{id}

创建以下宁静的URL /方法是否奇怪或错误,即使customer实际上没有按照它们所暗示的方式进行细分?

http://www.somewhere.com/customers/contactinformation (POST)
http://www.somewhere.com/customers/{id}/contactinformation (POST, or PUT for update? maybe GET)
http://www.somewhere.com/customers/{id}/foodpreference (POST, or PUT for update?, maybe GET)
http://www.somewhere.com/customers/{id}/fears (POST to add a single item?, maybe PUT for a batch?, maybe GET)

如果我一次没有整个资源,我曾考虑过使用备用向导网址,但在我看来,这似乎不适合以资源为导向:

http://www.somewhere.com/customerwizard/submitcontactinformation (POST)
http://www.somewhere.com/customerwizard/{customer-id}/submitcontactinformation
http://www.somewhere.com/customerwizard/{customer-id}/submitfoodpreference
http://www.somewhere.com/customerwizard/{customer-id}/fears

(可能是第二个问题,虽然相关):对于集合式资源有一个count子属性是不是很奇怪,它不一定出现在主集合上?我想这样做是为了支持分页视图......

http://www.somewhere.com/customers/count (GET)

3 个答案:

答案 0 :(得分:0)

我不认为这个问题与引用的问题重复。您不是要求对现有资源执行部分更新,而是要求服务器验证“部分”资源(我认为这些资源本身就是整个资源,而不是您以后通常会向用户提供的资源)。

在这种情况下,REST不一定是正确的选择。 REST旨在优化潜在分布式受众多次访问的静态或半静态资源的读取访问。为了帮助你,你能否回答这些问题:

  1. 您是否希望在初始请求 - 响应通信完成后获取验证结果?
  2. 您是否会提交两次相同的数据(来自不同用户或来自同一用户)?
  3. 如果是,那么响应是否总是相同的,即验证算法是否确定?
  4. 是否可以将所有这些数据以未加密的方式发送给其他人?
  5. 如果对所有这些人回答,那么REST非常合适。如果您对所有三个人都回答,那么REST就不合适了。混合的答案在某种程度上混淆了这个决定。

    如果我处于你的位置,没有进一步的数据,我会先用HTTPS实现它作为RPC接口,直到我知道数据缓存和安全要求是什么,所以我会知道哪些部分系统将受益于缓存(或者仅在最终用户的机器上,或者作为未加密和中间人可以传送的公共资源。

    有一个名为Classification of HTTP-based APIs的强大资源可能有助于确定REST是否实际上是您希望在API设计中遵循的路径。请记住,选择替代设计没有“错误”,这是一种权衡。根据各自的优点和缺点做出明智的决定。

答案 1 :(得分:0)

网址如/customers/{id}/contactinformation等等并不奇怪。您可能想要问自己的一个问题是,在写入时将Customer实体分解为单独的片段是否有意义这一事实并不意味着它们可能在阅读时更好地单独提供。它肯定会使任何HTTP缓存更加明智。例如,如果您PUT到实体片段然后GET父节点,则片段的后续PUT仅使片段无效,然后父节点可以提供陈旧数据。获取较小的父实体(其中包含指向每个实体片段的链接)然后获取每个片段更为直接,在这种情况下,片段的PUT会正确地提示后续GET检索新的副本。

答案 2 :(得分:0)

与 Nicholas Shanks 相比,我认为类似向导的系统符合 REST 背后的理念。在 Web 上看到这样的交互概念并不少见,即通常应用于结账页面,在页面上您输入客户信息,在下一页上输入交货地址,在第三页上输入您的付款数据,然后是确认页面,确认后将触发实际订单。

Jim Webber pointed out,在 REST 架构中,您主要实现域应用程序协议(客户端将遍历的状态机,如果您愿意的话)该客户端将跟随,因为它们获取由服务器,通过链接或类似于 HTML 表单的表单表示。这个概念被概括为 HATEOAS。

因此,上述 REST 架构中的结账系统可能如下所示:将商品放入购物篮后,服务器会为您提供附加链接,这些链接使用一些链接关系(伪 HAL 表示)进行了注释:

{
    ...,
    "_links": {
        "self": {
            "href": "https://..."
        },
        "create-form": {
            "href": "https://shop.acme.com/checkout-wizard-p1"
        },
        "https://acme.com/rel/checkout": {
            "href": "https://shop.acme.com/checkout-wizard-p1"
        },
        ...
    }
}

URI 本身与此处无关,因为 URI 的拼写在 REST 架构中并不重要,只要它符合 RFC 3986 (URI) 中概述的规则即可。为简单起见,它也可以以 UUID 结尾,而不是此处给出的 checkout-wizard-p1 名称。相反,重点是这里给出的链接关系名称,即 create-form,它符合在 IANA 处注册的标准化链接关系和一个自定义链接关系 https://acme.com/rel/checkout,它遵循Web Linking (RFC 5988) 概述的扩展机制。引入这种间接性允许服务器用其他形式替换实际的目标 URI,客户端仍然可以继续完成他们的任务。

不幸的是,与 RFC 5988 定义的 Link 标头不同,它可以在同一个 URI 上定义多个关系名称(参见 examples; Link: <.../checkout-wizard-p1>; rel="create-form https://acme.com/rel/checkout"),HAL JSON 只允许为每个 URI AFAIK 定义一个链接关系名称。

对于任意客户端,链接关系名称只是一个任意字符串。此外,Web 链接扩展机制不必首先指向人类可读的文档,尽管稍后可能会通过更新或插件添加对此类关系名称的支持。这里的重点是,了解某个链接关系名称的客户端可以相应地采取行动,并将使用 URI 仅向其发送请求。链接关系可以根据规则引擎在满足特定条件时触发伴随的 URI 使用,即特定客户端状态的可用性和指向同一 URI 的链接关系的存在,如本例中给定的链接-关系名称。

在请求用于 create-formhttps://acme.com/rel/checkout 关系的 URI 内容时,服务器可能会返回一个 HAL-FORMS 表示,允许客户端输入所需的客户信息:< /p>

{
  "_links": {
    "self": {
        "href": "https://shop.acme.com/checkout-wizard-p1"
    },
    "_templates": {
        "default": {
            "contentType": "application/x-www-form-urlencoded",
            "key": "default",
            "method": "POST",
            "properties": [
                { 
                    "name": "firstName",
                    "prompt": "First Name",
                    "readOnly": false,
                    "regex": "^[A-Z][a-z]{1,24}$",
                    "required": true,
                    "templated": false,
                    "value": "",
                    "maxLength": 25,
                    "minLength": 2,
                    "placeholder": "Your first name",
                    "type": "text"
                },
                ...
            ],
            "target": "https://shop.acme.com/tmp/ea2b3fb1-c640-40e2-b16a-f7433dee6ba2",
            "title": "Checkout Wizard - Customer Information"
        }
    }
  }
}

与传统的 HTML forms 类似,HAL-FORMS 教导客户端有关将请求发送到的目标 URI、要使用的 HTTP 操作以及在发送请求之前将请求编组到的媒体类型.与 HTML 相反,HAL-FORMS 在这里默认为 application/json 而不是 application/x-www-form-urlencoded。令人惊讶的是,HAL-FORMS 仅支持这两种媒体类型。

资源支持的整体结构也通过 properties 属性中包含的元素进行教导。就像在 HTML 表单中一样,可以定义所呈现输入的类型,可以是 texthiddentextareasearchtel 之一, urlemailpassworddatemonthweektimedatetime-local、{{ 1}}、numberrange。通过可选的 color 元素 UI 控件,例如(多选)选项、复选框和单选按钮也可以表示。

一旦客户端输入其数据并将其发送到服务器,服务器就可以简单地响应下一个 HAL-FORMS 表示,要求进一步输入,例如送货地址等。在这里,交互设计者可能倾向于通过使用 options 直接更新临时资源(即本场景中的 https://shop.acme.com/tmp/ea2b3fb1-c640-40e2-b16a-f7433dee6ba2),与 HTML 表单相比,HAL-FORMS 支持它,尽管这需要传递所有以前的数据也是如此,这使向导或多或少......多余。下一个方法是使用 PUT 作为 HTTP 操作来对临时资源执行部分更新,但由于 HAL-FORMS 目前仅支持 PATCHapplication/json 没有办法通知服务器有关将临时资源转换为所需结果的(隐式)指令,就像使用 application/json-patch+jsonapplication/merge-patch+json 一样。

因此,我更喜欢在这里使用 application/x-www-form-urlencoded 并为每个向导页面创建新的临时资源。通过支持隐藏属性,可以轻松地传递所有先前传输的属性,尽管不是传递所有数据,但仅提供临时资源的 URI 作为隐藏属性可能就足够了。最后一步可以将这些临时资源中的信息“合并”到一个有凝聚力的状态,对受影响的临时资源进行清理,并将信息呈现给用户最后确认,在这种情况下,实际资源被创建。

虽然通过 POST 获得的实际表单资源是可缓存的,但临时资源不是,因为它们只能通过不安全的操作进行操作,这无论如何都会使(中间)缓存中存储的表示无效。对表单资源进行缓存并不是一个坏主意,因为资源的新属性不会一直引入,因此缓存可以减少将该表示从服务器传输到客户端的开销。如果表单更新完成,对该资源的更新,即通过 GETPUT 上传新版本,将自动使该资源和服务器客户端的任何存储表示无效版本,第一次绕过缓存,直到响应被添加到缓存中。

虽然 Nicholas 提到只有在他的 4 点都可以回答是的情况下才应该使用 REST,其中一些点,例如文档加密,更多地针对传输通道(即基于 HTTP 或 HTTPS 的 TLS 或媒体-类型协商)而不是 REST 架构提出的交互概念,在我看来,REST 的目标应该是您的服务应该持续数年,支持大量不同的客户端并支持未来的发展,例如引入新领域在未来的资源上,而不必担心破坏客户。

长话短说,通过 HATEOAS 设计类似向导的交互确实有意义,尤其是当使用类似表单的表示来教导客户端将数据发送到哪里、使用什么 HTTP 方法以及发送数据的表示格式时数据输入。表单还可以帮助客户了解资源所支持的属性。使用的棘手部分是如何组合通过向导的不同页面提供的数据并呈现给客户端。虽然 PATCHPUT 一开始可能很有吸引力,但由于媒体类型或 HTTP 操作本身的限制,从更狭隘的角度看,它们可能并不理想。