【问题标题】:How to design RESTful search/filtering? [closed]如何设计 RESTful 搜索/过滤? [关闭]
【发布时间】:2011-06-28 14:06:38
【问题描述】:

我目前正在用 PHP 设计和实现一个 RESTful API。但是,我未能成功实施我的初始设计。

GET /users # list of users
GET /user/1 # get user with id 1
POST /user # create new user
PUT /user/1 # modify user with id 1
DELETE /user/1 # delete user with id 1

到目前为止相当标准,对吧?

我的问题是第一个GET /users。我正在考虑在请求正文中发送参数来过滤列表。这是因为我希望能够指定复杂的过滤器而无需获得超长的 url,例如:

GET /users?parameter1=value1&parameter2=value2&parameter3=value3&parameter4=value4

相反,我想拥有类似的东西:

GET /users
# Request body:
{
    "parameter1": "value1",
    "parameter2": "value2",
    "parameter3": "value3",
    "parameter4": "value4"
}

它更具可读性,并为您提供了设置复杂过滤器的巨大可能性。

无论如何,file_get_contents('php://input') 没有返回 GET 请求的请求正文。我也试过http_get_request_body(),但我使用的共享主机没有pecl_http。不确定它是否会有所帮助。

我找到this question 并意识到 GET 可能不应该有请求正文。这有点不确定,但他们建议不要这样做。

所以现在我不知道该怎么做。您如何设计 RESTful 搜索/过滤功能?

我想我可以使用 POST,但这似乎不太 RESTful。

【问题讨论】:

  • 小心!!! GET 方法必须是 IDEMPOTENT,并且必须是“可缓存的”。如果你在正文中发送信息,系统如何缓存你的请求? HTTP 允许仅使用 URL 而不是请求正文来缓存 GET 请求。例如,这两个请求:example.com { test:"some" } example.com { anotherTest:"some2" } 被缓存系统认为是相同的:它们都具有完全相同的 URL
  • 只是添加,你应该 POST 到 /users(集合)而不是 /user(单个用户)。
  • 要考虑的另一点是大多数应用服务器都有记录 url 的访问日志,因此可能介于两者之间。所以在 GET 上可能会有一些意外的信息泄露。

标签: rest search


【解决方案1】:

实现 RESTful 搜索的最佳方式是将搜索本身视为一种资源。然后您可以使用 POST 动词,因为您正在创建搜索。您不必为了使用 POST 在数据库中创建某些东西。

例如:

Accept: application/json
Content-Type: application/json
POST http://example.com/people/searches
{
  "terms": {
    "ssn": "123456789"
  },
  "order": { ... },
  ...
}

您正在从用户的角度创建搜索。这个的实现细节是无关紧要的。一些 RESTful API 甚至可能不需要持久性。这是一个实现细节。

【讨论】:

  • 对搜索端点使用 POST 请求的一个重要限制是它不能被添加书签。为搜索结果添加书签(尤其是复杂的查询)非常有用。
  • 使用 POST 进行搜索可能会破坏 REST 缓存约束。 whatisrest.com/rest_constraints/cache_excerps
  • 搜索,就其本质而言,是瞬态的:数据在具有相同参数的两次搜索之间演变,因此我认为 GET 请求并不能清晰地映射到搜索模式。相反,搜索请求应该是 POST (/Resource/search),然后您可以保存该搜索并重定向到搜索结果,例如/资源/搜索/iyn3zrt。这样,GET 请求就会成功并且有意义。
  • 我不认为 post 是适合搜索的方法,正常 GET 请求的数据也会随着时间而变化。
  • 这绝对是最糟糕的答案。我不敢相信它有这么多的赞成票。这个答案解释了原因:programmers.stackexchange.com/questions/233164/…
【解决方案2】:

如果您在 GET 请求中使用请求正文,则违反了 REST 原则,因为您的 GET 请求将无法被缓存,因为缓存系统仅使用 URL。

更糟糕的是,您的 URL 无法添加书签,因为该 URL 不包含将用户重定向到此页面所需的所有信息。

使用 URL 或 Query 参数代替请求正文参数,例如:

/myapp?var1=xxxx&var2=xxxx
/myapp;var1=xxxx/resource;var2=xxxx 

事实上,HTTP RFC 7231 说:

GET 请求消息中的有效负载没有定义的语义;在 GET 请求上发送有效负载正文可能会导致某些现有实现拒绝该请求。

更多信息请查看here

【讨论】:

  • 从我的错误中吸取教训 - 我使用公认答案的建议(POSTing json)设计了一个 api,但我正在转向 url 参数。书签能力可能比你想象的更重要。就我而言,需要将流量引导到某些搜索查询(广告活动)。此外,通过 URL 参数使用 history API 更有意义。
  • 这取决于它的使用方式。如果您要链接到基于这些参数加载页面的 URL,这是有道理的,但如果主页正在执行 AJAX 调用只是为了根据过滤器参数获取数据,那么无论如何您都不能将其添加为书签,因为它是ajax 调用并没有任何影响。当然,您也可以为一个 URL 添加书签,当您去那里时,它会建立一个过滤器并将其发布到 ajax 调用中,这样就可以正常工作了。
  • @DanielLorenz 为了获得最佳用户体验,在这种情况下仍应通过 History API 更改 URL。当网站不允许使用浏览器返回功能导航到以前的页面时,我无法忍受。如果它是一个标准的服务器端生成的页面,那么让它成为书签的唯一方法就是使用 GET 请求。似乎好的 ol' 查询参数是最好的解决方案。
  • @Nathan 我想我误读了这个答案。我在谈论在 get 中使用查询字符串参数。你永远不应该在 GET 调用中使用 body 参数,因为那将完全没有用。我更多地谈论的是可以使用/标记带有查询字符串的 GET,然后在页面启动时,您可以使用这些参数来构建过滤器以 POST,使用这些参数来获取数据。在那种情况下,历史仍然可以正常工作。
  • @DanielLorenz 嗯,有道理。我想我误解了你在说什么。
【解决方案3】:

似乎资源过滤/搜索可以以 RESTful 方式实现。这个想法是引入一个名为/filters//api/filters/ 的新端点。

使用此端点 filter 可以被视为一种资源,因此可以通过POST 方法创建。这样 - 当然 - body 可用于携带所有参数,并且可以创建复杂的搜索/过滤器结构。

创建此类过滤器后,有两种可能获得搜索/过滤结果。

  1. 将返回具有唯一 ID 的新资源以及 201 Created 状态代码。然后使用此 ID 可以向/api/users/ 发出GET 请求,例如:

    GET /api/users/?filterId=1234-abcd
    
  2. 通过POST 创建新过滤器后,它不会回复201 Created,而是立即回复303 SeeOther 以及指向Location 的标头/api/users/?filterId=1234-abcd。此重定向将通过底层库自动处理。

在这两种情况下,都需要发出两个请求来获得过滤结果 - 这可能被视为一个缺点,尤其是对于移动应用程序。对于移动应用程序,我会使用单个POST 调用/api/users/filter/

如何保留已创建的过滤器?

它们可以存储在数据库中并在以后使用。它们也可以存储在一些临时存储中,例如redis 并有一些 TTL,之后它们将过期并被删除。

这个想法有什么好处?

过滤器,过滤后的结果是可缓存的,甚至可以加入书签。

【讨论】:

  • 嗯,这应该是公认的答案。您不会违反 REST 原则,并且可以对资源进行长而复杂的查询。它很好,干净且兼容书签。唯一的额外缺点是需要为创建的过滤器存储键/值对,以及已经提到的两个请求步骤。
  • 这种方法的唯一问题是,如果您在查询中有日期时间过滤器(或不断变化的值)。那么存储在 db(或缓存)中的过滤器的数量是无数的。
【解决方案4】:

我认为您应该使用请求参数,但前提是没有适当的 HTTP 标头来完成您想做的事情。 HTTP specification 没有明确表示 GET 不能有正文。然而this paper 声明:

按照惯例,当 GET 方法是 使用,所需的所有信息 标识资源被编码在 URI。没有约定 HTTP/1.1 用于安全交互(例如, 检索)客户提供 以 HTTP 实体向服务器发送数据 正文而不是在查询部分 一个 URI。这意味着为了安全 操作,URI 可能很长。

【讨论】:

  • ElasticSearch 也可以对 body 进行 GET,而且效果很好!
  • 是的,但他们控制的服务器实现可能不是互联网上的。
【解决方案5】:

当我使用laravel/php 后端时,我倾向于使用这样的东西:

/resource?filters[status_id]=1&filters[city]=Sydney&page=2&include=relatedResource

PHP 自动将 [] 参数转换为数组,因此在本例中,我将得到一个 $filter 变量,该变量包含一个过滤器数组/对象,以及一个页面和我想要急切加载的任何相关资源.

如果您使用其他语言,这可能仍然是一个很好的约定,您可以创建一个解析器来将[] 转换为一个数组。

【讨论】:

  • 这种方法看起来不错,但在 URL 中使用方括号可能会出现问题,请参阅 what-characters-can-one-use-in-a-url
  • @Sky 这可以通过 URI 编码 [] 来避免。使用这些字符的编码表示来对查询参数进行分组是一种众所周知的做法。它甚至被用于JSON:API specification
【解决方案6】:

仅供参考:我知道这有点晚了,但对于任何有兴趣的人来说。 取决于您想要的 RESTful 程度,您必须实施自己的过滤策略,因为 HTTP 规范对此并不十分清楚。我想建议对所有过滤器参数进行 url 编码,例如

GET api/users?filter=param1%3Dvalue1%26param2%3Dvalue2

我知道这很丑,但我认为这是最 RESTful 的方式,应该很容易在服务器端解析:)

【讨论】:

  • 我不会采用这种方法,因为它没有明确的论据。这里唯一可读的参数是过滤器,它有一个 URL 编码值。如果您想采用这种方法,我会将其调整为 GET api/users?mode=filter&paramA=valueA&paramB=valueB 这样您就可以使用 mode=filter, mode=search, mode=exclude, ...
  • 这在存在大量 (n) 可能参数的情况下很有用(这对前端来说不是问题),但在后端使用建议的方法时,您只有一个参数(过滤器)而不是 n (huge num) 可选参数或动态参数处理。
【解决方案7】:

无论您的初始 API 是否完全 RESTful(特别是在您刚刚处于 alpha 阶段时),不要太担心。让后端管道首先工作。你总是可以做一些 URL 转换/重写来映射,迭代地改进直到你得到足够稳定的东西来进行广泛的测试(“beta”)。

您可以定义 URI,其参数由 URI 本身的位置和约定编码,前缀为您知道您将始终映射到某物的路径。我不了解 PHP,但我会假设存在这样的工具(因为它存在于具有 Web 框架的其他语言中):

.即。在商店 #1 上使用 param[i]=value[i] for i=1..4 执行“用户”类型的搜索(使用 value1,value2,value3,... 作为 URI 查询参数的简写):

1) GET /store1/search/user/value1,value2,value3,value4

2) GET /store1/search/user,value1,value2,value3,value4

或者如下(虽然我不推荐,稍后会详细介绍)

​​>
3) GET /search/store1,user,value1,value2,value3,value4

使用选项 1,您将所有以 /store1/search/user 为前缀的 URI 映射到默认搜索 store1 下的资源的搜索处理程序(或任何 PHP 名称)(相当于 /search?location=store1&type=user

按照 API 记录和强制执行的约定,参数值 1 到 4 用逗号分隔并按此顺序显示。

选项 2 将搜索类型(在本例中为 user)添加为位置参数 #1。任何一种选择都只是一种装饰选择。

选项 3 也是可能的,但我不认为我会喜欢它。我认为在某些资源中搜索的能力应该在搜索本身之前的 URI 本身中呈现(好像在 URI 中清楚地表明搜索在资源中是特定的。)

与在 URI 上传递参数相比,这样做的优点是搜索是 URI 的一部分(因此将搜索视为一种资源,一种其内容可以 - 并且将会 - 随时间变化的资源。)缺点是该参数订单是强制性的。

一旦你做了这样的事情,你就可以使用 GET,这将是一个只读资源(因为你不能 POST 或 PUT 到它 - 当它被 GET 时它会更新)。它也是一种只有在被调用时才会存在的资源。

还可以通过将结果缓存一段时间或使用 DELETE 导致缓存被删除来为其添加更多语义。然而,这可能与人们通常使用 DELETE 的目的背道而驰(并且因为人们通常使用缓存标头来控制缓存。)

你如何去做将是一个设计决定,但这将是我的做法。它并不完美,而且我相信在某些情况下这样做不是最好的做法(特别是对于非常复杂的搜索条件)。

【讨论】:

  • 哟,如果您(某人,无论谁/无论什么)都适合否决我的回答,那么至少发表评论表明您不同意什么会伤害您的自尊心吗?我知道这是 interweebz,但是 ... ;)
  • 我没有投反对票,但问题的开头是:“我目前正在设计和实现一个 RESTful API”,而您的回答开头是“如果您最初的API 是否完全 RESTful”对我来说感觉不对。如果你正在设计一个 API,那么你就是在设计一个 API。问题在于如何最好地设计 API,而不是是否应该设计 API。
  • API 系统,首先在 API 上工作,而不是后端管道,第一个实现可以/应该只是一个模拟。 HTTP 具有传递参数的机制,您建议对其进行重新发明,但更糟糕的是(有序参数而不是名称值对)。因此投反对票。
  • @gardarh - 是的,感觉不对,但有时,它是务实的。主要目标是设计一个适用于手头业务环境的 API。如果完全 RESTFULL 方法适合手头的业务,那么就去做吧。如果不是,那就不要去做。也就是说,设计一个满足您特定业务需求的 API。四处尝试使其成为 RESTfull 作为其主要要求与询问“我如何在 X/Y 问题中使用适配器模式”没有太大区别。除非它们能解决实际的、有价值的问题,否则不要强求范式。
  • 我将资源视为某种状态的集合,而将参数视为以参数方式操纵该状态表示的一种手段。这样想,如果您可以使用旋钮和开关来调整资源的显示方式(显示/隐藏它的某些位,以不同的方式排序,等等),那么这些控件就是参数。如果它实际上是不同的资源(例如,“/albums”与“/artists”),那就应该在路径中表示它。无论如何,这对我来说是直观的。
猜你喜欢
  • 2015-05-20
  • 1970-01-01
  • 1970-01-01
  • 2010-09-17
  • 2011-12-07
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多